[PATCH 5/5] Add a self-update mechanism for setup.exe

Jon TURNEY jon.turney@dronecode.org.uk
Sun Apr 24 13:06:00 GMT 2011


Updating setup.exe has 3 stages:
1) Download updated setup.exe to a temporary location
2) Execute that temporary copy of setup.exe with --copy-to instructing
it to copy itself over the setup.exe to be updated
3) Execute the updated setup.exe with --remove-from instructing it
to delete the temporary copy

A named mutex is used to ensure setup exits from each stage before
the next stage can start.

Unfortunately, at the moment, we don't usefully check the setup version number
until after we have downloaded and parsed setup.ini, which is perhaps a bit
late to offer to update setup.exe

v2: Address comments from Dave Korn
Properly quote arguments to ensure spaces in paths are handled safely
Place the setup URL in a string resouce

2011-03-29  Jon TURNEY  <jon.turney@dronecode.org.uk>

	* res.rc (IDS_OLD_SETUP_VERSION): Change text to offer to download
	new version of setup.
	(IDS_SETUP_URL): Added string resource.
	* resource.h (IDS_SETUP_URL): Added resource identifier.
	* main.cc (wait_for_exit, self_update_remove_from,
	self_update_copy_to, WinMain): Add -copy-to and --remove-from options
	for self-update process.
	* ini.cc (self_update_download): New function to download updated
	setup.
	(do_ini_thread): Prompt to download updated setup if a newer
	version exists.  Return a result indicating what should happen
	next.
	* threebar.h (PropertyPage::OnFinish): Add an implementation of
	OnFinish virtual function for this class.
	* threebar.cc (OnFinish): Ditto.
	 (OnMessageApp): Setup should finish on WM_APP_SETUP_INI_DOWNLOAD_COMPLETE
	if an updated setup was downloaded.

2011-03-29  Jon TURNEY  <jon.turney@dronecode.org.uk>

	* include/getopt++/DefaultFormatter.h (DefaultFormatter): Fix option string
	formatting when it has no short option.

Signed-off-by: Jon TURNEY <jon.turney@dronecode.org.uk>
---
 ini.cc                                          |   78 ++++++++++++++--
 libgetopt++/include/getopt++/DefaultFormatter.h |    6 +-
 main.cc                                         |  115 ++++++++++++++++++++++-
 res.rc                                          |    3 +-
 resource.h                                      |    1 +
 threebar.cc                                     |   14 +++-
 threebar.h                                      |    1 +
 7 files changed, 204 insertions(+), 14 deletions(-)

diff --git a/ini.cc b/ini.cc
index d99bc60..8b33e68 100644
--- a/ini.cc
+++ b/ini.cc
@@ -285,7 +285,68 @@ do_remote_ini (HWND owner)
   return ini_count;
 }
 
