From 33cb171fb1e5012d2f93fbc6c74ee4d1cab72d83 Mon Sep 17 00:00:00 2001 From: Flavio Fois Date: Wed, 11 Feb 2026 16:54:58 +0100 Subject: [PATCH] Adds silent update installation feature Implements silent update installation with detached process execution. This change introduces methods to perform silent updates, allowing the application to upgrade without user interaction. It also allows for custom SMB/network paths. The installer is launched as a detached process to prevent blocking issues with the main application, and the application quits after the installer launches. --- app_update.go | 232 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) diff --git a/app_update.go b/app_update.go index d637d28..f1659b6 100644 --- a/app_update.go +++ b/app_update.go @@ -410,6 +410,238 @@ func shellExecuteAsAdmin(exePath string) error { return nil } +// launchDetachedInstaller launches the installer as a completely detached process +// using CreateProcess with DETACHED_PROCESS and CREATE_NEW_PROCESS_GROUP flags. +// This allows the installer to continue running and close EMLy without errors. +// +// Parameters: +// - exePath: Full path to the installer executable +// - args: Array of command-line arguments to pass to the installer +// +// Returns: +// - error: Error if process creation fails +func launchDetachedInstaller(exePath string, args []string) error { + // Build command line: executable path + arguments + cmdLine := fmt.Sprintf(`"%s"`, exePath) + if len(args) > 0 { + cmdLine += " " + strings.Join(args, " ") + } + + log.Printf("Launching detached installer: %s", cmdLine) + + // Convert to UTF16 for Windows API + cmdLinePtr := syscall.StringToUTF16Ptr(cmdLine) + + // Setup process startup info + var si syscall.StartupInfo + var pi syscall.ProcessInformation + + si.Cb = uint32(unsafe.Sizeof(si)) + si.Flags = syscall.STARTF_USESHOWWINDOW + si.ShowWindow = syscall.SW_HIDE // Hide installer window (silent mode) + + // Process creation flags: + // CREATE_NEW_PROCESS_GROUP: Creates process in new process group + // DETACHED_PROCESS: Process has no console, completely detached from parent + const ( + CREATE_NEW_PROCESS_GROUP = 0x00000200 + DETACHED_PROCESS = 0x00000008 + ) + flags := uint32(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS) + + // Create the detached process + err := syscall.CreateProcess( + nil, // Application name (nil = use command line) + cmdLinePtr, // Command line + nil, // Process security attributes + nil, // Thread security attributes + false, // Inherit handles + flags, // Creation flags + nil, // Environment (nil = inherit) + nil, // Current directory (nil = inherit) + &si, // Startup info + &pi, // Process information (output) + ) + + if err != nil { + log.Printf("CreateProcess failed: %v", err) + return fmt.Errorf("failed to create detached process: %w", err) + } + + // Close process and thread handles immediately + // We don't need to wait for the process - it's fully detached + syscall.CloseHandle(pi.Process) + syscall.CloseHandle(pi.Thread) + + log.Printf("Detached installer process launched successfully (PID: %d)", pi.ProcessId) + + return nil +} + +// InstallUpdateSilent downloads the update (if needed) and launches the installer +// in completely silent mode with a detached process. The installer will run with +// these arguments: /VERYSILENT /ALLUSERS /SUPPRESSMSGBOXES /NORESTART /FORCEUPGRADE +// +// This method automatically quits EMLy after launching the installer, allowing the +// installer to close the application and complete the upgrade without user interaction. +// +// Returns: +// - error: Error if download or launch fails +func (a *App) InstallUpdateSilent() error { + log.Println("Starting silent update installation...") + + // If installer not ready, attempt to download first + if !updateStatus.Ready || updateStatus.InstallerPath == "" { + log.Println("Installer not ready, downloading update first...") + + _, err := a.DownloadUpdate() + if err != nil { + errMsg := fmt.Sprintf("Failed to download update: %v", err) + log.Println(errMsg) + updateStatus.ErrorMessage = errMsg + return fmt.Errorf("download failed: %w", err) + } + + // Wait briefly for download to complete + log.Println("Download initiated, waiting for completion...") + for i := 0; i < 60; i++ { // Wait up to 60 seconds + time.Sleep(1 * time.Second) + if updateStatus.Ready { + break + } + if updateStatus.ErrorMessage != "" { + return fmt.Errorf("download error: %s", updateStatus.ErrorMessage) + } + } + + if !updateStatus.Ready { + return fmt.Errorf("download timeout - update not ready after 60 seconds") + } + } + + installerPath := updateStatus.InstallerPath + + // Verify installer exists + if _, err := os.Stat(installerPath); os.IsNotExist(err) { + updateStatus.ErrorMessage = "Installer file not found" + updateStatus.Ready = false + log.Printf("Installer not found: %s", installerPath) + return fmt.Errorf("installer not found: %s", installerPath) + } + + log.Printf("Installer ready at: %s", installerPath) + + // Prepare silent installation arguments + args := []string{ + "/VERYSILENT", // No UI, completely silent + "/ALLUSERS", // Install for all users (requires admin) + "/SUPPRESSMSGBOXES", // Suppress all message boxes + "/NORESTART", // Don't restart system + "/FORCEUPGRADE", // Skip upgrade confirmation dialog + `/LOG="C:\install.log"`, // Create installation log + } + + log.Printf("Launching installer with args: %v", args) + + // Launch detached installer + if err := launchDetachedInstaller(installerPath, args); err != nil { + errMsg := fmt.Sprintf("Failed to launch installer: %v", err) + log.Println(errMsg) + updateStatus.ErrorMessage = errMsg + return fmt.Errorf("failed to launch installer: %w", err) + } + + log.Println("Detached installer launched successfully, quitting EMLy...") + + // Quit application to allow installer to replace files + time.Sleep(500 * time.Millisecond) // Brief delay to ensure installer starts + runtime.Quit(a.ctx) + + return nil +} + +// InstallUpdateSilentFromPath downloads an installer from a custom SMB/network path +// and launches it in silent mode with a detached process. Use this when you know the +// exact installer path (e.g., \\server\updates\EMLy_Installer.exe) without needing +// to check the version.json manifest. +// +// Parameters: +// - smbPath: Full UNC path or local path to the installer (e.g., \\server\share\EMLy.exe) +// +// Returns: +// - error: Error if download or launch fails +func (a *App) InstallUpdateSilentFromPath(smbPath string) error { + log.Printf("Starting silent installation from custom path: %s", smbPath) + + // Verify source installer exists and is accessible + if _, err := os.Stat(smbPath); os.IsNotExist(err) { + errMsg := fmt.Sprintf("Installer not found at: %s", smbPath) + log.Println(errMsg) + return fmt.Errorf("%s", errMsg) + } + + // Create temporary directory for installer + tempDir := os.TempDir() + installerFilename := filepath.Base(smbPath) + tempInstallerPath := filepath.Join(tempDir, installerFilename) + + log.Printf("Copying installer to temp location: %s", tempInstallerPath) + + // Copy installer from SMB path to local temp + sourceFile, err := os.Open(smbPath) + if err != nil { + errMsg := fmt.Sprintf("Failed to open source installer: %v", err) + log.Println(errMsg) + return fmt.Errorf("failed to open installer: %w", err) + } + defer sourceFile.Close() + + destFile, err := os.Create(tempInstallerPath) + if err != nil { + errMsg := fmt.Sprintf("Failed to create temp installer: %v", err) + log.Println(errMsg) + return fmt.Errorf("failed to create temp file: %w", err) + } + defer destFile.Close() + + // Copy file + bytesWritten, err := io.Copy(destFile, sourceFile) + if err != nil { + errMsg := fmt.Sprintf("Failed to copy installer: %v", err) + log.Println(errMsg) + return fmt.Errorf("failed to copy installer: %w", err) + } + + log.Printf("Installer copied successfully (%d bytes)", bytesWritten) + + // Prepare silent installation arguments + args := []string{ + "/VERYSILENT", // No UI, completely silent + "/ALLUSERS", // Install for all users (requires admin) + "/SUPPRESSMSGBOXES", // Suppress all message boxes + "/NORESTART", // Don't restart system + "/FORCEUPGRADE", // Skip upgrade confirmation dialog + `/LOG="C:\install.log"`, // Create installation log + } + + log.Printf("Launching installer with args: %v", args) + + // Launch detached installer + if err := launchDetachedInstaller(tempInstallerPath, args); err != nil { + errMsg := fmt.Sprintf("Failed to launch installer: %v", err) + log.Println(errMsg) + return fmt.Errorf("failed to launch installer: %w", err) + } + + log.Println("Detached installer launched successfully, quitting EMLy...") + + // Quit application to allow installer to replace files + time.Sleep(500 * time.Millisecond) // Brief delay to ensure installer starts + runtime.Quit(a.ctx) + + return nil +} + // ============================================================================= // Status Methods // =============================================================================