這個看似簡單的運作機制,看似相當的簡單,實作的過程中卻是傷痕累累,主要的原因是 ApplicationDeployment 類別在管理員模式下無法執行,為了解決這問題我動了點手腳,也花了不少時間
本文連結
ClickOnce 工作流程
1.定期檢查是否有更新
2.ClickOnce 用 Process 執行另外一隻具有管理員身分的應用程式,然後無窮等待這隻 Admin Process,主要的工作就交給 Admin Process,ClickOnce App 則是隱藏起來
3.檢查到更新則關閉 Admin Process
PS.ApplicationDeployment 類別在管理員模式下無法執行,ClickOnce 一值在等待,所以不用擔心兩隻程式會碰撞。
主程式
上面的 ClickOnce 工作流程實作如下
[STAThread] private static void Main(string[] args) { GlobalErrorHandler(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); if (AppManager.IsRunningAsAdministrator()) { if (AppManager.IsAdminAlreadyRunning()) { Environment.Exit(1); return; } var arg = Configuration.Configure<CommandArgs>().CreateAndBind(args); //TODO:執行緒堵塞 Application.Run(new Form1(arg)); } else { if (AppManager.IsClickOnceAlreadyRunning()) { Environment.Exit(1); return; } var title = AppManager.GetTitle(); s_process = AppManager.CreateAdminProcess($"/Title {title}"); AppManager.CheckAndUpdate(); //TODO:另一條執行緒檢查更新 StartCheckUpdateAsync(10000); //TODO:執行管理員身分,主執行緒堵塞 AppManager.RunAsAdminAndWaitForExit(s_process); } }
AppManager
要講完裡面的代碼實在太累,也應該沒有必要,所以我挑出我認為相對重要的提一下,若有問題歡迎在底下留言
AppManager 詳細代碼如下
程序不重覆運行
剛提到這樣的模式會執行兩隻應用程式(ClickOnce、Admin),為了避免應用程式重複執行所以用 Mutex 來堵塞主執緒,確保一台電腦同時間共有兩隻程式會運行
public static bool IsAdminAlreadyRunning() { var exeName = GetExecuteName(); var canCreateNew = false; try { s_mutexAdmin = new Mutex(true, $"Global\\{exeName}.Admin", out canCreateNew); } catch (Exception) { } if (canCreateNew) { s_mutexAdmin.ReleaseMutex(); } else { var msg = "Only one instance at a time."; MessageBox.Show(msg, "Info", MessageBoxButtons.OK, MessageBoxIcon.Information); Environment.Exit(1); } return !canCreateNew; } public static bool IsClickOnceAlreadyRunning() { var exeName = GetExecuteName(); var canCreateNew = false; try { s_mutexClickOnce = new Mutex(true, $"Global\\{exeName}.ClickOnce", out canCreateNew); } catch (Exception) { } if (canCreateNew) { s_mutexClickOnce.ReleaseMutex(); } else { var msg = "Only one instance at a time."; MessageBox.Show(msg, "Info", MessageBoxButtons.OK, MessageBoxIcon.Information); Environment.Exit(1); } return !canCreateNew; }
取得應用程式資訊
應用程式用 ClickOnce 部署之後,可以透過 Mainfest 來取得部署的資訊,比如產品名稱、發布者、桌面捷徑、選單捷徑、版號,我需要這些路徑建立 Admin Process
public static AppDeployInfo GetCurrentInfo() { var result = new AppDeployInfo(); if (ApplicationDeployment.IsNetworkDeployed) { var deployment = ApplicationDeployment.CurrentDeployment; var manifest = GetManifest(); try { var uri = deployment.UpdateLocation; result = GetRemoteInfo(uri); } catch (Exception e) { var activationUri = deployment.ActivationUri; result.ProductName = manifest.Product; result.PublisherName = manifest.Publisher; result.CurrentVersion = deployment.CurrentVersion; result.HasUpdateVersion = result.UpdateVersion > result.CurrentVersion; result.UpdatedApplicationFullName = deployment.UpdatedApplicationFullName; result.UpdateLocation = deployment.UpdateLocation; result.UpdateVersion = deployment.UpdatedVersion; result.Arguments = GetArgs(); result.DesktopShortcutPath = GetDesktopShortcutPath(manifest.Product); result.StartMenuShortcutPath = GetStartMenuShortcutPath(manifest.Publisher, manifest.Product); result.ActivationLocation = activationUri == null ? Application.ExecutablePath : activationUri.ToString(); } } else { var assembly = Assembly.GetEntryAssembly(); var fileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location); result.ProductName = fileVersionInfo.ProductName; result.CurrentVersion = new Version(fileVersionInfo.ProductVersion); result.ActivationLocation = assembly.Location; } return result; }
開發模式是拿不到 ApplicationDeployment 的東西,這要用 ClickOnce 發行後才拿的到,我們可以用 FileVersionInfo 來取得產品名稱、版號,這得在專案內設定 Assembly
切換管理員身分
建立管理員身分的 Process:CreateAdminProcess()
執行 Admin Process:process.Start()
等待 Admin Process 結束:process.WaitForExit()
public static void RunAsAdminAndWaitForExit(Process process = null) { if (IsRunningAsAdministrator()) { return; } try { if (process == null) { process = CreateAdminProcess(); } process.Start(); process.WaitForExit(); Environment.Exit(1); } catch (Win32Exception ex) { if (ex.NativeErrorCode == 1223) //The operation was canceled by the user. { MessageBox.Show("Why did you not selected Yes?\r\nLet me one more time", "Info", MessageBoxButtons.OK, MessageBoxIcon.Stop); } else { throw new Exception("Something went wrong :-("); } } } public static Process CreateAdminProcess(string args = "") { var processStartInfo = new ProcessStartInfo(Assembly.GetEntryAssembly().CodeBase); var process = new Process(); processStartInfo.UseShellExecute = true; processStartInfo.Verb = "runas"; processStartInfo.Arguments = args; processStartInfo.CreateNoWindow = true; process.StartInfo = processStartInfo; return process; }
定期檢查更新
Admin Process 手動關閉後,ClickOnce App 才會跟在後面關,除了手動關之外,還有,更新後也要關
private static void StartCheckUpdateAsync(int checkInterval) { if (!ApplicationDeployment.IsNetworkDeployed) { return; } lock (s_lock) { if (s_isStart) { return; } s_isStart = true; Task.Factory.StartNew(() => { CheckUpdateWithEvent(checkInterval); }); } } private static void CheckUpdateWithEvent(int checkInterval) { while (s_isStart) { if (!ApplicationDeployment.IsNetworkDeployed) { return; } try { var hasUpdate = ApplicationDeployment.CurrentDeployment.CheckForUpdate(); if (hasUpdate) { var updated = ApplicationDeployment.CurrentDeployment.Update(); s_isStart = false; s_process.Kill(); } } catch (Exception ex) { s_logger.Error(ex); } finally { SpinWait.SpinUntil(() => !s_isStart, checkInterval); } } }
專案位置
https://github.com/yaochangyu/sample.dotblog/tree/master/ClickOnce/WinForm/Lab.ClickOnceRunAdmin
PS.VS IDE 必須要用管理員模式開啟,才能中斷除錯喔
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET