Kaju self-updater talk (v.2.x)

  1. ‹ Older
  2. 7 weeks ago

    Frank S

    Feb 21 Australia

    To follow up:

    I tested my app on a real Windows 10 laptop and the Kaju Updater function worked without a hitch :) Repeated testing on my Windows 10 virtual machine consistently failed with the "requested operation cannot be performed on a file with a user-mapped section open" error. So I am going to put it down to an issue with running it on a virtual machine. Fortunately, my users shouldn't have to do that as they can use the native Mac version.


  3. Frank S

    Feb 22 Australia

    Unfortunately I celebrated too soon. On 2nd testing on my Windows 10 laptop, the update failed with the same error. On 3rd test it worked!

    I think I will have to try to change the workflow so that it downloads an InnoSetup installer instead that overwrites the previous version. Earlier in this thread some people have suggested this workaround when they have had similar problems. Would anyone have a working version that demonstrates how to do that?


  4. @Frank S I think I will have to try to change the workflow so that it downloads an InnoSetup installer instead that overwrites the previous version. Earlier in this thread some people have suggested this workaround when they have had similar problems. Would anyone have a working version that demonstrates how to do that?

    The link to the project is a demo of what I picked up from the forum a long time ago and then made some minor fixes. The bulk is not my work...
    I use this for both MacOS and Windows and works ok. I am using this for a deployed application for the last 4 years with at least 15 updates over time. Limitations with this implementation:

    • it uses HTTP and there might be problems with certain hosting domains security requirements
    • It downloads dmg and .exe files and I had to change hosting company as my original company identified innosetup .exe as malware and it got blocked/removed automatically.
    • There is no protection at all against downloading a modified file and downloading malware. It has no security whatsoever which is my main reason of switching to Kaju sooner or later.

    I just cut and paste this from my main app so not sure if I missed out something.

    Long term I still want to switch to Kaju because it is a much neater and more capable option, but on the other hand I don't have the confidence yet to deploy it for an application where the users are having poor computer literacy and are 2 hours flying from my place. If there is a problem, I have no way of getting help identifying the problem. At the moment I am in the mood of "if ain't broke, don't touch it". But I know it will break sooner or later.

    To me this is a work around and not a long term fix.

  5. Frank S

    Feb 22 Australia

    Thanks for that Niels :) I will have a look at the project and hopefully get some ideas. I've been working my way through following the logic of what Kaju does. The first couple times I tried to do that I got hopelessly confused, but now with a bit more knowledge, I'm starting to follow :) . Here is my understanding of what Kaju does, I write it here to help me get it straight in my head and perhaps also help other newbies like me (someone please correct me if I'm wrong):

    1. Instantiate a New Kaju.UpdateChecker in Open event of your main window
    Checker = New Kaju.UpdateChecker
    Checker is a property on your main window (an instance of Kaju.UpdateChecker)

    2. Check if any new updates are available
    Initiate the check by running the method Checker.ExecuteAsync
    This does a PreCheck to make sure that the user has write permissions and that the xcopy tool is available
    Then passes on to the method FetchAsync which instantiates a Kaju.HTTPSocketAsync, uses its Get method to query your website and sets the PageReceived event handler as AsyncHTTP_PageReceived

    3. Data is received from your website and processed
    AsyncHTTP_PageReceived calls the method ProcessRaw, which checks that the data is not corrupted by using Crypto.RSAVerifySignature( tester, sig, ServerPublicRSAKey ) then passes the data to ProcessUpdateData. If new updates are available then KajuUpdateWindow.ChooseUpdate is called

    4. KajuUpdateWindow is shown, populated with the available updates, the user chooses the update then presses the "Download Update" button (btnOK) which calls HandleOKButton

    5. Starts downloading the update zip file
    HandleOKButton, when at the CurrentStage of Stage.ChoosingUpdate, uses hsSocket.Get (an instance of Kaju.HTTPSocketAsync) to download the update zip file into a temporary folder

    6. Once the file finishes downloading, it is decompressed
    In the hsSocket.FileReceived event, it uses shZipper.Decompress (an instance of Kaju.ZipShell) to decompress the file into a folder within the App's main folder, called "AppName - decompressed", then changes the CurrentStage to Stage.WaitingToQuit

    7. User presses the Quit & Install button (btnOK)
    HandleOKButton calls Kaju.StartUpdate which passes the KajuUpdateWindow.Initiater to the App.UpdateInitiater property then quits the App

    8. The App.UpdateInitiater destructor event calls RunScript, which on Windows calls RunScriptWindows, which creates a Shell batch file that continues to run after the App has quit

    9. The Shell script (which is saved in the constant kUpdaterScript) performs the following steps

    • Copies all the program files into a backup folder within App's main folder, called "AppName-datetimestamp"
    • Moves the executable file into the backup folder
    • Copies all the new update files into the App's main folder from the decompressed folder using xcopy
    • Deletes the backup folder and decompressed folder
    • Launches the new updated App with the parameter "--kaju-success" so that it can detect that an update just occurred

    There is a lot of error checking and clean up along the way that I have not included (mainly because I don't understand it well :P)

    So what I am thinking of trying is to make the following changes:

    • Have an InnoSetup created Setup.exe uploaded to the website instead of a zip file
    • For Windows only, divert the flow at step 5:
    • Download Setup.exe into the Downloads folder instead of a temporary folder
    • When the user presses Quit & Install, simply run the Setup.exe

    If I manage to get something working, I'll post it back here!

  6. 6 weeks ago

    Frank S

    Feb 24 Australia

    Ok, I'm pretty pleased with myself, I've got it to work how I want and the user experience is quite streamlined, I feel :) Plus I've learnt a bit more about how InnoSetup and shell work.

    For Windows only, an InnoSetup installer is registered in the Kaju Admin app instead of a zip file and uploaded to your website. I've followed the usual Kaju workflow, up till the point where the file has been downloaded. So the benefits of Kaju checking the integrity of the downloaded data, using Crypto.RSAVerifySignature, and the downloaded file, by checking against the hash, are retained.

    When the user presses Quit & Install, the following sequence occurs:

    1. The setup.exe (which is downloaded into a temp folder) is launched
    2. App quits
    3. UAC dialog box appears asking if the user wants to allow Setup to make changes (get admin privileges)
    4. The wizard shows only the ready to install page and user click Install
    5. The wizard shows the installation complete page and the user can launch the updated app or close the wizard
    6. A small .bat file is created that deletes the setup.exe file then deletes itself

    It's worked out better than I had hoped, I'm starting to see how powerful InnoSetup is. You can use the same installer as for a new installation, as long as AppId is kept the same, InnoSetup will automatically install into the same directory and update the version info in the registry.

    Here is a dropbox link to a Kaju Update Test Altered project which demonstrates the changes that I made. All the changes are labelled with "//MyKajuChanges". Below is the InnoSetup script that I use to make the installer, which is based on the Xojo Documentation 64-bit example with bits of code that I found on various forums. I hope this can help other people who may have experienced similar problems to me.

    Kem, is it ok for me to share this? I've kind of bastardised your code and possibly introduced some errors in logic.

    InnoSetup Script
    ; Sample script for creating an installer for a 64-bit Xojo desktop app

    #define XojoAppName "MyApp"
    #define MyAppURL "https://mywebsite.com/"
    #define AppGUID "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"
    #define MyAppVersion GetFileVersion(AddBackslash(SourcePath) + "MyApp.exe")
    #define MyAppPublisher "Frank Yung-Tai Sun"
    #define MyAppExeName "{#XojoAppName}.exe"

    ; NOTE: The value of AppId uniquely identifies this application.
    ; Do not use the same AppId value in installers for other applications.
    AppVerName={#XojoAppName} {#MyAppVersion}
    ; Since no icons will be created in "{group}", we don't need the wizard
    ; to ask for a Start Menu folder name:
    LicenseFile=C:\path\to\{#XojoAppName} End User License Agreement.rtf
    ; Require Windows 7 SP1 or later

    Name: "english"; MessagesFile: "compiler:Default.isl"

    Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked

    ; These directories will be created by the installer inside the DefaultDirName
    ; (defined above).
    Name: "{app}\{#XojoAppName} Libs"
    Name: "{app}\{#XojoAppName} Resources"
    Name: "{app}\locales"
    Name: "{app}\swiftshader"

    ; These are the files to include. By default you want to include
    ; the EXE plus the Libs and Resources folders
    ; but you can include any other files you like as well.
    Source: ".\{#XojoAppName}\{#XojoAppName}.exe"; DestDir: "{app}"; Flags: ignoreversion signonce
    Source: ".\{#XojoAppName}\*"; DestDir: "{app}\"; Flags: ignoreversion recursesubdirs createallsubdirs
    Source: ".\{#XojoAppName}\{#XojoAppName} Libs\*"; DestDir: "{app}\{#XojoAppName} Libs"; Flags: ignoreversion recursesubdirs createallsubdirs
    Source: ".\{#XojoAppName}\{#XojoAppName} Resources\*"; DestDir: "{app}\{#XojoAppName} Resources"; Flags: ignoreversion recursesubdirs createallsubdirs
    Source: ".\{#XojoAppName}\locales\*"; DestDir: "{app}\locales"; Flags: ignoreversion recursesubdirs createallsubdirs
    Source: ".\{#XojoAppName}\swiftshader\*"; DestDir: "{app}\swiftshader"; Flags: ignoreversion recursesubdirs createallsubdirs

    ; NOTE: Don't use "Flags: ignoreversion" on any shared system files

    ; Creates icons/links in the Start Menu and/or the desktop if the user chooses during installation.
    ;Name: "{group}\{#XojoAppName}"; Filename: "{app}\{#XojoAppName}.exe";
    Name: "{commonprograms}\{#XojoAppName}"; Filename: "{app}\{#XojoAppName}.exe";
    Name: "{commondesktop}\{#XojoAppName}"; Filename: "{app}\{#XojoAppName}.exe"; Tasks: desktopicon;

    ; Give the user the option to run the app after the installation is finished.
    Filename: "{app}\{#XojoAppName}.exe"; Description: "{cm:LaunchProgram,{#XojoAppName}}"; Flags: nowait postinstall skipifsilent

    ; This specifies the Visual C++ Windows Runtime Redistributable to also install because
    ; it is required by Xojo apps made with 2016r1 or later.
    Source: ".\Windows Universal Runtime\Installers\VC_redist.x64.exe"; DestDir: {tmp}

    Filename: {tmp}\VC_redist.x64.exe; Parameters: "/install /quiet /norestart"; StatusMsg: "Installing 64-bit Windows Universal runtime..."; Flags: waituntilterminated; Check: InstallVCRuntime();

    // Most of this code is pinched from https://stackoverflow.com/questions/2000296/inno-setup-how-to-automatically-uninstall-previous-installed-version

    function GetUninstallString(): String;
    sUnInstPath: String;
    sUnInstallString: String;
    sUnInstPath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{#emit SetupSetting("AppId")}_is1');
    sUnInstallString := '';
    if not RegQueryStringValue(HKLM, sUnInstPath, 'UninstallString', sUnInstallString) then
    RegQueryStringValue(HKCU, sUnInstPath, 'UninstallString', sUnInstallString);
    Result := sUnInstallString;

    function IsUpgrade(): Boolean;
    Result := (GetUninstallString() <> '');

    function UnInstallOldVersion(): Integer;
    sUnInstallString: String;
    iResultCode: Integer;
    // Return Values:
    // 1 - uninstall string is empty
    // 2 - error executing the UnInstallString
    // 3 - successfully executed the UnInstallString

    // default return value
    Result := 0;

    // get the uninstall string of the old app
    sUnInstallString := GetUninstallString();
    if sUnInstallString <> '' then begin
    sUnInstallString := RemoveQuotes(sUnInstallString);
    if Exec(sUnInstallString, '/SILENT /NORESTART /SUPPRESSMSGBOXES','', SW_HIDE, ewWaitUntilTerminated, iResultCode) then
    Result := 3
    Result := 2;
    end else
    Result := 1;

    // Uninstalling old version doesn't seem to be necessary unless app file structure has changed
    // procedure CurStepChanged(CurStep: TSetupStep);
    // begin
    // if (CurStep=ssInstall) then
    // begin
    // if (IsUpgrade()) then
    // begin
    // UnInstallOldVersion();
    // end;
    // end;
    // end;

    function ShouldSkipPage(PageID: Integer): Boolean;
    Result := False;

    if ((PageID = wpLicense) or (PageID = wpSelectTasks)) and (IsUpgrade()) then
    Result := True;

    // Adapted from https://stackoverflow.com/questions/3618257/is-it-possible-to-accept-custom-command-line-parameters-with-inno-setup
    { Check if there is a command-line parameter "kajuupdate=true" }
    function IsKajuUpdate(): Boolean;
    Result := False;
    if ExpandConstant('{param:kajuupdate|false}') = 'true' then
    Result := True;

    { If installer is being run by a kaju update then user already has VCRuntime }
    function InstallVCRuntime(): Boolean;
    Result := not IsKajuUpdate();

    // Adapted from https://stackoverflow.com/questions/28763220/inno-setup-delete-the-installer-after-the-install-process
    procedure CurStepChanged(CurStep: TSetupStep);
    strContent: String;
    intErrorCode: Integer;
    strSelf_Delete_BAT: String;
    if (CurStep=ssDone) and (IsKajuUpdate()) then
    // strContent := ':try_delete' + #13 + #10 +
    // 'del "' + ExpandConstant('{srcexe}') + '"' + #13 + #10 +
    // 'if exist "' + ExpandConstant('{srcexe}') + '" goto try_delete' + #13 + #10 +
    // 'del %0';

    strContent := 'for /L %%a in (1,1,100) do (' + #13 + #10 +
    'del "' + ExpandConstant('{srcexe}') + '"' + #13 + #10 +
    'if not exist "' + ExpandConstant('{srcexe}') + '" goto end_loop' + #13 + #10 +
    ')' + #13 + #10 +
    ':end_loop' + #13 + #10 +
    'del %0';
    strSelf_Delete_BAT := ExtractFilePath(ExpandConstant('{tmp}')) + 'SelfDelete.bat';
    SaveStringToFile(strSelf_Delete_BAT, strContent, False);
    Exec(strSelf_Delete_BAT, '', '', SW_HIDE, ewNoWait, intErrorCode);

  7. Kem T

    Feb 24 Pre-Release Testers, Xojo Pro, XDC Speakers, MVP Connecticut

    I can only tell you my preference, and that's 1) you share it on GitHub as a fork, 2) submit your additions in a pull request so I can consider adding them to the main project, and 3) make sure your changes are clearly marked so there is no confusion between the main project and yours.

    Nice work though. This is something I've wanted to add on both the Mac and Windows sides for some time, but just haven't found the time.

  8. Frank S

    Feb 25 Australia

    Hi Kem, ok thanks. It'll probably take me a while, but will do - GitHub is something I've been avoiding so far at it seems so intimidating to learn! But gotta take the plunge some time, i guess. It'll be good to contribute something back to this community.

  9. Beatrix W

    Feb 25 Pre-Release Testers, Third Party Store Europe (Germany)
    Edited 6 weeks ago

    One user just sent me an error log with an IOException from Kaju:

    2020-02-25, 15:07:08 An error happened:
    2020-02-25, 15:07:08 Class/Method: KajuUpdateWindow.HandleOKButton
    2020-02-25, 15:07:08 Time: Tuesday, February 25, 2020 15:07:08 1496171
    2020-02-25, 15:07:08 Type of Error: Xojo.IO.IOException
    2020-02-25, 15:07:08 --------------------------
    2020-02-25, 15:07:08 Stack:
    2020-02-25, 15:07:08
    Sub Xojo.Net.HTTPSocket.Send(Xojo.Net.HTTPSocket , Xojo.IO.FolderItem )
    Sub KajuHTTPSocketAsync.Get(string, FolderItem)
    Sub KajuUpdateWindow.KajuUpdateWindow.HandleOKButton(KajuUpdateWindow.KajuUpdateWindow)
    Sub KajuUpdateWindow.KajuUpdateWindow.btnOK_Action(KajuUpdateWindow.KajuUpdateWindow, PushButton)
    Sub Delegate.IM_Invoke(PushButton)
    Sub AddHandler.Stub.15()
    Sub Application._CallFunctionWithExceptionHandling()

    Then I had a look at the code:

    Public Sub Get(url As String, file As FolderItem)
      SetSecure url
      dim path as string = file.NativePath.DefineEncoding( Encodings.UTF8 )
      dim f as new Xojo.IO.FolderItem( path.ToText )
      super.Send "GET", url.ToText, f
    End Sub

    The user must have managed to loose access to his own download folder on Mojave.

  10. 4 weeks ago

    Douglas H

    Mar 10 Pre-Release Testers, Xojo Pro

    I've been successfully using Kaju v2.1 for macOS updates for all but one user, who never got the prompt an updates was available, whether or not a silent check was being made. The app menu option to check for an update never displayed any result, even if simply "No updates available".

    It was only a single user with this issue, and was friendly and didn't really mind having to install himself when I sent links, but it always struck me as odd. He did not have the firewall turned on, or anything other malware or virus detection software active, but I still attributed it to something unique about his machine.

    This weekend we were in the same city, so he gave me a chance to sit together and debug the issue. And to my initial surprise, when I stepped through the code in debug it worked and told me there were no updates available. So then I removed all my breakpoints but still ran in debug mode and it still worked. Compiled the same source, and it failed to report anything! So I figured it had to be timing dependent, and looked closer at how Kaju.UpdateCheckerFetchAsync performed this section of code:

    dim url as string = UpdateURL
    dim http as Kaju.HTTPSocketAsync = GetAsyncHTTPSocket
    http.Get url

    with the GetAsyncHTTPSocket method doing this:

    if AsyncHTTP is nil then
      AsyncHTTP = new HTTPSocketAsync
      AddHandler AsyncHTTP.PageReceived, WeakAddressOf AsyncHTTP_PageReceived
      AddHandler AsyncHTTP.Error, WeakAddressOf AsyncHTTP_Error
    end if
    return AsyncHTTP

    So on a whim, since it seemed to be timing dependent and since I already had the MBS plugins available, changed to this:

    if AsyncHTTP is nil then
      AsyncHTTP = new HTTPSocketAsync
      AddHandler AsyncHTTP.PageReceived, WeakAddressOf AsyncHTTP_PageReceived
      AddHandler AsyncHTTP.Error, WeakAddressOf AsyncHTTP_Error
    end if
    // Add slight delay to allow time for AddHandlers to complete
    return AsyncHTTP

    to add a 1/20 second delay before the socket was returned.

    It then worked even in compiled mode. This user's machine was the fastest of any of my users -- including me -- and it at least seems the socket was getting returned and the .Get url completing faster than the AddHandler's had taken effect. Or so it seems.

    And perhaps 0.05 seconds is overkill and another shorter delay tactic would suffice. But I'm ok with adding 1/20 second delay into an operation so am just leaving it as it for now.

    So this is more for the sake of reporting the issue I saw and the eventual circumvention in the hopes of helping others.

    This was using Kaju v2.1, and Xojo 2019R1.1, with client machine running 10.14.16 (Mojave) with firewall etc off.

or Sign Up to reply!