Как начать удаление приложения ClickOnce из приложения?

Могу ли я надежно инициировать отмену установки приложения ClickOnce, из приложения?

Другими словами, я хочу дать пользователю большую кнопку "Удалить меня сейчас" на одной из форм. Когда пользователь нажимает кнопку, я хочу начать процесс удаления Windows для этого приложения и, возможно, закрыть приложение.

Причина: мы заканчиваем приложение ClickOnce и хотим упростить удаление , как и при установке. Мы не хотим отправлять их по пути "Добавить или Remove Programs" и риск их потери или отвлечения.

Можно ли это сделать надежно?

Ответ 1

Я бы рекомендовал проверить эту статью MSDN здесь. В нем объясняется, как программно удалить приложение (и переустановить его с нового URL-адреса, если вы хотите):

http://msdn.microsoft.com/en-us/library/ff369721.aspx

Это вариант в записи блога jameshart, но он включает в себя пару исправлений, которые вы собираетесь использовать. Есть загрузка кода как в С#, так и в VB.

На самом деле вы можете просто нажать обновление и удалить приложение самостоятельно, вам даже не нужно, чтобы пользователь говорил "хорошо".

Ответ 2

Я просто оставлю это здесь для всех, кто ищет код, и обнаруживает, что ссылки на скачивание в других ответах мертвы:

https://code.google.com/p/clickonce-application-reinstaller-api

Изменить: добавлен код из Reinstaller.cs и инструкции из ReadMe.txt

/* ClickOnceReinstaller v 1.0.0
 *  - Author: Richard Hartness ([email protected])
 *  - Project Site: http://code.google.com/p/clickonce-application-reinstaller-api/
 * 
 * Notes:
 * This code has heavily borrowed from a solution provided on a post by
 * RobinDotNet (sorry, I couldn't find her actual name) on her blog,
 * which was a further improvement of the code posted on James Harte's
 * blog.  (See references below)
 * 
 * This code contains further improvements on the original code and
 * wraps it in an API which you can include into your own .Net, 
 * ClickOnce projects.
 * 
 * See the ReadMe.txt file for instructions on how to use this API.
 * 
 * References:
 * RobinDoNet Blog Post:
 * - ClickOnce and Expiring Certificates
 *   http://robindotnet.wordpress.com/2009/03/30/clickonce-and-expiring-certificates/
 *   
 * Jim Harte Original Blog Post:
 * - ClickOnce and Expiring Code Signing Certificates
 *   http://www.jamesharte.com/blog/?p=11
 */


using Microsoft.Win32;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Deployment.Application;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Policy;
using System.Windows.Forms;
using System.Xml;

namespace ClickOnceReinstaller
{
    #region Enums
    /// <summary>
    /// Status result of a CheckForUpdates API call.
    /// </summary>
    public enum InstallStatus { 
        /// <summary>
        /// There were no updates on the server or this is not a ClickOnce application.
        /// </summary>
        NoUpdates, 
        /// <summary>
        /// The installation process was successfully executed.
        /// </summary>
        Success, 
        /// <summary>
        /// In uninstall process failed.
        /// </summary>
        FailedUninstall, 
        /// <summary>
        /// The uninstall process succeeded, however the reinstall process failed.
        /// </summary>
        FailedReinstall };
    #endregion

    public static class Reinstaller
    {
        #region Public Methods

        /// <summary>
        /// Check for reinstallation instructions on the server and intiate reinstallation.  Will look for a "reinstall" response at the root of the ClickOnce application update address.
        /// </summary>
        /// <param name="exitAppOnSuccess">If true, when the function is finished, it will execute Environment.Exit(0).</param>
        /// <returns>Value indicating the uninstall and reinstall operations successfully executed.</returns>
        public static InstallStatus CheckForUpdates(bool exitAppOnSuccess)
        {
            //Double-check that this is a ClickOnce application.  If not, simply return and keep running the application.
            if (!ApplicationDeployment.IsNetworkDeployed) return InstallStatus.NoUpdates;

            string reinstallServerFile = ApplicationDeployment.CurrentDeployment.UpdateLocation.ToString();

            try
            {
                reinstallServerFile = reinstallServerFile.Substring(0, reinstallServerFile.LastIndexOf("/") + 1);
                reinstallServerFile = reinstallServerFile + "reinstall";
#if DEBUG
                Trace.WriteLine(reinstallServerFile);

#endif          
            } 
            catch 
            {
                return InstallStatus.FailedUninstall;
            }
            return CheckForUpdates(exitAppOnSuccess, reinstallServerFile);
        }

