diff --git a/bazaar/Updater/Updater.cpp b/bazaar/Updater/Updater.cpp new file mode 100644 index 000000000..b1495ea9d --- /dev/null +++ b/bazaar/Updater/Updater.cpp @@ -0,0 +1,619 @@ +#include "Updater.h" + +#include + +#include + +// gets platform root folder +String Updater::GetPlatformRoot(void) +{ +#ifdef WIN64 + return webRoot + "/WINDOWS64/"; +#elif WIN32 + return webRoot + "/WINDOWS32/"; +#elif PLATFORM_POSIX +#ifdef __x86_64 + return webRoot + "/LINUX64/"; +#else + return webRoot + "/LINUX32/"; +#endif +#else +#error "PLATFORM NOT SUPPORTED" +#endif +} + +// constructor +Updater::Updater() +{ + // fetches and stores environment, we need to change it later + environ <<= Environment(); + + // gets state variable, if not present assume starting in normal mode + String stateStr; + if(environ.Find("UPDATER_STATE") < 0) + state = NormalRun; + else + { + stateStr = environ.Get("UPDATER_STATE"); + if(stateStr == "INSIDEUPDATER") + state = InsideUpdater; + else if(stateStr == "UNINSTALLFAILED") + state = UninstallFailed; + else if(stateStr == "INSTALLFAILED") + state = InstallFailed; + else if(stateStr == "UPDATEFAILED") + state = UpdateFailed; + else if(stateStr == "UNINSTALLSUCCEEDED") + state = UninstallSucceeded; + else if(stateStr == "INSTALLSUCCEEDED") + state = InstallSucceeded; + else if(stateStr == "UPDATESUCCEEDED") + state = UpdateSucceeded; + else + NEVER(); + } + + // gets user name + if(state != InsideUpdater) + user = GetUserName(); + else if(environ.Find("UPDATER_USER") < 0) + user = "unknown"; + else + user = environ.Get("UPDATER_USER"); + + // get application name -- either from GetExeTitle() if in normal mode + // or from environment if in superuser mode + if(state != InsideUpdater) + appName = GetExeTitle(); + else if(environ.Find("UPDATER_APPNAME") >= 0) + appName = environ.Get("UPDATER_APPNAME"); + else + appName = GetExeTitle(); // fallout -- should NEVER get here + + // gets app's user config path +#ifdef PLATFORM_POSIX + userConfigPath = "/home/" + user + "./" + appName + "/"; +#else + userConfigPath = GetHomeDirectory() + "./" + appName + "/"; +#endif + + // gets app's system config path +#ifdef PLATFORM_POSIX + systemConfigPath = "/usr/share/" + appName + "/"; +#else + systemConfigPath = GetProgramsFolder() + "/" + appName + "/"; +#endif + + // checks wether app is already installed and gather its version + installedVersion = -1; +#ifdef PLATFORM_POSIX + appInstalled = FileExists(GetProgramsFolder() + "/" + appName); +#else + appInstalled = FileExists(GetProgramsFolder() + "/" + appName + "/" + appName + ".exe"); +#endif + appInstalled &= FileExists(systemConfigPath + "version"); + String verStr; + if(appInstalled) + { + verStr = LoadFile(systemConfigPath + "version"); + if(verStr != "") + installedVersion = ScanDouble(verStr); + } + + // create user config path only on normal run + if(state == NormalRun) + RealizePath(userConfigPath); + + // creates system config path on superuser mode + if(state == InsideUpdater) + RealizePath(systemConfigPath); +} + +/////////////////////////////////////////////////////////////////////////////////////// +// installer/updater subfunctions + +// setup environment for failing update/install +void Updater::FailUpdate() +{ + switch(updaterOp) + { + case Install: + environ.Add("UPDATER_STATE", "INSTALLFAILED"); + break; + + case Uninstall: + environ.Add("UPDATER_STATE", "UNINSTALLFAILED"); + break; + + case Update: + environ.Add("UPDATER_STATE", "UPDATEFAILED"); + break; + + default: + NEVER(); + } +} + +// setup environment for succeeding update/install +void Updater::SucceedUpdate() +{ + switch(updaterOp) + { + case Install: + environ.Add("UPDATER_STATE", "INSTALLSUCCEEDED"); + break; + + case Uninstall: + environ.Add("UPDATER_STATE", "UNINSTALLSUCCEEDED"); + break; + + case Update: + environ.Add("UPDATER_STATE", "UPDATESUCCEEDED"); + break; + + default: + NEVER(); + } +} + +// main updater proc inside normal mode +// prepares environment, copy app in temporary folder +// and restarts it in superuser mode +bool Updater::START_Updater(String const &operation) +{ + // prepare environment for updating + environ.Add("UPDATER_USER", user); + environ.Add("UPDATER_OP", operation); + environ.Add("UPDATER_STATE", "INSIDEUPDATER"); + environ.Add("UPDATER_APPNAME", appName); + environ.Add("UPDATER_EXEPATH", GetExeFilePath()); + + if(CommandLine().GetCount() && CommandLine()[0] != "--UNINSTALL") + { + String s; + for(int i = 0; i < CommandLine().GetCount(); i++) + s += CommandLine()[i] + " "; + s = s.Left(s.GetCount() - 1); + environ.Add("UPDATER_CMDLINE", s); + } + + // gets current executable path + String exePath = GetExeFilePath(); + + // creates a temporary filename and copies itself + // in temporary folder + String tempName = GetTempFileName(); + if(!FileCopy(exePath, tempName)) + return false; + +#ifdef PLATFORM_POSIX + // for posix, change temp file permission + // allowing its execution and executes it as a superuser + if(chmod(~tempName, 0755) != 0) + return false; + + // note the -k to gksu -- it makes it preserve the environment + String params = "-k -u root \"" + tempName + "\""; + if(!SysStart("gksu", params, environ)) + return false; +#else + // for windows, simply execute the file + if(!SysStart(tempName, "", environ)) + return false; +#endif + return true; +} + +// uninstall app +void Updater::START_Uninstall(void) +{ + if(!START_Updater("UNINSTALL")) + Exclamation(t_("Uninstall failed&Press OK to quit")); +} + +// install app +bool Updater::START_Install(void) +{ + if(!START_Updater("INSTALL")) + return !PromptYesNo(t_("Install failed&Continue running the application?")); + return true; +} + +// update app +bool Updater::START_Update(void) +{ + if(!START_Updater("UPDATE")) + return !PromptYesNo(t_("Update failed&Continue running the application?")); + return true; +} + +// app started, must check for update/install requests +bool Updater::DO_NormalRun(void) +{ + // we now check if we just want to uninstall app + // it can be done by command line --UNINSTALL option + if(CommandLine().GetCount() && CommandLine()[0] == "--UNINSTALL") + { + START_Uninstall(); + return false; + } + + // if app not installed, we shall install it + if(!appInstalled) + return !START_Install(); + + // not installing nor uninstalling + + // we shall check which kind of update is enabled + String updateMode; + if(!FileExists(userConfigPath + "UPDATER_MODE")) + { + updateMode = "ASK"; + SaveFile(userConfigPath + "UPDATER_MODE", updateMode); + } + else + updateMode = LoadFile(userConfigPath + "UPDATER_MODE"); + + // if updates are disabled, just do nothing + if(updateMode == "DISABLED") + return true; + + // if we don't have a new version available, just do nothing + double maxVer; + if( (maxVer = FetchMaxValidVersion()) <= installedVersion) + return true; + + // if we want manual updates, just ask + if(updateMode == "ASK") + if(!PromptYesNo(t_("New version ") + FormatDoubleFix(maxVer, 2, FD_ZERO) + t_(" is available&Install it ?"))) + return true; + + // updater enabled, start it + // if update failed, resume normal run, otherwise exit + // as the app will be launched again by updater itself + return !START_Update(); +} + +bool Updater::DO_InsideUpdater(void) +{ + String operation; + + // gets variables from environment + // fails update if no operation found (shoudn't happen...) + bool failed = false; + + if(environ.Find("UPDATER_USER") < 0) + failed = true; + else + environ.RemoveKey("UPDATER_USER"); + + if(environ.Find("UPDATER_OP") < 0) + failed = true; + else + { + operation = environ.Get("UPDATER_OP"); + environ.RemoveKey("UPDATER_OP"); + + // setup update operation code + if(operation == "INSTALL") + updaterOp = Install; + else if(operation == "UNINSTALL") + updaterOp = Uninstall; + else if(operation == "UPDATE") + updaterOp = Update; + else + NEVER(); + + } + + if(environ.Find("UPDATER_APPNAME") < 0) + failed = true; + else + { + appName = environ.Get("UPDATER_APPNAME"); + environ.RemoveKey("UPDATER_APPNAME"); + } + environ.RemoveKey("UPDATER_STATE"); + + if(environ.Find("UPDATER_CMDLINE") >= 0) + { + commandLine = environ.Get("UPDATER_CMDLINE"); + environ.RemoveKey("UPDATER_CMDLINE"); + } + + if(environ.Find("UPDATER_EXEPATH") < 0) + failed = true; + else + { + exePath = environ.Get("UPDATER_EXEPATH"); + environ.RemoveKey("UPDATER_EXEPATH"); + } + + // if failed getting environment data, signals and restarts in normal mode + if(failed) + { + FailUpdate(); + RestartApp(RestartOrig); + return false; + } + + // execute the requested operation + switch(updaterOp) + { + case Install: + DO_Install(); + break; + + case Uninstall: + DO_Uninstall(); + break; + + case Update: + DO_Update(); + break; + + default: + NEVER(); + } + + // returns false anyways, it shouldn't continue in superuser mode ! + return false; +} + +void Updater::DO_Uninstall(void) +{ + // removes executable + String path; +#ifdef PLATFORM_POSIX + path = GetProgramsFolder() + "/" + appName; +#else + path = GetProgramsFolder() + "/" + appName + "/" + appName + ".exe"; +#endif + FileDelete(path); + + // removes system config folder + // leaves user one alone, it can contain user data + // (on win32, that's also the program folder) + DeleteFolderDeep(systemConfigPath); + + // signal op success and leave + // as it was uninstalling, it will restart + // launching app (uninstaller, possibily) + SucceedUpdate(); + RestartApp(RestartTemp); +} + +void Updater::DO_Install(void) +{ + // fetch the new app version and install it + if(!FetchApp()) + { + FailUpdate(); + RestartApp(RestartOrig); + } + else + { + SucceedUpdate(); + RestartApp(RestartNew); + } +} + +void Updater::DO_Update(void) +{ + // fetch the new app version and replace old one + if(!FetchApp()) + { + FailUpdate(); + RestartApp(RestartOrig); + } + else + { + SucceedUpdate(); + RestartApp(RestartNew); + } +} + +bool Updater::DO_UninstallFailed(void) +{ + Exclamation("Uninstall failed!&Aborting...."); + return false; +} + +bool Updater::DO_InstallFailed(void) +{ + return PromptYesNo("Install failed&Do you want to run the app anyways?"); +} + +bool Updater::DO_UpdateFailed(void) +{ + return PromptYesNo("Update failed&Do you want to run the app anyways?"); +} + +bool Updater::DO_UninstallSucceeded(void) +{ + PromptOK("Uninstall succeeded!"); + return false; +} + +bool Updater::DO_InstallSucceeded(void) +{ + PromptOK("Install succeeded!"); + return true; +} + +bool Updater::DO_UpdateSucceeded(void) +{ + PromptOK("Update succeeded!"); + return true; +} + +// restarts app in normal mode after updater mode +// either it re-launches the original one if update didn't succeed +// or the new one if it did OR the temporary app if uninstalling +void Updater::RestartApp(RestartModes restartMode) +{ + String path; + switch(restartMode) + { + case RestartTemp: + path = GetExeFilePath(); + break; + case RestartOrig: + default: + path = exePath; + break; + case RestartNew: +#ifdef PLATFORM_POSIX + path = GetProgramsFolder() + "/" + appName; +#else + path = GetProgramsFolder() + "/" + appName + "/" + appName + ".exe"; +#endif + break; + } + SysStart(path, commandLine, environ); +} + +// fetch list of available app versions +VectorUpdater::FetchVersions(void) +{ + Vector res; + + HttpClient http; + http.TimeoutMsecs(1000); + http.URL(GetPlatformRoot() + "versions"); + + // fetch version file from server + String verStr = http.Execute(); + int err = http.GetStatusCode(); + if(verStr == "" || err != 200) + return res; + StringStream ss(verStr); + while(!ss.IsEof()) + { + String s = ss.GetLine(); + if(s != "") + res.Add(ScanDouble(s)); + } + return res; +} + +// fetch MAX valid version, i.e. the greatest among all available +// but smaller or equal than maxVersion +double Updater::FetchMaxValidVersion(void) +{ + Vectorversions = FetchVersions(); + if(!versions.GetCount()) + return -1; + double maxV = -1; + for(int i = 0; i < versions.GetCount(); i++) + if(versions[i] <= maxVersion && versions[i] > maxV) + maxV = versions[i]; + return maxV; +} + +// fetch the new app version from web server +// and replaces older one +// if ver is not specified, fetches the maximum available +// one but which is less than or equal maxVersion +bool Updater::FetchApp(double ver) +{ + int err = 0; + + if(ver < 0) + ver = FetchMaxValidVersion(); + if(ver < 0) + return false; + + HttpClient http; + http.URL(GetPlatformRoot() + appName + "_" + FormatDoubleFix(ver, 2, FD_ZERO)); + http.TimeoutMsecs(1000*60*30); + http.MaxContentSize(100000000); + + // fetch version file from server + Progress progress(t_("Downloading application.... please wait")); + appBuffer = http.Execute(progress); + err = http.GetStatusCode(); + if(err != 200 || http.IsAborted() || http.IsError()) + { + appBuffer = ""; + return false; + } + + // replaces/installs app + String destPath; +#ifdef PLATFORM_POSIX + destPath = GetProgramsFolder() + "/" + appName; +#else + destPath = GetProgramsFolder() + "/" + appName + "/" + appName + ".exe"; +#endif + if(!SaveFile(destPath, appBuffer)) + return false; +#ifdef PLATFORM_POSIX + if(chmod(~destPath, 0755) != 0) + return false; +#endif + + // stores current version inside system config path + String verStr = FormatDoubleFix(ver, 2, FD_ZERO); + if(!SaveFile(systemConfigPath + "version", verStr)) + return false; + installedVersion = ver; + + return true; +} + + +/////////////////////////////////////////////////////////////////////////////////////// +// main updater call +// returns TRUE if app should continue, FALSE if should terminate +// BEWARE, app MUST check for return value AND behave as needed +// te SELF_UPDATE() macro does all what is needed +bool Updater::Run() +{ + switch(state) + { + case NormalRun : + return DO_NormalRun(); + case InsideUpdater : + return DO_InsideUpdater(); + case UninstallFailed : + return DO_UninstallFailed(); + case InstallFailed : + return DO_InstallFailed(); + case UpdateFailed : + return DO_UpdateFailed(); + case UninstallSucceeded : + return DO_UninstallSucceeded(); + case InstallSucceeded : + return DO_InstallSucceeded(); + case UpdateSucceeded : + return DO_UpdateSucceeded(); + default: + NEVER(); + break; + } + return false; +} + +// sets updater to manual mode -- if update is available, asks user +Updater &Updater::UpdateManual(void) +{ + SaveFile(userConfigPath + "UPDATER_MODE", "ASK"); + return *this; +} + +// sets updater to auto -- updates app on launch without asking user +Updater &Updater::UpdateAuto(void) +{ + SaveFile(userConfigPath + "UPDATER_MODE", "AUTO"); + return *this; +} + +// disable updater -- app will not update on launch +// re-enable it to restart updating system on next run +Updater &Updater::UpdateDisable(void) +{ + SaveFile(userConfigPath + "UPDATER_MODE", "DISABLE"); + return *this; +} + diff --git a/bazaar/Updater/Updater.h b/bazaar/Updater/Updater.h new file mode 100644 index 000000000..489d8d25d --- /dev/null +++ b/bazaar/Updater/Updater.h @@ -0,0 +1,171 @@ +#ifndef _TimberStarter_Updater_h_ +#define _TimberStarter_Updater_h_ + +#include + +using namespace Upp; + +#define SELFUPDATE_OK 0 +#define SELFUPDATE_NO_NETWORK 1 +#define SELFUPDATE_NO_VERSIONFILE 2 +#define SELFUPDATE_NO_APPFILE 3 +#define SELFUPDATE_NO_PERMISSION 4 + +extern int SELFUPDATE_RESULT; + +#define SELFUPDATE(remoteRoot, appName, currentVersion, maxVersion) \ + SELFUPDATE_RESULT = __SelfUpdate(remoteRoot, appName, currentVersion, maxVersion); \ + if(SELFUPDATE_RESULT == -1) \ + return; + +// UPDATE HELPER USED BY SELFUPDATE MACRO + +// IF APP STARTED NORMALLY : +// stores current command line on a temporary file +// restarts current applcation copying to temporary +// folder and acting as a superuser +// parameters passed to application : +// --UPDATE update command +// exepath complete path of itself +// cmdlinepath path of the command line file +// returns 0 if failed, 1 if success +// IF APP STARTED BY UPDATER (as a superuser): +// fetches the update file in a temporary folder +// overwrite the old executable with the new one +// restarts as a normal user the executable with original command line +// returns 2 if failed, 3 if success +// if failed, adds --UPDATEFAILED as last cmdline parameter +// if success, adds --UPDATEOK as last cmdline parameter +// +// IF APP STARTED WITH --UPDATEFAILED AS LAST PARAMETER +// returns 4 +// +// IF APP STARTED WITH --INSTALLFAILED AS LAST PARAMETER +// returns 5 +// +// IF APP STARTED WITH --UPDATEOK AS LAST PARAMETER +// just return 1 and skip further processing +int __SelfUpdate(String const &remoteRoot, String const &appName, String const ¤tVersion, String const &maxVersion); + + +class Updater +{ + private: + // state of updater engine + typedef enum + { + NormalRun, InsideUpdater, + UninstallFailed, InstallFailed, UpdateFailed, + UninstallSucceeded, InstallSucceeded, UpdateSucceeded + } UpdaterState; + UpdaterState state; + + // requested operation + typedef enum { Install, Update, Uninstall } UpdaterOp; + UpdaterOp updaterOp; + + // setup environment for failing update/install + void FailUpdate(void); + + // setup environment for succeeding update/install + void SucceedUpdate(void); + + // main updater proc inside normal mode + // prepares environment, copy app in temporary folder + // and restarts it in superuser mode + bool START_Updater(String const &operation); + + // operations procedures + void START_Uninstall(void); + void DO_Uninstall(void); + bool START_Install(void); + void DO_Install(void); + bool START_Update(void); + void DO_Update(void); + + // installer/updater subfunctions + bool DO_NormalRun(void); + bool DO_InsideUpdater(void); + bool DO_UninstallFailed(void); + bool DO_InstallFailed(void); + bool DO_UpdateFailed(void); + bool DO_UninstallSucceeded(void); + bool DO_InstallSucceeded(void); + bool DO_UpdateSucceeded(void); + + // gets platform root folder + String GetPlatformRoot(void); + + // restarts app in normal mode after updater mode + typedef enum { RestartOrig, RestartNew, RestartTemp } RestartModes; + void RestartApp(RestartModes restartMode); + + // fetch list of available app versions + VectorFetchVersions(void); + double FetchMaxValidVersion(void); + + // fetch the new app version from web server + // and replaces older one + String appBuffer; + bool FetchApp(double ver = -1); + + // running copy of environment + VectorMapenviron; + + // user name (which launched app in normal mode + String user; + + // user configuration path + String userConfigPath; + + // sistem configuration path (to keep installed version number) + String systemConfigPath; + + // flag stating if app is installed + bool appInstalled; + + // current installed version, if any + double installedVersion; + + // copy of starting command line -- needed to restart app correctly + String commandLine; + + // running application name + String appName; + + // copy of running executable path + // when app was first launched in normal mode + String exePath; + + // user variables + double maxVersion; + String webRoot; + + protected: + + public: + + Updater(); + + Updater &SetMaxVersion(double mv) { maxVersion = mv; return *this; } + Updater &SetWebRoot(String const &wr) { webRoot = wr; return *this; } + + // sets updater to manual mode -- if update is available, asks user + Updater &UpdateManual(void); + + // sets updater to auto -- updates app on launch without asking user + Updater &UpdateAuto(void); + + // disable updater -- app will not update on launch + // re-enable it to restart updating system on next run + Updater &UpdateDisable(void); + + bool IsAppInstalled(void) { return appInstalled; } + double GetInstalledVersion(void) { return installedVersion; } + double GetMaxVersion(void) { return maxVersion; } + String const &GetWebRoot(void) { return webRoot; } + + bool Run(void); +}; + +#endif diff --git a/bazaar/Updater/Updater.upp b/bazaar/Updater/Updater.upp new file mode 100644 index 000000000..756f3a09c --- /dev/null +++ b/bazaar/Updater/Updater.upp @@ -0,0 +1,11 @@ +description "Web Installer/Updater module\377"; + +uses + CtrlLib, + SysExec, + Web; + +file + Updater.h, + Updater.cpp; + diff --git a/bazaar/Updater/init b/bazaar/Updater/init new file mode 100644 index 000000000..4a5258ced --- /dev/null +++ b/bazaar/Updater/init @@ -0,0 +1,6 @@ +#ifndef _Updater_icpp_init_stub +#define _Updater_icpp_init_stub +#include "CtrlLib/init" +#include "SysExec/init" +#include "Web/init" +#endif diff --git a/bazaar/UpdaterTest/UpdaterTest.cpp b/bazaar/UpdaterTest/UpdaterTest.cpp new file mode 100644 index 000000000..5efc0c2f0 --- /dev/null +++ b/bazaar/UpdaterTest/UpdaterTest.cpp @@ -0,0 +1,22 @@ +#include + +double GetMaxVersion(void) +{ + return 2.0; +} + +GUI_APP_MAIN +{ + Updater updater; + updater + .SetMaxVersion(GetMaxVersion()) + .SetWebRoot("www.timberstruct.com/webupdater/updatertest") + .UpdateManual(); + + if(!updater.Run()) + return; + +// PromptOK("After update call -- should be in normal mode&User is : '" + GetUserName() + "'"); + +} + diff --git a/bazaar/UpdaterTest/UpdaterTest.upp b/bazaar/UpdaterTest/UpdaterTest.upp new file mode 100644 index 000000000..47c78a87b --- /dev/null +++ b/bazaar/UpdaterTest/UpdaterTest.upp @@ -0,0 +1,11 @@ +description "Testbed for Updater package\377"; + +uses + Updater; + +file + UpdaterTest.cpp; + +mainconfig + "" = "GUI"; + diff --git a/bazaar/UpdaterTest/init b/bazaar/UpdaterTest/init new file mode 100644 index 000000000..49503fbee --- /dev/null +++ b/bazaar/UpdaterTest/init @@ -0,0 +1,4 @@ +#ifndef _UpdaterTest_icpp_init_stub +#define _UpdaterTest_icpp_init_stub +#include "Updater/init" +#endif