-static bool
+static int
+self_update_download(HWND owner)
+{
+  /* Get the download URL string resource */
+  char setup_url[1000];
+  if (LoadString (GetModuleHandle(NULL), IDS_SETUP_URL, setup_url, sizeof(setup_url)) <= 0)
+    {
+      MessageBox (owner, "Unable to determine download URL.", "Cygwin Setup", MB_OK);
+      return IDD_CHOOSE;
+    }
+
+  /* Download new setup to a temporary location */
+  std::string downloadedSetup = local_dir + "\\setup.tmp.exe";
+  if (get_url_to_file (setup_url, downloadedSetup, 0, owner))
+    {
+      MessageBox (owner, "Downloading updated setup.exe failed.", "Cygwin Setup", MB_OK);
+      return IDD_CHOOSE;
+    }
+
+  /* Create and claim a mutex so the new version invoked with --copy-to doesn't
+     try to overwrite us before we've exited */
+  HANDLE mutex = CreateMutex(NULL, TRUE, "Global\\Cygwin.Setup");
+  if (!mutex)
+    {
+      log (LOG_PLAIN) << "CreateMutex failed :" << GetLastError () << endLog;
+    }
+
+  /* Invoke setup with same options + --copy-to current location */
+  std::string newArgs = "\"" + downloadedSetup + "\"";
+
+  for (int i = 1; _argv[i]; i++)
+    {
+      newArgs += " \"";
+      newArgs += _argv[i];
+      newArgs += "\"";
+    }
+
+  TCHAR filename[MAX_PATH+1];
+  GetModuleFileName(NULL, filename, MAX_PATH);
+  newArgs += " \"--copy-to=";
+  newArgs += filename;
+  newArgs += "\"";
+
+#ifdef DEBUG
+  log (LOG_BABBLE) << "invoking '" << newArgs << "'" << endLog;
+#endif
+
+  PROCESS_INFORMATION processInfo;
+  STARTUPINFO startupInfo;
+  GetStartupInfo(&startupInfo);
+  if (!CreateProcess(downloadedSetup.c_str(), (char *)(newArgs.c_str()),
+                    NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo) > 0)
+    {
+      MessageBox (owner, "Executing updated setup.exe failed.", "Cygwin Setup", MB_OK);
+      log (LOG_PLAIN) << "CreateProcess failed :" << GetLastError () << endLog;
+      return IDD_CHOOSE;
+    }
+
+  return -1;
+}
+
+static int
 do_ini_thread (HINSTANCE h, HWND owner)
 {
   size_t ini_count = 0;
@@ -295,7 +356,7 @@ do_ini_thread (HINSTANCE h, HWND owner)
     ini_count = do_remote_ini (owner);
 
   if (ini_count == 0)
-    return false;
+    return 0;
 
   if (get_root_dir ().c_str())
     {
@@ -339,11 +400,14 @@ do_ini_thread (HINSTANCE h, HWND owner)
   if (ini_setup_version.size())
     {
       if (version_compare(setup_version, ini_setup_version) < 0)
-	note (owner, IDS_OLD_SETUP_VERSION, setup_version,
-	      ini_setup_version.c_str());
+        if (yesno (owner, IDS_OLD_SETUP_VERSION, setup_version,
+                   ini_setup_version.c_str()) == IDYES)
+          {
+            return self_update_download(owner);
+          }
     }
 
-  return true;
+  return IDD_CHOOSE;
 }
 
 static DWORD WINAPI
@@ -354,10 +418,10 @@ do_ini_thread_reflector(void* p)
 
   try
   {
-    bool succeeded = do_ini_thread((HINSTANCE)context[0], (HWND)context[1]);
+    int nextpage = do_ini_thread((HINSTANCE)context[0], (HWND)context[1]);
 
     // Tell the progress page that we're done downloading
-    Progress.PostMessageNow(WM_APP_SETUP_INI_DOWNLOAD_COMPLETE, 0, succeeded);
+    Progress.PostMessageNow(WM_APP_SETUP_INI_DOWNLOAD_COMPLETE, 0, nextpage);
   }
   TOPLEVEL_CATCH("ini");
 
diff --git a/libgetopt++/include/getopt++/DefaultFormatter.h b/libgetopt++/include/getopt++/DefaultFormatter.h
index ced6956..cbf700b 100644
--- a/libgetopt++/include/getopt++/DefaultFormatter.h
+++ b/libgetopt++/include/getopt++/DefaultFormatter.h
@@ -27,7 +27,11 @@ class DefaultFormatter {
   public:
     DefaultFormatter (std::ostream &aStream) : theStream(aStream) {}
     void operator () (Option *anOption) {
-      std::string output = std::string() + " -" + anOption->shortOption ()[0];
+      std::string output;
+      if (anOption->shortOption()[0] != '\0')
+          output +=  std::string() + " -" + anOption->shortOption ()[0];
+      else
+        output += "   ";
       output += " --" ;
       output += anOption->longOption ();
       output += std::string (40 - output.size(), ' ');
diff --git a/main.cc b/main.cc
index bddff6f..47619ba 100644
--- a/main.cc
+++ b/main.cc
@@ -68,6 +68,7 @@ static const char *cvsid =
 
 #include "getopt++/GetOption.h"
 #include "getopt++/BoolOption.h"
+#include "getopt++/StringOption.h"
 
 #include "Exception.h"
 #include <stdexcept>
@@ -88,6 +89,8 @@ bool is_legacy;
 static BoolOption UnattendedOption (false, 'q', "quiet-mode", "Unattended setup mode");
 static BoolOption PackageManagerOption (false, 'M', "package-manager", "Semi-attended chooser-only mode");
 static BoolOption HelpOption (false, 'h', "help", "print help");
+static StringOption SelfUpdateCopyTo ("", '\0', "copy-to", "Used by self-update.  Copy self to location and invoke.", true);
+static StringOption SelfUpdateRemoveFrom ("", '\0', "remove-from", "Used by self-update.  Remove file after starting.", true);
 static BOOL WINAPI (*dyn_AttachConsole) (DWORD);
 static BOOL WINAPI (*dyn_GetLongPathName) (LPCTSTR, LPTSTR, DWORD);
 
@@ -239,6 +242,91 @@ set_legacy (const char *command)
   is_legacy = strstr (buf, "setup-legacy");
 }
 
+static HANDLE
+wait_for_exit()
+{
+  /* Wait on mutex held by previous instance until it exits, so
+     we don't try to remove it before it's exited */
+  HANDLE mutex = CreateMutex(NULL, FALSE, "Global\\Cygwin.Setup");
+  if (!mutex)
+    {
+      log (LOG_PLAIN) << "CreateMutex failed :" << GetLastError () << endLog;
+    }
+  else
+    {
+      log (LOG_PLAIN) << "Waiting on Mutex" << endLog;
+      WaitForSingleObject(mutex, INFINITE);
+    }
+
+  return mutex;
+}
+
+static void
+self_update_remove_from(const std::string &SelfUpdateRemoveFromString)
+{
+  HANDLE mutex = wait_for_exit();
+
+  log (LOG_PLAIN) << "Removing temporary copy of self '" << SelfUpdateRemoveFromString << "'" << endLog;
+  if (DeleteFile(SelfUpdateRemoveFromString.c_str()) == 0)
+    {
+      log (LOG_PLAIN) << "DeleteFile failed :" << GetLastError () << endLog;
+    }
+
+  ReleaseMutex(mutex);
+}
+
+static void
+self_update_copy_to(const std::string &SelfUpdateCopyToString, int argc)
+{
+  wait_for_exit();
+  /*
+     We hold on to this mutex until we exit, which implicitly releases it, as it's
+     the next instance of ourself should not try to remove us until we have exited
+  */
+
+  log (LOG_PLAIN) << "Copying self to '" << SelfUpdateCopyToString << "'" << endLog;
+
+  /* Copy self to new location */
+  TCHAR filename[MAX_PATH+1];
+  GetModuleFileName(NULL, filename, MAX_PATH);
+  if (CopyFile(filename, SelfUpdateCopyToString.c_str(), FALSE) == 0)
+    {
+      log (LOG_PLAIN) << "CopyFile failed :" << GetLastError () << endLog;
+    }
+
+  /*
+     Now invoke ourself in new location with all the same arguments, except
+     --remove-from this temporary copy, rather than --copy-to the new location
+  */
+  std::string newArgs = "\"" + SelfUpdateCopyToString + "\"";
+
+  for (int i = 1; i < argc; i++)
+    {
+      if (strncmp(_argv[i],"--copy-to",strlen("--copy-to")) != 0)
+        {
+          newArgs += " \"";
+          newArgs += _argv[i];
+          newArgs += "\"";
+        }
+      else
+        {
+          newArgs += " \"--remove-from=";
+          newArgs += filename;
+          newArgs += "\"";
+        }
+    }
+
+#ifdef DEBUG
+  log (LOG_BABBLE) << "invoking '" << newArgs << "'" << endLog;
+#endif
+
+  PROCESS_INFORMATION processInfo;
+  STARTUPINFO startupInfo;
+  GetStartupInfo(&startupInfo);
+  CreateProcess(SelfUpdateCopyToString.c_str(), (char *)newArgs.c_str(),
+                NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
+}
+
 #ifndef __CYGWIN__
 int WINAPI
 WinMain (HINSTANCE h,
@@ -318,11 +406,32 @@ main (int argc, char **argv)
     log (LOG_PLAIN) << "Starting cygwin install, version " 
                     << setup_version << endLog;
 
-    UserSettings Settings (local_dir);
+#ifdef DEBUG
+    for (int i = 0; i < argc; i++)
+      {
+        log (LOG_BABBLE) << "argv[" << i << "] = " << _argv[i] << endLog;
+      }
+#endif
+
+    std::string SelfUpdateRemoveFromString = SelfUpdateRemoveFrom;
+    if (SelfUpdateRemoveFromString.size())
+      {
+        self_update_remove_from(SelfUpdateRemoveFromString);
+      }
+
+    std::string SelfUpdateCopyToString = SelfUpdateCopyTo;
+    if (SelfUpdateCopyToString.size())
+      {
+        self_update_copy_to(SelfUpdateCopyToString, argc);
+      }
+    else
+      {
+        UserSettings Settings (local_dir);
 
-    main_display ();
+        main_display ();
 
-    Settings.save ();	// Clean exit.. save user options.
+        Settings.save ();	// Clean exit.. save user options.
+      }
 
     if (rebootneeded)
       {
diff --git a/res.rc b/res.rc
index 92454eb..83e8bad 100644
--- a/res.rc
+++ b/res.rc
@@ -501,7 +501,7 @@ BEGIN
     IDS_UNINSTALL_COMPLETE  "Uninstalls complete."
     IDS_WININET             "Unable to find or load the Internet Explorer 5 DLLs"
     IDS_ERR_CHDIR           "Could not change dir to %s: %s [%.8x]"
-    IDS_OLD_SETUP_VERSION   "This setup is version %s, but setup.ini claims version %s is available.\nYou might want to upgrade to get the latest features and bug fixes."
+    IDS_OLD_SETUP_VERSION   "This setup is version %s, but setup.ini claims version %s is available.\nDo you want to upgrade to get the latest features and bug fixes?"
     IDS_DOWNLOAD_INCOMPLETE "Download Incomplete.  Try again?"
     IDS_INSTALL_ERROR	    "Installation error (%s), Continue with other packages?"
     IDS_INSTALL_INCOMPLETE  "Installation incomplete.  Check %s for details"
@@ -552,4 +552,5 @@ BEGIN
     IDS_CANT_MKDIR     "Couldn't create directory %s, sorry.  (Is drive full or read-only?)"
     IDS_NO_CWD	       "Local package directory %s not found.\nYou can still use setup.exe to remove installed\npackages, but there "
       "will be nothing to install.\n\nPress OK if that's what you wanted\nor Cancel to choose a different directory."
+    IDS_SETUP_URL      "http://cygwin.com/setup.exe"
 END
diff --git a/resource.h b/resource.h
index 8845497..e79d840 100644
--- a/resource.h
+++ b/resource.h
@@ -39,6 +39,7 @@
 #define IDS_MAYBE_MKDIR                   136
 #define IDS_CANT_MKDIR                    137
 #define IDS_NO_CWD			  138
+#define IDS_SETUP_URL                     139
 
 // Dialogs
 
diff --git a/threebar.cc b/threebar.cc
index b88dd38..cc4e083 100644
--- a/threebar.cc
+++ b/threebar.cc
@@ -240,9 +240,9 @@ ThreeBarProgressPage::OnMessageApp (UINT uMsg, WPARAM wParam, LPARAM lParam)
       }
     case WM_APP_SETUP_INI_DOWNLOAD_COMPLETE:
       {
-	if (lParam)
+	if (lParam > 0)
 	  GetOwner ()->SetActivePageByID (IDD_CHOOSE);
-	else
+	else if (lParam == 0) /* download failed */
 	  {
 	    if (source == IDC_SOURCE_CWD)
 	      {
@@ -276,6 +276,10 @@ ThreeBarProgressPage::OnMessageApp (UINT uMsg, WPARAM wParam, LPARAM lParam)
 		GetOwner ()->SetActivePageByID (IDD_SITE);
 	      }
 	  }
+        else /* a setup.exe update was downloaded */
+          {
+            PropSheet_PressButton(GetOwner()->GetHWND(), PSBTN_FINISH);
+          }
 	break;
       }
     default:
@@ -288,3 +292,9 @@ ThreeBarProgressPage::OnMessageApp (UINT uMsg, WPARAM wParam, LPARAM lParam)
   return true;
 
 }
+
+bool
+ThreeBarProgressPage::OnFinish ()
+{
+  return true;
+}
diff --git a/threebar.h b/threebar.h
index 225703a..328ccbf 100644
--- a/threebar.h
+++ b/threebar.h
@@ -63,6 +63,7 @@ public:
 
   virtual void OnInit ();
   virtual void OnActivate ();
+  virtual bool OnFinish ();
   virtual bool OnMessageApp (UINT uMsg, WPARAM wParam, LPARAM lParam);
   virtual long OnUnattended ()
   {
-- 
1.7.4



More information about the Cygwin-apps mailing list