[ClickOnce] ClickOnce 如何切換使用管理員和程式運行中自動更新

這個看似簡單的運作機制,看似相當的簡單,實作的過程中卻是傷痕累累,主要的原因是 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);
    }
}

https://github.com/yaochangyu/sample.dotblog/blob/master/ClickOnce/WinForm/Lab.ClickOnceRunAdmin/WindowsFormsApp1/Program.cs

 

AppManager

要講完裡面的代碼實在太累,也應該沒有必要,所以我挑出我認為相對重要的提一下,若有問題歡迎在底下留言

AppManager 詳細代碼如下

https://github.com/yaochangyu/sample.dotblog/blob/master/ClickOnce/WinForm/Lab.ClickOnceRunAdmin/WindowsFormsApp1/AppManager.cs

 

程序不重覆運行

剛提到這樣的模式會執行兩隻應用程式(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

Image result for microsoft+mvp+logo