        /// <summary>
        /// Check for reinstallation instructions on the server and intiate reinstall.
        /// </summary>
        /// <param name="exitAppOnSuccess">If true, when the function is finished, it will execute Environment.Exit(0).</param>
        /// <param name="reinstallServerFile">Specify server address for reinstallation instructions.</param>
        /// <returns>InstallStatus state of reinstallation process.</returns>
        public static InstallStatus CheckForUpdates(bool exitAppOnSuccess, string reinstallServerFile)
        {
            string newAddr = "";

            if (!ApplicationDeployment.IsNetworkDeployed) return InstallStatus.NoUpdates;

            //Check to see if there is a new installation.
            try
            {
                HttpWebRequest rqHead = (HttpWebRequest)HttpWebRequest.Create(reinstallServerFile);
                rqHead.Method = "HEAD";
                rqHead.Credentials = CredentialCache.DefaultCredentials;
                HttpWebResponse rsHead = (HttpWebResponse)rqHead.GetResponse();

#if DEBUG
                Trace.WriteLine(rsHead.Headers.ToString());
#endif
                if (rsHead.StatusCode != HttpStatusCode.OK) return InstallStatus.NoUpdates;

                //Download the file and extract the new installation location
                HttpWebRequest rq = (HttpWebRequest)HttpWebRequest.Create(reinstallServerFile);
                WebResponse rs = rq.GetResponse();
                Stream stream = rs.GetResponseStream();
                StreamReader sr = new StreamReader(stream);

                //Instead of reading to the end of the file, split on new lines.
                //Currently there should be only one line but future options may be added.  
                //Taking the first line should maintain a bit of backwards compatibility.
                newAddr = sr.ReadToEnd()
                    .Split(new string[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries)[0];

                //No address, return as if there are no updates.
                if (newAddr == "") return InstallStatus.NoUpdates;
            }
            catch
            {
                //If we receive an error at this point in checking, we can assume that there are no updates.
                return InstallStatus.NoUpdates;
            }


            //Begin Uninstallation Process
            MessageBox.Show("There is a new version available for this application.  Please click OK to start the reinstallation process.");

            try
            {
                string publicKeyToken = GetPublicKeyToken();
#if DEBUG
                Trace.WriteLine(publicKeyToken);
#endif

                // Find Uninstall string in registry    
                string DisplayName = null;
                string uninstallString = GetUninstallString(publicKeyToken, out DisplayName);
                if (uninstallString == null || uninstallString == "") 
                    throw new Exception("No uninstallation string was found.");
                string runDLL32 = uninstallString.Substring(0, uninstallString.IndexOf(" "));
                string args = uninstallString.Substring(uninstallString.IndexOf(" ") + 1);

#if DEBUG
                Trace.WriteLine("Run DLL App: " + runDLL32);
                Trace.WriteLine("Run DLL Args: " + args);
#endif
                Process uninstallProcess = Process.Start(runDLL32, args);
                PushUninstallOKButton(DisplayName);
            }
            catch
            {
                return InstallStatus.FailedUninstall;
            }

            //Start the re-installation process
#if DEBUG
            Trace.WriteLine(reinstallServerFile);
#endif

            try
            {
#if DEBUG
                Trace.WriteLine(newAddr);
#endif
                //Start with IE-- other browser will certainly fail.
                Process.Start("iexplore.exe", newAddr);             
            }
            catch
            {
                return InstallStatus.FailedReinstall;
            }

            if (exitAppOnSuccess) Environment.Exit(0);
            return InstallStatus.Success;
        }
        #endregion

        #region Helper Methods
        //Private Methods
        private static string GetPublicKeyToken()
        {
            ApplicationSecurityInfo asi = new ApplicationSecurityInfo(AppDomain.CurrentDomain.ActivationContext);

            byte[] pk = asi.ApplicationId.PublicKeyToken;
            StringBuilder pkt = new StringBuilder();
            for (int i = 0; i < pk.GetLength(0); i++)
                pkt.Append(String.Format("{0:x2}", pk[i]));

            return pkt.ToString();
        }
        private static string GetUninstallString(string PublicKeyToken, out string DisplayName)
        {
            string uninstallString = null;
            string searchString = "PublicKeyToken=" + PublicKeyToken;
#if DEBUG
            Trace.WriteLine(searchString);
#endif
            RegistryKey uninstallKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall");
            string[] appKeyNames = uninstallKey.GetSubKeyNames();
            DisplayName = null;
            foreach (string appKeyName in appKeyNames)
            {
                RegistryKey appKey = uninstallKey.OpenSubKey(appKeyName);
                string temp = (string)appKey.GetValue("UninstallString");
                DisplayName = (string)appKey.GetValue("DisplayName");
                appKey.Close();
                if (temp.Contains(searchString))
                {
                    uninstallString = temp;
                    DisplayName = (string)appKey.GetValue("DisplayName");
                    break;
                }
            }
            uninstallKey.Close();
            return uninstallString;
        }
        #endregion

        #region Win32 Interop Code
        //Structs
        [StructLayout(LayoutKind.Sequential)]
        private struct FLASHWINFO
        {
            public uint cbSize;
            public IntPtr hwnd;
            public uint dwFlags;
            public uint uCount;
            public uint dwTimeout;
        }

        //Interop Declarations
        [DllImport("user32.Dll")]
        private static extern int EnumWindows(EnumWindowsCallbackDelegate callback, IntPtr lParam);
        [DllImport("User32.Dll")]
        private static extern void GetWindowText(int h, StringBuilder s, int nMaxCount);
        [DllImport("User32.Dll")]
        private static extern void GetClassName(int h, StringBuilder s, int nMaxCount);
        [DllImport("User32.Dll")]
        private static extern bool EnumChildWindows(IntPtr hwndParent, EnumWindowsCallbackDelegate lpEnumFunc, IntPtr lParam);
        [DllImport("User32.Dll")]
        private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
        [DllImport("user32.dll")]
        private static extern short FlashWindowEx(ref FLASHWINFO pwfi);
        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

        //Constants
        private const int BM_CLICK = 0x00F5;
        private const uint FLASHW_ALL = 3;
        private const uint FLASHW_CAPTION = 1;
        private const uint FLASHW_STOP = 0;
        private const uint FLASHW_TIMER = 4;
        private const uint FLASHW_TIMERNOFG = 12;
        private const uint FLASHW_TRAY = 2;
        private const int FIND_DLG_SLEEP = 200; //Milliseconds to sleep between checks for installation dialogs.
        private const int FIND_DLG_LOOP_CNT = 50; //Total loops to look for an install dialog. Defaulting 200ms sleap time, 50 = 10 seconds.

        //Delegates
        private delegate bool EnumWindowsCallbackDelegate(IntPtr hwnd, IntPtr lParam);

        //Methods
        private static IntPtr SearchForTopLevelWindow(string WindowTitle)
        {
            ArrayList windowHandles = new ArrayList();
            /* Create a GCHandle for the ArrayList */
            GCHandle gch = GCHandle.Alloc(windowHandles);
            try
            {
                EnumWindows(new EnumWindowsCallbackDelegate(EnumProc), (IntPtr)gch);
                /* the windowHandles array list contains all of the
                    window handles that were passed to EnumProc.  */
            }
            finally
            {
                /* Free the handle */
                gch.Free();
            }

            /* Iterate through the list and get the handle thats the best match */
            foreach (IntPtr handle in windowHandles)
            {
                StringBuilder sb = new StringBuilder(1024);
                GetWindowText((int)handle, sb, sb.Capacity);
                if (sb.Length > 0)
                {
                    if (sb.ToString().StartsWith(WindowTitle))
                    {
                        return handle;
                    }
                }
            }

            return IntPtr.Zero;
        }
        private static IntPtr SearchForChildWindow(IntPtr ParentHandle, string Caption)
        {
            ArrayList windowHandles = new ArrayList();
            /* Create a GCHandle for the ArrayList */
            GCHandle gch = GCHandle.Alloc(windowHandles);
            try
            {
                EnumChildWindows(ParentHandle, new EnumWindowsCallbackDelegate(EnumProc), (IntPtr)gch);
                /* the windowHandles array list contains all of the
                    window handles that were passed to EnumProc.  */
            }
            finally
            {
                /* Free the handle */
                gch.Free();
            }

            /* Iterate through the list and get the handle thats the best match */
            foreach (IntPtr handle in windowHandles)
            {
                StringBuilder sb = new StringBuilder(1024);
                GetWindowText((int)handle, sb, sb.Capacity);
                if (sb.Length > 0)
                {
                    if (sb.ToString().StartsWith(Caption))
                    {
                        return handle;
                    }
                }
            }

            return IntPtr.Zero;

        }
        private static bool EnumProc(IntPtr hWnd, IntPtr lParam)
        {
            /* get a reference to the ArrayList */
            GCHandle gch = (GCHandle)lParam;
            ArrayList list = (ArrayList)(gch.Target);
            /* and add this window handle */
            list.Add(hWnd);
            return true;
        }
        private static void DoButtonClick(IntPtr ButtonHandle)
        {
            SendMessage(ButtonHandle, BM_CLICK, IntPtr.Zero, IntPtr.Zero);
        }
        private static IntPtr FindDialog(string dialogName)
        {
            IntPtr hWnd = IntPtr.Zero;

            int cnt = 0;
            while (hWnd == IntPtr.Zero && cnt++ != FIND_DLG_LOOP_CNT)
            {
                hWnd = SearchForTopLevelWindow(dialogName);
                System.Threading.Thread.Sleep(FIND_DLG_SLEEP);
            }

            if (hWnd == IntPtr.Zero) 
                throw new Exception(string.Format("Installation Dialog \"{0}\" not found.", dialogName));
            return hWnd;
        }
        private static IntPtr FindDialogButton(IntPtr hWnd, string buttonText)
        {
            IntPtr button = IntPtr.Zero;
            int cnt = 0;
            while (button == IntPtr.Zero && cnt++ != FIND_DLG_LOOP_CNT)
            {
                button = SearchForChildWindow(hWnd, buttonText);
                System.Threading.Thread.Sleep(FIND_DLG_SLEEP);
            }
            return button;
        }
        private static bool FlashWindowAPI(IntPtr handleToWindow)
        {
            FLASHWINFO flashwinfo1 = new FLASHWINFO();
            flashwinfo1.cbSize = (uint)Marshal.SizeOf(flashwinfo1);
            flashwinfo1.hwnd = handleToWindow;
            flashwinfo1.dwFlags = 15;
            flashwinfo1.uCount = uint.MaxValue;
            flashwinfo1.dwTimeout = 0;
            return (FlashWindowEx(ref flashwinfo1) == 0);
        }

        //These are the only functions that should be called above.
        private static void PushUninstallOKButton(string DisplayName)
        {
            IntPtr diag = FindDialog(DisplayName + " Maintenance");
            IntPtr button = FindDialogButton(diag, "&OK");
            DoButtonClick(button);
        }
        #endregion
    }
}

Инструкции из ReadMe.txt:

A. Ссылка на этот API в текущих приложениях.

Следуйте этим инструкциям, чтобы подготовить приложение к будущему приложению переустановка из другой точки установки. Эти шаги добавляют необходимую библиотеку ссылки, чтобы ваше приложение могло автоматически переустанавливаться из нового местоположения.
Эти шаги могут выполняться в любой момент, даже если новая установка еще не установлена необходимо.

