Created
September 14, 2020 16:57
-
-
Save tkouba/99222f99c0d163660232d213dbae589d to your computer and use it in GitHub Desktop.
ClickOnce application migration tool
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* ClickOnceReinstaller v 1.1.0 | |
* - Author: Tomas Kouba (tomas.kouba@gmail.com)* | |
* - Changes: | |
* - add reinstall file extension .txt | |
* - using log4net | |
* - migration should be cancelled by user | |
* - TODO | |
* - add cancellation message to reinstall.txt file | |
* | |
* v 1.0.0 | |
* - Author: Richard Hartness (rhartness@gmail.com) | |
* - 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's Blog Post: | |
* - ClickOnce and Expiring Certificates | |
* http://robindotnet.wordpress.com/2009/03/30/clickonce-and-expiring-certificates/ | |
* | |
* Jim Harte's 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 | |
{ | |
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(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 CheckForReinstall(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 newAddr = String.Empty; | |
string originalLocation = ApplicationDeployment.CurrentDeployment.UpdateLocation.ToString(); | |
try | |
{ | |
string reinstallServerFile = originalLocation.Substring(0, originalLocation.LastIndexOf("/") + 1) + "reinstall.txt"; | |
log.Debug($"Reinstall server file: {reinstallServerFile}"); | |
HttpWebRequest rqHead = (HttpWebRequest)HttpWebRequest.Create(reinstallServerFile); | |
rqHead.Method = "HEAD"; | |
rqHead.Credentials = CredentialCache.DefaultCredentials; | |
HttpWebResponse rsHead = (HttpWebResponse)rqHead.GetResponse(); | |
log.Debug($"Response status: {rsHead.StatusCode}"); | |
log.Debug($"Response headers: {rsHead.Headers}"); | |
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 (Exception ex) | |
{ | |
log.Debug("Check new location failed", ex); | |
//If we receive an error at this point in checking, we can assume that there are no updates. | |
return InstallStatus.FailedUninstall; | |
} | |
//Begin Uninstallation Process | |
if (MessageBox.Show("Je naplánována migrace aplikace.\n" + | |
"Prosíme potvrďte instalaci až budete požádáni o potvrzení instalace aplikace.\n" + | |
"\nChcete začít migraci hned?", "Migrace aplikace", | |
MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1) != DialogResult.Yes) | |
{ | |
log.Debug("Migration aborted"); | |
return InstallStatus.FailedUninstall; | |
} | |
try | |
{ | |
// Find Uninstall string in registry | |
string DisplayName = null; | |
string uninstallString = GetUninstallString(originalLocation, 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); | |
log.Debug($"Run DLL App: {runDLL32}"); | |
log.Debug($"Run DLL Args: {args}"); | |
Process uninstallProcess = Process.Start(runDLL32, args); | |
PushUninstallOKButton(DisplayName); | |
} | |
catch (Exception ex) | |
{ | |
log.Debug("Uninstall failed", ex); | |
return InstallStatus.FailedUninstall; | |
} | |
//Start the re-installation process | |
try | |
{ | |
log.Debug($"New location: {newAddr}"); | |
//Start with IE-- other browser will certainly fail. | |
Process.Start("iexplore.exe", newAddr); | |
} | |
catch (Exception ex) | |
{ | |
log.Debug("Reinstall failed", ex); | |
return InstallStatus.FailedReinstall; | |
} | |
if (exitAppOnSuccess) Environment.Exit(0); | |
return InstallStatus.Success; | |
} | |
#endregion | |
#region Helper Methods | |
//Private Methods | |
private static string GetUninstallString(string updateUrl, out string DisplayName) | |
{ | |
string uninstallString = null; | |
#if DEBUG | |
Trace.WriteLine(updateUrl); | |
#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 urlUpdateInfo = (string)appKey.GetValue("UrlUpdateInfo"); | |
string shortcutAppId = (string)appKey.GetValue("ShortcutAppId"); | |
if (urlUpdateInfo.StartsWith(updateUrl) || shortcutAppId.StartsWith(updateUrl)) | |
{ | |
uninstallString = (string)appKey.GetValue("UninstallString"); ; | |
DisplayName = (string)appKey.GetValue("DisplayName"); | |
appKey.Close(); | |
break; | |
} | |
appKey.Close(); | |
} | |
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 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment