Compare commits

...

2 commits

Author SHA1 Message Date
avi
e794f241ba chore(release): bump to 0.1.7 2026-10-04 10:03:04 -05:00
avi
1f001dc306 fix(updater): narrate the handoff to ~/.config/Folio/update.log + safe tree flatten
- installer script now logs every step (wait, extract, flatten, swap, relaunch)
  to a persistent log so a failed update is diagnosable after the fact
- flatten the electron-builder nested dir by adopting the dir itself; the old
  'mv NEW/* ' glob missed dotfiles and could leave a half-moved tree (prime
  suspect for the two in-app update failures)
- mkdir failure aborts with relaunch of the old app instead of falling through
2026-10-04 10:03:04 -05:00
4 changed files with 32 additions and 8 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "folio", "name": "folio",
"version": "0.1.6", "version": "0.1.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "folio", "name": "folio",
"version": "0.1.6", "version": "0.1.7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tiptap/core": "^2.27.2", "@tiptap/core": "^2.27.2",

View file

@ -1,6 +1,6 @@
{ {
"name": "folio", "name": "folio",
"version": "0.1.6", "version": "0.1.7",
"description": "A simple, local, open-source desktop writing app.", "description": "A simple, local, open-source desktop writing app.",
"productName": "Folio", "productName": "Folio",
"author": "Folio Contributors", "author": "Folio Contributors",

View file

@ -1254,6 +1254,9 @@ handleIpc("folio:performUpdate", async () => {
app.getPath("temp"), app.getPath("temp"),
`folio-update-${process.pid}.sh` `folio-update-${process.pid}.sh`
); );
// Persistent, append-only handoff log: survives the swap and tells us
// exactly where a failed update stopped (temp files can vanish).
const updateLog = path.join(app.getPath("userData"), "update.log");
fs.writeFileSync( fs.writeFileSync(
script, script,
buildInstallerScript({ buildInstallerScript({
@ -1261,6 +1264,7 @@ handleIpc("folio:performUpdate", async () => {
tarball, tarball,
installDir: cfg.installDir, installDir: cfg.installDir,
relaunch: cfg.launcher, relaunch: cfg.launcher,
log: updateLog,
}), }),
{ mode: 0o700 } { mode: 0o700 }
); );

View file

@ -163,6 +163,8 @@ export interface InstallerSpec {
tarball: string; tarball: string;
installDir: string; installDir: string;
relaunch: string; relaunch: string;
/** Optional append-only log; the script narrates every step into it. */
log?: string;
} }
// The handoff script. Deliberately forgiving (no set -e): on any failure it // The handoff script. Deliberately forgiving (no set -e): on any failure it
@ -170,7 +172,10 @@ export interface InstallerSpec {
// user without an app. The old tree is kept as .old.<pid> and pruned only // user without an app. The old tree is kept as .old.<pid> and pruned only
// after a successful swap. // after a successful swap.
export function buildInstallerScript(spec: InstallerSpec): string { export function buildInstallerScript(spec: InstallerSpec): string {
const { pid, tarball, installDir, relaunch } = spec; const { pid, tarball, installDir, relaunch, log } = spec;
const logLine = log
? `say() { echo "$(date '+%F %T') $*" >> ${shQuote(log)} 2>/dev/null; }`
: "say() { :; }";
return [ return [
"#!/bin/sh", "#!/bin/sh",
"# Folio updater — waits for the app to exit, swaps in the new version,", "# Folio updater — waits for the app to exit, swaps in the new version,",
@ -181,29 +186,44 @@ export function buildInstallerScript(spec: InstallerSpec): string {
`RELAUNCH=${shQuote(relaunch)}`, `RELAUNCH=${shQuote(relaunch)}`,
`NEW="$INSTALL.update-new.$$"`, `NEW="$INSTALL.update-new.$$"`,
`OLD="$INSTALL.old.$$"`, `OLD="$INSTALL.old.$$"`,
logLine,
'say "handoff start pid=$PID tarball=$TARBALL"',
"# Wait (bounded to ~2 minutes) for the app process to exit.", "# Wait (bounded to ~2 minutes) for the app process to exit.",
"i=0", "i=0",
'while [ "$PID" -gt 0 ] 2>/dev/null && kill -0 "$PID" 2>/dev/null && [ "$i" -lt 400 ]; do sleep 0.3; i=$((i+1)); done', 'while [ "$PID" -gt 0 ] 2>/dev/null && kill -0 "$PID" 2>/dev/null && [ "$i" -lt 400 ]; do sleep 0.3; i=$((i+1)); done',
'kill -0 "$PID" 2>/dev/null && say "WARN: app still alive after wait window" || say "app exited after ${i} polls"',
'rm -rf "$NEW"', 'rm -rf "$NEW"',
'mkdir -p "$NEW"', 'mkdir -p "$NEW" || { say "FAIL: mkdir $NEW"; "$RELAUNCH" >/dev/null 2>&1 & exit 1; }',
'if tar -xzf "$TARBALL" -C "$NEW"; then', 'if tar -xzf "$TARBALL" -C "$NEW"; then',
' # electron-builder nests everything under a top-level dir; flatten it.', ' say "extracted ok"',
' # electron-builder nests everything under a top-level dir; flatten it',
" # by adopting the nested dir itself (mv of the dir, not a glob — globs",
" # miss dotfiles and a partial mv leaves a broken tree).",
' if [ ! -e "$NEW/folio" ]; then', ' if [ ! -e "$NEW/folio" ]; then',
' ONLY=$(find "$NEW" -mindepth 1 -maxdepth 1 -type d | head -n 1)', ' ONLY=$(find "$NEW" -mindepth 1 -maxdepth 1 -type d | head -n 1)',
' [ -n "$ONLY" ] && mv "$ONLY"/* "$NEW"/ 2>/dev/null', ' REST=$(find "$NEW" -mindepth 1 -maxdepth 1 | wc -l)',
' if [ -n "$ONLY" ] && [ "$REST" -eq 1 ]; then',
' mv "$ONLY" "$NEW.new" && rm -rf "$NEW" && mv "$NEW.new" "$NEW" && say "flattened nested dir"',
" fi",
" fi", " fi",
"else",
' say "FAIL: tar extract failed"',
"fi", "fi",
'if [ -x "$NEW/folio" ]; then', 'if [ -x "$NEW/folio" ]; then',
' [ -e "$INSTALL" ] && mv "$INSTALL" "$OLD"', ' [ -e "$INSTALL" ] && mv "$INSTALL" "$OLD" && say "moved old install aside"',
' if mv "$NEW" "$INSTALL"; then', ' if mv "$NEW" "$INSTALL"; then',
' say "SWAP OK — new version installed"',
' rm -rf "$OLD" "$INSTALL.update-new".* "$INSTALL.old".* 2>/dev/null', ' rm -rf "$OLD" "$INSTALL.update-new".* "$INSTALL.old".* 2>/dev/null',
' rm -f "$TARBALL"', ' rm -f "$TARBALL"',
" else", " else",
' say "FAIL: mv NEW->INSTALL; restoring old"',
' [ -e "$OLD" ] && mv "$OLD" "$INSTALL"', ' [ -e "$OLD" ] && mv "$OLD" "$INSTALL"',
" fi", " fi",
"else", "else",
' say "FAIL: no executable at $NEW/folio; keeping old install"',
' rm -rf "$NEW"', ' rm -rf "$NEW"',
"fi", "fi",
'say "relaunching"',
'"$RELAUNCH" >/dev/null 2>&1 &', '"$RELAUNCH" >/dev/null 2>&1 &',
"", "",
].join("\n"); ].join("\n");