  • Откройте проект ClickOnceReinstaller и создайте проект в режиме Release.

  • Откройте приложение ClickOnce и ссылку на ClickOnceReinstaller.dll в ваш проект запуска.

    В качестве альтернативы вы можете добавить проект ClickOnceReinstaller в свое приложение и refrence проект.

  • Затем откройте файл кода, содержащий точку входа для вашего приложения. (Как правило, в С# это Program.cs)

    Внутри файла точки входа приложения вызовите Reinstaller.CheckForUpdates(). Существует несколько методов подписи для CheckForUpdates(). См. Описания Intellisense для определяя, какая подпись требует вашего приложения. Первоначально это не имеет значения, потому что необходимый файл поиска не должен публиковаться ваш сервер установки.

    (ДОПОЛНИТЕЛЬНО) Метод Reinstaller.CheckForUpdates возвращает объект InstallStatus который является перечисляемым значением состояния процесса установки. Захватите это и обрабатывать его соответствующим образом. Определения для каждого потенциального возвращаемого значения можно найти через Intellisense для каждого значения.

    Ответ NoUpdates означает, что в настоящее время нет новых обновлений, требующих переустановка вашего приложения.

  • Скомпилируйте приложение и опубликуйте новую версию приложения в сервер установки.

В. Обновление приложения из нового места установки

Эти шаги требуются после того, как приложение должно перейти на новый веб-адрес или необходимо внести изменения в приложение, требующее переустановки приложение.

Если вашему веб-серверу необходимо перейти в новое место, настоятельно рекомендуется, чтобы вы выполните следующие шаги и выполните новую точку установки, прежде чем принимать текущие Нажмите кнопку "Установить точку" ClickOnce в автономном режиме.

  • В текстовом редакторе создайте новый файл.
  • В первой строке файла добавьте полное местоположение для новой установки место нахождения. (т.е. сохранить новый файл в http://www.example.com/ClickOnceInstall_NewLocation/)
  • Сохраните файл как "переустановить" в корень ваших текущих приложений ClickOnce установить местоположение. (т.е. http://www.example.com/ClickOnceInstall/reinstall где http://www.example.com/ClickOnceInstall/ является корнем пути установки.)
  • Запустите приложение с тестовой машины. Приложение должно автоматически удалите текущую версию приложения и переустановите это из местоположения, указанного в файле переустановки.

С. Специальные заметки

  • Вам не нужно сохранять файл переустановки в корень оригинала папке установки приложения, однако вам нужно будет опубликовать версию ваше приложение к исходной точке установки, которая ссылается на веб-адрес который будет содержать файл переустановки, который будет указывать новую точку установки.

    Для этого требуется некоторое предварительное планирование, чтобы ссылка могла быть сделана из приложение к пути, который, как вы знаете, вы будете контролировать.

  • Переустановить файл можно сохранить в корневую папку начальной установки но должен быть оставлен пустым, если приложение еще не нужно переустанавливать.
    Пустой файл переустановки игнорируется.

  • Технически API ищет веб-ответ от вызова "переустановить". На сервере может быть реализован механизм, который возвращает текстовый ответ с указанием местоположения новой установки.

  • Файл переустановки анализируется, просматривая первую строку файла для расположение новой установки. Все остальные тексты игнорируются. Эта является преднамеренным, так что последующие обновления этого API могут потенциально реализовать более новые свойства в ответе переустановить.

  • API в этом текущем состоянии будет поддерживать только приложения ClickOnce, которые были установлены в соответствии с английским вариантом культуры. Причина этого ограничение связано с тем, что процесс автоматизирован, ища удаление диалога и передачи команды Click на кнопку с текстовым значением "OK".

Ответ 4

Для безумного или отчаянного отражения на спасение! Замените "X" на ваше приложение. Имя файла приложения (не путь) и токен открытого ключа.

Проверено только на Windows 10.

        var textualSubId = "XXXXXXXXXXXXXXXXXX.application, Culture=neutral, PublicKeyToken=XXXXXXXXXXXXXXXX, processorArchitecture=amd64";

        var deploymentServiceCom = new System.Deployment.Application.DeploymentServiceCom();
        var _r_m_GetSubscriptionState = typeof(System.Deployment.Application.DeploymentServiceCom).GetMethod("GetSubscriptionState", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

        var subState = _r_m_GetSubscriptionState.Invoke(deploymentServiceCom, new[] { textualSubId });
        var subscriptionStore = subState.GetType().GetProperty("SubscriptionStore").GetValue(subState);
        subscriptionStore.GetType().GetMethod("UninstallSubscription").Invoke(subscriptionStore, new[] { subState });

Надеюсь, это поможет кому-то.