@@ -13,6 +13,7 @@ import java.nio.file.Files
1313import java.nio.file.Path
1414import java.nio.file.StandardCopyOption
1515import java.security.MessageDigest
16+ import java.util.logging.Logger
1617import java.util.regex.Matcher
1718import java.util.zip.ZipEntry
1819import java.util.zip.ZipInputStream
@@ -24,6 +25,7 @@ import java.util.zip.ZipInputStream
2425 */
2526final class UpdateService {
2627
28+ private static final Logger log = Logger . getLogger(UpdateService . name)
2729 private static final String REPO = ' Alipsa/accounting'
2830 private static final String LATEST_RELEASE_URL = " https://api.github.com/repos/${ REPO} /releases/latest"
2931 private static final String DIST_ASSET_PREFIX = ' app-'
@@ -83,6 +85,7 @@ final class UpdateService {
8385 throw new IllegalStateException (' No download URL available for the update.' )
8486 }
8587
88+ log. info(" Downloading update ${ info.availableVersion} from ${ info.downloadUrl} " )
8689 Path stagingDir = stagingDirectory()
8790 Files . createDirectories(stagingDir)
8891 Path targetFile = stagingDir. resolve(" app-${ info.availableVersion} .zip" )
@@ -115,9 +118,11 @@ final class UpdateService {
115118 }
116119
117120 if (info. checksumUrl != null ) {
121+ log. info(" Verifying checksum for ${ targetFile} " )
118122 verifyChecksum(targetFile, info. checksumUrl, client)
119123 }
120124
125+ log. info(" Downloaded update archive to ${ targetFile} " )
121126 targetFile
122127 }
123128
@@ -153,21 +158,36 @@ final class UpdateService {
153158 }
154159
155160 void applyUpdateAndRestart (Path downloadedZip ) {
161+ applyUpdateAndRestart(downloadedZip, null )
162+ }
163+
164+ void applyUpdateAndRestart (Path downloadedZip , Closure<Void > phaseCallback ) {
165+ log. info(" Preparing to apply update from ${ downloadedZip} " )
156166 Path stagingDir = stagingDirectory()
157167 Path extractedDir = stagingDir. resolve(' extracted' )
168+ notifyApplyPhase(phaseCallback, ApplyPhase . EXTRACTING )
169+ log. info(" Extracting update archive to ${ extractedDir} " )
158170 deleteDirectoryContents(extractedDir)
159171 Files . createDirectories(extractedDir)
160172
161173 extractJars(downloadedZip, extractedDir)
162174
175+ notifyApplyPhase(phaseCallback, ApplyPhase . STAGING )
176+ log. info(' Staging updater script.' )
163177 Path installDir = installationJarDirectory()
164178 if (installDir == null ) {
165179 throw new IllegalStateException (' Cannot determine installation JAR directory.' )
166180 }
181+ Path updaterLog = updateLogPath()
182+ Files . createDirectories(updaterLog. parent)
167183
168- Path updaterScript = writeUpdaterScript(stagingDir, extractedDir, installDir)
184+ Path updaterScript = writeUpdaterScript(stagingDir, extractedDir, installDir, updaterLog)
185+ log. info(" Updater script written to ${ updaterScript} ; updater log will be ${ updaterLog} " )
186+ notifyApplyPhase(phaseCallback, ApplyPhase . LAUNCHING )
187+ log. info(' Launching updater and exiting application.' )
169188 LoggingConfigurer . shutdown()
170189 launchUpdaterAndExit(updaterScript)
190+ throw new IllegalStateException (' Updater was launched, but the application did not exit.' )
171191 }
172192
173193 Path installationJarDirectory () {
@@ -190,6 +210,10 @@ final class UpdateService {
190210 launcherPath(jarDir, System . getProperty(' os.name' , ' ' ). toLowerCase(Locale . ROOT ))
191211 }
192212
213+ Path updateLogPath () {
214+ AppPaths . logDirectory(). resolve(' updater.log' )
215+ }
216+
193217 static Path launcherPath (Path jarDir , String osName ) {
194218 if (osName. contains(' win' )) {
195219 return appImageRoot(jarDir). resolve(' AlipsaAccounting.exe' )
@@ -225,65 +249,133 @@ final class UpdateService {
225249 }
226250 }
227251
228- private Path writeUpdaterScript (Path stagingDir , Path extractedDir , Path installDir ) {
252+ private Path writeUpdaterScript (Path stagingDir , Path extractedDir , Path installDir , Path updaterLog ) {
229253 String osName = System . getProperty(' os.name' , ' ' ). toLowerCase(Locale . ROOT )
230254 boolean isWindows = osName. contains(' win' )
231255 Path launcher = launcherPath()
232256 String launcherCommand = launcher != null ? " \" ${ launcher} \" " : ' '
233257 Path backupDir = installDir. resolve(' .update-backup' )
234258 String newMainJar = mainJarFileName(extractedDir) ?: ' '
235259 String newVersion = versionFromMainJar(newMainJar) ?: ' '
260+ UpdaterScriptContext context = new UpdaterScriptContext (
261+ stagingDir,
262+ extractedDir,
263+ installDir,
264+ updaterLog,
265+ backupDir,
266+ launcherCommand,
267+ newMainJar,
268+ newVersion
269+ )
236270
237271 Path script
238272 if (isWindows) {
239273 script = stagingDir. resolve(' updater.bat' )
240- script. toFile(). text = """ \
274+ script. toFile(). text = windowsUpdaterScript(context)
275+ } else {
276+ script = stagingDir. resolve(' updater.sh' )
277+ script. toFile(). text = unixUpdaterScript(context)
278+ script. toFile(). setExecutable(true )
279+ }
280+ script
281+ }
282+
283+ private static String windowsUpdaterScript (UpdaterScriptContext context ) {
284+ """ \
241285@echo off
286+ set "LOG_FILE=${ context.updaterLog} "
287+ call :main >> "%LOG_FILE%" 2>&1
288+ exit /b %ERRORLEVEL%
289+
290+ :main
291+ echo [%DATE% %TIME%] Starting update.
292+ echo [%DATE% %TIME%] Install dir: ${ context.installDir}
293+ echo [%DATE% %TIME%] Extracted dir: ${ context.extractedDir}
242294timeout /t 3 /nobreak >nul
243- if exist "${ backupDir} " rd /s /q "${ backupDir} "
244- mkdir "${ backupDir} "
245- for %%f in ("${ installDir} \\ *.jar") do move "%%f" "${ backupDir} \\ "
246- for %%f in ("${ extractedDir} \\ *.jar") do copy /y "%%f" "${ installDir} \\ "
295+ echo [%DATE% %TIME%] Preparing backup directory: ${ context.backupDir}
296+ if exist "${ context.backupDir} " rd /s /q "${ context.backupDir} "
297+ mkdir "${ context.backupDir} "
298+ if errorlevel 1 (
299+ echo [%DATE% %TIME%] Failed to create backup directory.
300+ exit /b 1
301+ )
302+ echo [%DATE% %TIME%] Backing up current JAR files.
303+ for %%f in ("${ context.installDir} \\ *.jar") do move "%%f" "${ context.backupDir} \\ "
247304if errorlevel 1 (
248- echo Update failed, restoring backup...
249- for %%f in ("${ backupDir} \\ *.jar") do move "%%f" "${ installDir} \\ "
250- rd /s /q "${ backupDir} "
251- echo Update failed. Please try again.
305+ echo [%DATE% %TIME%] Failed to back up current JAR files.
306+ exit /b 1
307+ )
308+ echo [%DATE% %TIME%] Copying updated JAR files.
309+ for %%f in ("${ context.extractedDir} \\ *.jar") do copy /y "%%f" "${ context.installDir} \\ "
310+ if errorlevel 1 (
311+ echo [%DATE% %TIME%] Update failed while copying files, restoring backup...
312+ for %%f in ("${ context.backupDir} \\ *.jar") do move "%%f" "${ context.installDir} \\ "
313+ rd /s /q "${ context.backupDir} "
314+ echo [%DATE% %TIME%] Update failed. Please try again.
252315 pause
253316 exit /b 1
254317)
255- ${ newMainJar.isEmpty() ? '' : windowsConfigUpdateCommand(installDir, newMainJar, newVersion)}
256- rd /s /q "${ backupDir} "
257- rd /s /q "${ extractedDir} "
258- del "${ stagingDir} \\ *.zip"
259- ${ launcherCommand.isEmpty() ? 'echo Update complete.' : "start \"\" ${launcherCommand}"}
318+ echo [%DATE% %TIME%] Updating launcher configuration.
319+ ${ context.newMainJar.isEmpty() ? '' : windowsConfigUpdateCommand(context.installDir, context.newMainJar, context.newVersion)}
320+ echo [%DATE% %TIME%] Cleaning update staging files.
321+ rd /s /q "${ context.backupDir} "
322+ rd /s /q "${ context.extractedDir} "
323+ del "${ context.stagingDir} \\ *.zip"
324+ echo [%DATE% %TIME%] Launching application.
325+ ${ context.launcherCommand.isEmpty() ? 'echo Update complete.' : "start \"\" ${context.launcherCommand}"}
326+ echo [%DATE% %TIME%] Update script finished.
260327del "%~f0"
261328""" . stripIndent()
262- } else {
263- script = stagingDir. resolve(' updater.sh' )
264- script. toFile(). text = """ \
329+ }
330+
331+ private static String unixUpdaterScript (UpdaterScriptContext context ) {
332+ """ \
265333#!/usr/bin/env bash
266- sleep 3
267- rm -rf "${ backupDir} "
268- mkdir -p "${ backupDir} "
269- mv "${ installDir} /"*.jar "${ backupDir} /"
270- if ! cp "${ extractedDir} /"*.jar "${ installDir} /"; then
271- echo "Update failed, restoring backup..."
272- mv "${ backupDir} /"*.jar "${ installDir} /"
273- rm -rf "${ backupDir} "
274- echo "Update failed. Please try again."
334+ LOG_FILE="${ context.updaterLog} "
335+ exec >> "\$ LOG_FILE" 2>&1
336+
337+ timestamp() {
338+ date -Is
339+ }
340+
341+ log() {
342+ echo "[\$ (timestamp)] \$ *"
343+ }
344+
345+ fail() {
346+ log "\$ *"
275347 exit 1
348+ }
349+
350+ log "Starting update."
351+ log "Install dir: ${ context.installDir} "
352+ log "Extracted dir: ${ context.extractedDir} "
353+ sleep 3
354+ log "Preparing backup directory: ${ context.backupDir} "
355+ rm -rf "${ context.backupDir} "
356+ mkdir -p "${ context.backupDir} " || fail "Failed to create backup directory."
357+ log "Backing up current JAR files."
358+ if ! mv "${ context.installDir} /"*.jar "${ context.backupDir} /"; then
359+ fail "Failed to back up current JAR files."
360+ fi
361+ log "Copying updated JAR files."
362+ if ! cp "${ context.extractedDir} /"*.jar "${ context.installDir} /"; then
363+ log "Update failed while copying files, restoring backup..."
364+ mv "${ context.backupDir} /"*.jar "${ context.installDir} /"
365+ rm -rf "${ context.backupDir} "
366+ fail "Update failed. Please try again."
276367fi
277- ${ newMainJar.isEmpty() ? '' : unixConfigUpdateCommand(installDir, newMainJar, newVersion)}
278- rm -rf "${ backupDir} "
279- rm -rf "${ extractedDir} "
280- rm -f "${ stagingDir} /"*.zip
281- ${ launcherCommand.isEmpty() ? 'echo "Update complete."' : "exec ${launcherCommand} &"}
368+ log "Updating launcher configuration."
369+ ${ context.newMainJar.isEmpty() ? '' : unixConfigUpdateCommand(context.installDir, context.newMainJar, context.newVersion)}
370+ log "Cleaning update staging files."
371+ rm -rf "${ context.backupDir} "
372+ rm -rf "${ context.extractedDir} "
373+ rm -f "${ context.stagingDir} /"*.zip
374+ log "Launching application."
375+ ${ context.launcherCommand.isEmpty() ? 'echo "Update complete."' : "${context.launcherCommand} &"}
376+ log "Update script finished."
282377rm -f "\$ 0"
283378""" . stripIndent()
284- script. toFile(). setExecutable(true )
285- }
286- script
287379 }
288380
289381 private static String mainJarFileName (Path directory ) {
@@ -385,6 +477,49 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-ChildItem -LiteralPa
385477 }
386478 }
387479
480+ private static void notifyApplyPhase (Closure<Void > callback , ApplyPhase phase ) {
481+ if (callback != null ) {
482+ callback. call(phase)
483+ }
484+ }
485+
486+ enum ApplyPhase {
487+ EXTRACTING ,
488+ STAGING ,
489+ LAUNCHING
490+ }
491+
492+ private static final class UpdaterScriptContext {
493+
494+ final Path stagingDir
495+ final Path extractedDir
496+ final Path installDir
497+ final Path updaterLog
498+ final Path backupDir
499+ final String launcherCommand
500+ final String newMainJar
501+ final String newVersion
502+
503+ private UpdaterScriptContext (
504+ Path stagingDir ,
505+ Path extractedDir ,
506+ Path installDir ,
507+ Path updaterLog ,
508+ Path backupDir ,
509+ String launcherCommand ,
510+ String newMainJar ,
511+ String newVersion ) {
512+ this . stagingDir = stagingDir
513+ this . extractedDir = extractedDir
514+ this . installDir = installDir
515+ this . updaterLog = updaterLog
516+ this . backupDir = backupDir
517+ this . launcherCommand = launcherCommand
518+ this . newMainJar = newMainJar
519+ this . newVersion = newVersion
520+ }
521+ }
522+
388523 static final class UpdateInfo {
389524
390525 String currentVersion
0 commit comments