Enhance Windows installer update process with improved timing and error handling
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Increased sleep durations in the `restartFarmControlAfterUpdate` function to ensure smoother application restarts during updates. - Refactored the `startWindowsInstallerProgressWatch` function to reduce polling interval for better responsiveness. - Cleaned up code formatting for improved readability and consistency across the `appupdate.js` and `winappupdate.js` files.
This commit is contained in:
parent
6bf8648e1f
commit
67511e8b8c
@ -311,21 +311,22 @@ Function restartFarmControlAfterUpdate
|
|||||||
!insertmacro progressSuccess
|
!insertmacro progressSuccess
|
||||||
|
|
||||||
!insertmacro progressStatus "Waiting for Farm Control to close..."
|
!insertmacro progressStatus "Waiting for Farm Control to close..."
|
||||||
Sleep 1500
|
Sleep 2000
|
||||||
|
|
||||||
restart_wait_loop:
|
restart_wait_loop:
|
||||||
ExecWait 'cmd.exe /c tasklist /FI "IMAGENAME eq FarmControl.exe" 2>nul | find /I "FarmControl.exe"' $R0
|
ExecWait 'cmd.exe /c tasklist /FI "IMAGENAME eq FarmControl.exe" 2>nul | find /I "FarmControl.exe"' $R0
|
||||||
${If} $R0 == 0
|
${If} $R0 == 0
|
||||||
Sleep 1000
|
Sleep 1500
|
||||||
Goto restart_wait_loop
|
Goto restart_wait_loop
|
||||||
${EndIf}
|
${EndIf}
|
||||||
ExecWait 'cmd.exe /c tasklist /FI "IMAGENAME eq launcher.exe" 2>nul | find /I "launcher.exe"' $R0
|
ExecWait 'cmd.exe /c tasklist /FI "IMAGENAME eq launcher.exe" 2>nul | find /I "launcher.exe"' $R0
|
||||||
${If} $R0 == 0
|
${If} $R0 == 0
|
||||||
Sleep 1000
|
Sleep 1500
|
||||||
Goto restart_wait_loop
|
Goto restart_wait_loop
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
${If} $IsInAppUpdate == "1"
|
${If} $IsInAppUpdate == "1"
|
||||||
|
Sleep 1000
|
||||||
!insertmacro swapUpdateStaging
|
!insertmacro swapUpdateStaging
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
|
|||||||
@ -1,137 +1,136 @@
|
|||||||
import { createWriteStream, promises as fs } from "node:fs";
|
import { createWriteStream, promises as fs } from 'node:fs'
|
||||||
import http from "node:http";
|
import http from 'node:http'
|
||||||
import https from "node:https";
|
import https from 'node:https'
|
||||||
import os from "node:os";
|
import os from 'node:os'
|
||||||
import path from "node:path";
|
import path from 'node:path'
|
||||||
import process from "node:process";
|
import process from 'node:process'
|
||||||
import { Utils } from "electrobun/bun";
|
import { Utils } from 'electrobun/bun'
|
||||||
import { launchMacInstaller } from "./macappupdate.js";
|
import { launchMacInstaller } from './macappupdate.js'
|
||||||
import { launchWindowsInstaller } from "./winappupdate.js";
|
import { launchWindowsInstaller } from './winappupdate.js'
|
||||||
import { checkForDuplicateInstallations } from "./check-duplicate-installations.js";
|
import { checkForDuplicateInstallations } from './check-duplicate-installations.js'
|
||||||
import { scheduleAppRestart } from "./updater-runner.js";
|
import { scheduleAppRestart } from './updater-runner.js'
|
||||||
import { getAppSettings, setAppSettings } from "./store.js";
|
import { getAppSettings, setAppSettings } from './store.js'
|
||||||
|
|
||||||
const SUPPORTED_TARGETS = {
|
const SUPPORTED_TARGETS = {
|
||||||
darwin: {
|
darwin: {
|
||||||
extension: ".pkg",
|
extension: '.pkg',
|
||||||
osMatchers: ["darwin", "mac", "macos", "osx"],
|
osMatchers: ['darwin', 'mac', 'macos', 'osx']
|
||||||
},
|
},
|
||||||
win32: {
|
win32: {
|
||||||
extension: ".exe",
|
extension: '.exe',
|
||||||
osMatchers: ["win32", "win", "windows"],
|
osMatchers: ['win32', 'win', 'windows']
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const DEFAULT_UPDATE_ENGINE = "native";
|
const DEFAULT_UPDATE_ENGINE = 'native'
|
||||||
|
|
||||||
let runningUpdate = null;
|
let runningUpdate = null
|
||||||
|
|
||||||
const getArtifactName = (artifact) =>
|
const getArtifactName = (artifact) =>
|
||||||
String(artifact?.fileName || artifact?.relativePath || artifact?.url || "");
|
String(artifact?.fileName || artifact?.relativePath || artifact?.url || '')
|
||||||
|
|
||||||
const normalizeArch = (arch) => {
|
const normalizeArch = (arch) => {
|
||||||
if (arch === "x64" || arch === "amd64") return "x64";
|
if (arch === 'x64' || arch === 'amd64') return 'x64'
|
||||||
if (arch === "arm64" || arch === "aarch64") return "arm64";
|
if (arch === 'arm64' || arch === 'aarch64') return 'arm64'
|
||||||
return arch;
|
return arch
|
||||||
};
|
}
|
||||||
|
|
||||||
const normalizeEngine = (engine) => {
|
const normalizeEngine = (engine) => {
|
||||||
const value = String(engine || "")
|
const value = String(engine || '')
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase();
|
.toLowerCase()
|
||||||
|
|
||||||
if (value === "chromium" || value === "cef") return "chromium";
|
if (value === 'chromium' || value === 'cef') return 'chromium'
|
||||||
if (value === "native") return "native";
|
if (value === 'native') return 'native'
|
||||||
return null;
|
return null
|
||||||
};
|
}
|
||||||
|
|
||||||
const artifactIsChromium = (artifact) => {
|
const artifactIsChromium = (artifact) => {
|
||||||
const explicit = String(
|
const explicit = String(
|
||||||
artifact?.engine || artifact?.renderer || "",
|
artifact?.engine || artifact?.renderer || ''
|
||||||
).toLowerCase();
|
).toLowerCase()
|
||||||
|
|
||||||
if (explicit === "cef" || explicit === "chromium") return true;
|
if (explicit === 'cef' || explicit === 'chromium') return true
|
||||||
if (explicit === "native") return false;
|
if (explicit === 'native') return false
|
||||||
|
|
||||||
const name = getArtifactName(artifact).toLowerCase();
|
const name = getArtifactName(artifact).toLowerCase()
|
||||||
return /[-_.]cef(?:[-_.]|$)/.test(name);
|
return /[-_.]cef(?:[-_.]|$)/.test(name)
|
||||||
};
|
}
|
||||||
|
|
||||||
const artifactMatchesEngine = (artifact, engine) => {
|
const artifactMatchesEngine = (artifact, engine) => {
|
||||||
const wantsChromium = normalizeEngine(engine) === "chromium";
|
const wantsChromium = normalizeEngine(engine) === 'chromium'
|
||||||
return artifactIsChromium(artifact) === wantsChromium;
|
return artifactIsChromium(artifact) === wantsChromium
|
||||||
};
|
}
|
||||||
|
|
||||||
const artifactMatchesPlatform = (artifact, target, platform, arch) => {
|
const artifactMatchesPlatform = (artifact, target, platform, arch) => {
|
||||||
const name = getArtifactName(artifact).toLowerCase();
|
const name = getArtifactName(artifact).toLowerCase()
|
||||||
const normalizedArch = normalizeArch(arch);
|
const normalizedArch = normalizeArch(arch)
|
||||||
const artifactArch = normalizeArch(String(artifact?.arch || "").toLowerCase());
|
const artifactArch = normalizeArch(String(artifact?.arch || '').toLowerCase())
|
||||||
const artifactPlatform = String(
|
const artifactPlatform = String(
|
||||||
artifact?.platform || artifact?.os || artifact?.target || "",
|
artifact?.platform || artifact?.os || artifact?.target || ''
|
||||||
).toLowerCase();
|
).toLowerCase()
|
||||||
|
|
||||||
if (!name.endsWith(target.extension)) return false;
|
if (!name.endsWith(target.extension)) return false
|
||||||
if (!artifact?.url) return false;
|
if (!artifact?.url) return false
|
||||||
|
|
||||||
const matchesArch =
|
const matchesArch =
|
||||||
artifactArch === normalizedArch ||
|
artifactArch === normalizedArch ||
|
||||||
name.includes(`-${normalizedArch}`) ||
|
name.includes(`-${normalizedArch}`) ||
|
||||||
name.includes(`_${normalizedArch}`) ||
|
name.includes(`_${normalizedArch}`) ||
|
||||||
name.includes(`.${normalizedArch}.`) ||
|
name.includes(`.${normalizedArch}.`) ||
|
||||||
name.includes(normalizedArch);
|
name.includes(normalizedArch)
|
||||||
|
|
||||||
const matchesOs =
|
const matchesOs =
|
||||||
!artifactPlatform ||
|
!artifactPlatform ||
|
||||||
target.osMatchers.includes(artifactPlatform) ||
|
target.osMatchers.includes(artifactPlatform) ||
|
||||||
target.osMatchers.some((matcher) => name.includes(matcher)) ||
|
target.osMatchers.some((matcher) => name.includes(matcher)) ||
|
||||||
(platform === "darwin" && name.includes("mac")) ||
|
(platform === 'darwin' && name.includes('mac')) ||
|
||||||
(platform === "win32" && name.includes("win"));
|
(platform === 'win32' && name.includes('win'))
|
||||||
|
|
||||||
return matchesArch && matchesOs;
|
return matchesArch && matchesOs
|
||||||
};
|
}
|
||||||
|
|
||||||
const selectUpdateArtifact = (
|
const selectUpdateArtifact = (
|
||||||
update,
|
update,
|
||||||
platform = process.platform,
|
platform = process.platform,
|
||||||
arch = process.arch,
|
arch = process.arch
|
||||||
) => {
|
) => {
|
||||||
const target = SUPPORTED_TARGETS[platform];
|
const target = SUPPORTED_TARGETS[platform]
|
||||||
if (!target) {
|
if (!target) {
|
||||||
throw new Error(`App updates are not supported on ${platform}.`);
|
throw new Error(`App updates are not supported on ${platform}.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const engine =
|
const engine = normalizeEngine(update?.engine) || DEFAULT_UPDATE_ENGINE
|
||||||
normalizeEngine(update?.engine) || DEFAULT_UPDATE_ENGINE;
|
const artifacts = Array.isArray(update?.artifacts) ? update.artifacts : []
|
||||||
const artifacts = Array.isArray(update?.artifacts) ? update.artifacts : [];
|
|
||||||
const matchingArtifact = artifacts.find(
|
const matchingArtifact = artifacts.find(
|
||||||
(artifact) =>
|
(artifact) =>
|
||||||
artifactMatchesPlatform(artifact, target, platform, arch) &&
|
artifactMatchesPlatform(artifact, target, platform, arch) &&
|
||||||
artifactMatchesEngine(artifact, engine),
|
artifactMatchesEngine(artifact, engine)
|
||||||
);
|
)
|
||||||
|
|
||||||
if (!matchingArtifact) {
|
if (!matchingArtifact) {
|
||||||
const engineLabel = engine === "chromium" ? "Chromium (cef)" : "Native";
|
const engineLabel = engine === 'chromium' ? 'Chromium (cef)' : 'Native'
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No ${target.extension} ${engineLabel} update artifact found for ${platform}/${arch}.`,
|
`No ${target.extension} ${engineLabel} update artifact found for ${platform}/${arch}.`
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return matchingArtifact;
|
return matchingArtifact
|
||||||
};
|
}
|
||||||
|
|
||||||
const getInstallErrorMessage = (error, output = "") => {
|
const getInstallErrorMessage = (error, output = '') => {
|
||||||
const combined = `${output}\n${error?.message || ""}`.trim();
|
const combined = `${output}\n${error?.message || ''}`.trim()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
/cancel/i.test(combined) ||
|
/cancel/i.test(combined) ||
|
||||||
/did not grant permission/i.test(combined) ||
|
/did not grant permission/i.test(combined) ||
|
||||||
/user canceled/i.test(combined)
|
/user canceled/i.test(combined)
|
||||||
) {
|
) {
|
||||||
return "Update installation was cancelled.";
|
return 'Update installation was cancelled.'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/incorrect/i.test(combined)) {
|
if (/incorrect/i.test(combined)) {
|
||||||
return "The administrator password was incorrect.";
|
return 'The administrator password was incorrect.'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -139,124 +138,124 @@ const getInstallErrorMessage = (error, output = "") => {
|
|||||||
/forbidden by system policy/i.test(combined) ||
|
/forbidden by system policy/i.test(combined) ||
|
||||||
/Non-assigned apps are disabled/i.test(combined)
|
/Non-assigned apps are disabled/i.test(combined)
|
||||||
) {
|
) {
|
||||||
return "Update installation was blocked by system policy.";
|
return 'Update installation was blocked by system policy.'
|
||||||
}
|
}
|
||||||
|
|
||||||
return combined || "Failed to install update.";
|
return combined || 'Failed to install update.'
|
||||||
};
|
}
|
||||||
|
|
||||||
const getDownloadUrl = (url, redirectCount = 0) =>
|
const getDownloadUrl = (url, redirectCount = 0) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
if (redirectCount > 5) {
|
if (redirectCount > 5) {
|
||||||
reject(new Error("Too many redirects while downloading update."));
|
reject(new Error('Too many redirects while downloading update.'))
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url)
|
||||||
const client = parsedUrl.protocol === "https:" ? https : http;
|
const client = parsedUrl.protocol === 'https:' ? https : http
|
||||||
const request = client.get(parsedUrl, (response) => {
|
const request = client.get(parsedUrl, (response) => {
|
||||||
const location = response.headers.location;
|
const location = response.headers.location
|
||||||
|
|
||||||
if (response.statusCode >= 300 && response.statusCode < 400 && location) {
|
if (response.statusCode >= 300 && response.statusCode < 400 && location) {
|
||||||
response.resume();
|
response.resume()
|
||||||
resolve(
|
resolve(
|
||||||
getDownloadUrl(
|
getDownloadUrl(
|
||||||
new URL(location, parsedUrl).toString(),
|
new URL(location, parsedUrl).toString(),
|
||||||
redirectCount + 1,
|
redirectCount + 1
|
||||||
),
|
)
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve({ response, url: parsedUrl.toString() });
|
resolve({ response, url: parsedUrl.toString() })
|
||||||
});
|
})
|
||||||
|
|
||||||
request.on("error", reject);
|
request.on('error', reject)
|
||||||
});
|
})
|
||||||
|
|
||||||
const downloadArtifact = async (artifact, destinationPath, sendProgress) => {
|
const downloadArtifact = async (artifact, destinationPath, sendProgress) => {
|
||||||
const { response } = await getDownloadUrl(artifact.url);
|
const { response } = await getDownloadUrl(artifact.url)
|
||||||
|
|
||||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||||
response.resume();
|
response.resume()
|
||||||
throw new Error(`Update download failed with HTTP ${response.statusCode}.`);
|
throw new Error(`Update download failed with HTTP ${response.statusCode}.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalBytes =
|
const totalBytes =
|
||||||
Number.parseInt(response.headers["content-length"], 10) || 0;
|
Number.parseInt(response.headers['content-length'], 10) || 0
|
||||||
let downloadedBytes = 0;
|
let downloadedBytes = 0
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const output = createWriteStream(destinationPath);
|
const output = createWriteStream(destinationPath)
|
||||||
|
|
||||||
response.on("data", (chunk) => {
|
response.on('data', (chunk) => {
|
||||||
downloadedBytes += chunk.length;
|
downloadedBytes += chunk.length
|
||||||
const percent = totalBytes
|
const percent = totalBytes
|
||||||
? Math.round((downloadedBytes / totalBytes) * 100)
|
? Math.round((downloadedBytes / totalBytes) * 100)
|
||||||
: null;
|
: null
|
||||||
|
|
||||||
sendProgress({
|
sendProgress({
|
||||||
phase: "downloading",
|
phase: 'downloading',
|
||||||
percent,
|
percent,
|
||||||
downloadedBytes,
|
downloadedBytes,
|
||||||
totalBytes,
|
totalBytes,
|
||||||
message: totalBytes
|
message: totalBytes
|
||||||
? `Downloading update (${percent}%)`
|
? `Downloading update (${percent}%)`
|
||||||
: "Downloading update",
|
: 'Downloading update'
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
response.on("error", reject);
|
response.on('error', reject)
|
||||||
output.on("error", reject);
|
output.on('error', reject)
|
||||||
output.on("finish", resolve);
|
output.on('finish', resolve)
|
||||||
response.pipe(output);
|
response.pipe(output)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const getRunningEngine = (mainWindow) =>
|
const getRunningEngine = (mainWindow) =>
|
||||||
mainWindow?.renderer === "cef" ? "chromium" : "native";
|
mainWindow?.renderer === 'cef' ? 'chromium' : 'native'
|
||||||
|
|
||||||
const getRunningAppState = (mainWindow, settings) => ({
|
const getRunningAppState = (mainWindow, settings) => ({
|
||||||
version: process.env.ELECTROBUN_VERSION || null,
|
version: process.env.ELECTROBUN_VERSION || null,
|
||||||
branch: settings?.appUpdateRunningBranch || null,
|
branch: settings?.appUpdateRunningBranch || null,
|
||||||
engine: getRunningEngine(mainWindow),
|
engine: getRunningEngine(mainWindow)
|
||||||
});
|
})
|
||||||
|
|
||||||
// Snapshot the running version/branch/engine so the next launch can tell
|
// Snapshot the running version/branch/engine so the next launch can tell
|
||||||
// whether an update actually completed.
|
// whether an update actually completed.
|
||||||
const persistCurrentAppState = async (mainWindow) => {
|
const persistCurrentAppState = async (mainWindow) => {
|
||||||
try {
|
try {
|
||||||
const settings = await getAppSettings();
|
const settings = await getAppSettings()
|
||||||
await setAppSettings({
|
await setAppSettings({
|
||||||
...settings,
|
...settings,
|
||||||
current: getRunningAppState(mainWindow, settings),
|
current: getRunningAppState(mainWindow, settings)
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[app-update] Failed to persist current app state.", error);
|
console.warn('[app-update] Failed to persist current app state.', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Ignore missing values: a field only counts as changed when it was recorded
|
// Ignore missing values: a field only counts as changed when it was recorded
|
||||||
// both before and after the update.
|
// both before and after the update.
|
||||||
const stateValueChanged = (previous, next) =>
|
const stateValueChanged = (previous, next) =>
|
||||||
Boolean(previous) && Boolean(next) && previous !== next;
|
Boolean(previous) && Boolean(next) && previous !== next
|
||||||
|
|
||||||
let completedUpdateResult = null;
|
let completedUpdateResult = null
|
||||||
|
|
||||||
export const checkForCompletedUpdate = async (mainWindow) => {
|
export const checkForCompletedUpdate = async (mainWindow) => {
|
||||||
// Cache per process so repeated renderer calls (e.g. remounts) get the same
|
// Cache per process so repeated renderer calls (e.g. remounts) get the same
|
||||||
// answer instead of a false negative after `current` has been rewritten.
|
// answer instead of a false negative after `current` has been rewritten.
|
||||||
if (completedUpdateResult) return completedUpdateResult;
|
if (completedUpdateResult) return completedUpdateResult
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = await getAppSettings();
|
const settings = await getAppSettings()
|
||||||
const previous =
|
const previous =
|
||||||
settings?.current && typeof settings.current === "object"
|
settings?.current && typeof settings.current === 'object'
|
||||||
? settings.current
|
? settings.current
|
||||||
: null;
|
: null
|
||||||
const current = getRunningAppState(mainWindow, settings);
|
const current = getRunningAppState(mainWindow, settings)
|
||||||
|
|
||||||
await setAppSettings({ ...settings, current });
|
await setAppSettings({ ...settings, current })
|
||||||
|
|
||||||
const updated =
|
const updated =
|
||||||
Boolean(previous) &&
|
Boolean(previous) &&
|
||||||
@ -264,137 +263,135 @@ export const checkForCompletedUpdate = async (mainWindow) => {
|
|||||||
stateValueChanged(previous.branch, current.branch) ||
|
stateValueChanged(previous.branch, current.branch) ||
|
||||||
stateValueChanged(
|
stateValueChanged(
|
||||||
normalizeEngine(previous.engine),
|
normalizeEngine(previous.engine),
|
||||||
normalizeEngine(current.engine),
|
normalizeEngine(current.engine)
|
||||||
));
|
))
|
||||||
|
|
||||||
const duplicates = checkForDuplicateInstallations();
|
const duplicates = checkForDuplicateInstallations()
|
||||||
|
|
||||||
completedUpdateResult = { updated, previous, current, duplicates };
|
completedUpdateResult = { updated, previous, current, duplicates }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[app-update] Failed to check for a completed update.", error);
|
console.warn('[app-update] Failed to check for a completed update.', error)
|
||||||
completedUpdateResult = {
|
completedUpdateResult = {
|
||||||
updated: false,
|
updated: false,
|
||||||
previous: null,
|
previous: null,
|
||||||
current: null,
|
current: null,
|
||||||
duplicates: checkForDuplicateInstallations(),
|
duplicates: checkForDuplicateInstallations()
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return completedUpdateResult;
|
return completedUpdateResult
|
||||||
};
|
}
|
||||||
|
|
||||||
const persistInstalledUpdateSettings = async (update) => {
|
const persistInstalledUpdateSettings = async (update) => {
|
||||||
try {
|
try {
|
||||||
const settings = await getAppSettings();
|
const settings = await getAppSettings()
|
||||||
const engine =
|
const engine =
|
||||||
normalizeEngine(update?.engine) ||
|
normalizeEngine(update?.engine) ||
|
||||||
normalizeEngine(settings?.appUpdateEngine) ||
|
normalizeEngine(settings?.appUpdateEngine) ||
|
||||||
DEFAULT_UPDATE_ENGINE;
|
DEFAULT_UPDATE_ENGINE
|
||||||
|
|
||||||
await setAppSettings({
|
await setAppSettings({
|
||||||
...settings,
|
...settings,
|
||||||
appUpdateEngine: engine,
|
appUpdateEngine: engine,
|
||||||
...(update?.branch
|
...(update?.branch ? { appUpdateRunningBranch: update.branch } : {})
|
||||||
? { appUpdateRunningBranch: update.branch }
|
})
|
||||||
: {}),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[app-update] Failed to persist installed update settings.",
|
'[app-update] Failed to persist installed update settings.',
|
||||||
error,
|
error
|
||||||
);
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const launchInstallerAndRestart = async (
|
const launchInstallerAndRestart = async (
|
||||||
mainWindow,
|
mainWindow,
|
||||||
installerPath,
|
installerPath,
|
||||||
sendProgress,
|
sendProgress,
|
||||||
update,
|
update
|
||||||
) => {
|
) => {
|
||||||
const installerHelpers = { sendProgress, getInstallErrorMessage };
|
const installerHelpers = { sendProgress, getInstallErrorMessage }
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === 'darwin') {
|
||||||
await launchMacInstaller(
|
await launchMacInstaller(
|
||||||
mainWindow,
|
mainWindow,
|
||||||
installerPath,
|
installerPath,
|
||||||
sendProgress,
|
sendProgress,
|
||||||
installerHelpers,
|
installerHelpers
|
||||||
);
|
)
|
||||||
} else if (process.platform === "win32") {
|
} else if (process.platform === 'win32') {
|
||||||
await launchWindowsInstaller(
|
await launchWindowsInstaller(
|
||||||
mainWindow,
|
mainWindow,
|
||||||
installerPath,
|
installerPath,
|
||||||
sendProgress,
|
sendProgress,
|
||||||
installerHelpers,
|
installerHelpers
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`App updates are not supported on ${process.platform}.`);
|
throw new Error(`App updates are not supported on ${process.platform}.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
await persistInstalledUpdateSettings(update);
|
await persistInstalledUpdateSettings(update)
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === 'darwin') {
|
||||||
scheduleAppRestart();
|
scheduleAppRestart()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give the UI a moment to show completion before the app exits.
|
// Give the UI a moment to show completion before the app exits.
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
Utils.quit();
|
Utils.quit()
|
||||||
};
|
}
|
||||||
|
|
||||||
const runAppUpdate = async (mainWindow, update, sendProgress) => {
|
const runAppUpdate = async (mainWindow, update, sendProgress) => {
|
||||||
await persistCurrentAppState(mainWindow);
|
await persistCurrentAppState(mainWindow)
|
||||||
|
|
||||||
const artifact = selectUpdateArtifact(update);
|
const artifact = selectUpdateArtifact(update)
|
||||||
const tempDirectory = await fs.mkdtemp(
|
const tempDirectory = await fs.mkdtemp(
|
||||||
path.join(os.tmpdir(), "farmcontrol-update-"),
|
path.join(os.tmpdir(), 'farmcontrol-update-')
|
||||||
);
|
)
|
||||||
const artifactName = path.basename(getArtifactName(artifact));
|
const artifactName = path.basename(getArtifactName(artifact))
|
||||||
const installerPath = path.join(tempDirectory, artifactName);
|
const installerPath = path.join(tempDirectory, artifactName)
|
||||||
|
|
||||||
sendProgress({
|
sendProgress({
|
||||||
phase: "preparing",
|
phase: 'preparing',
|
||||||
percent: 0,
|
percent: 0,
|
||||||
artifact,
|
artifact,
|
||||||
message: "Preparing update download",
|
message: 'Preparing update download'
|
||||||
});
|
})
|
||||||
|
|
||||||
await downloadArtifact(artifact, installerPath, sendProgress);
|
await downloadArtifact(artifact, installerPath, sendProgress)
|
||||||
|
|
||||||
sendProgress({
|
sendProgress({
|
||||||
phase: "downloaded",
|
phase: 'downloaded',
|
||||||
percent: 100,
|
percent: 100,
|
||||||
downloadedBytes: null,
|
downloadedBytes: null,
|
||||||
totalBytes: null,
|
totalBytes: null,
|
||||||
artifact,
|
artifact,
|
||||||
message: "Update downloaded",
|
message: 'Update downloaded'
|
||||||
});
|
})
|
||||||
|
|
||||||
await launchInstallerAndRestart(
|
await launchInstallerAndRestart(
|
||||||
mainWindow,
|
mainWindow,
|
||||||
installerPath,
|
installerPath,
|
||||||
sendProgress,
|
sendProgress,
|
||||||
update,
|
update
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export function startAppUpdate(mainWindow, update, sendProgress) {
|
export function startAppUpdate(mainWindow, update, sendProgress) {
|
||||||
if (runningUpdate) return runningUpdate;
|
if (runningUpdate) return runningUpdate
|
||||||
|
|
||||||
runningUpdate = runAppUpdate(mainWindow, update, sendProgress)
|
runningUpdate = runAppUpdate(mainWindow, update, sendProgress)
|
||||||
.then(() => ({ ok: true }))
|
.then(() => ({ ok: true }))
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
sendProgress({
|
sendProgress({
|
||||||
phase: "error",
|
phase: 'error',
|
||||||
percent: null,
|
percent: null,
|
||||||
message: error?.message || "Failed to update app.",
|
message: error?.message || 'Failed to update app.'
|
||||||
});
|
})
|
||||||
throw error;
|
throw error
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
runningUpdate = null;
|
runningUpdate = null
|
||||||
});
|
})
|
||||||
|
|
||||||
return runningUpdate;
|
return runningUpdate
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,7 +107,8 @@ const startWindowsInstallerProgressWatch = (
|
|||||||
offset = stat.size
|
offset = stat.size
|
||||||
installerOutput += buffer.toString('utf8')
|
installerOutput += buffer.toString('utf8')
|
||||||
|
|
||||||
const { percent, message } = parseWindowsInstallerProgress(installerOutput)
|
const { percent, message } =
|
||||||
|
parseWindowsInstallerProgress(installerOutput)
|
||||||
const resolvedMessage = message || 'Installing update...'
|
const resolvedMessage = message || 'Installing update...'
|
||||||
|
|
||||||
if (percent !== lastPercent || resolvedMessage !== lastMessage) {
|
if (percent !== lastPercent || resolvedMessage !== lastMessage) {
|
||||||
@ -142,7 +143,7 @@ const startWindowsInstallerProgressWatch = (
|
|||||||
poll().catch((error) => {
|
poll().catch((error) => {
|
||||||
console.error('[app-update] installer log poll error:', error)
|
console.error('[app-update] installer log poll error:', error)
|
||||||
})
|
})
|
||||||
}, 300)
|
}, 200)
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
clearInterval(intervalId)
|
clearInterval(intervalId)
|
||||||
@ -223,12 +224,7 @@ export const launchWindowsInstaller = async (
|
|||||||
// /UPDATE = in-app update (stage into Farm Control.new; swap after exit)
|
// /UPDATE = in-app update (stage into Farm Control.new; swap after exit)
|
||||||
// /RESTARTFC = installer waits for this process to exit, swaps folders, relaunches
|
// /RESTARTFC = installer waits for this process to exit, swaps folders, relaunches
|
||||||
// /LOG= + FARMCONTROL_INSTALL_LOG = progress log (installer:% / PHASE / STATUS)
|
// /LOG= + FARMCONTROL_INSTALL_LOG = progress log (installer:% / PHASE / STATUS)
|
||||||
const installerArgs = [
|
const installerArgs = ['/S', '/UPDATE', '/RESTARTFC', `/LOG=${logPath}`]
|
||||||
'/S',
|
|
||||||
'/UPDATE',
|
|
||||||
'/RESTARTFC',
|
|
||||||
`/LOG=${logPath}`
|
|
||||||
]
|
|
||||||
|
|
||||||
const installerProcess = spawn(resolvedPath, installerArgs, {
|
const installerProcess = spawn(resolvedPath, installerArgs, {
|
||||||
detached: true,
|
detached: true,
|
||||||
@ -265,7 +261,10 @@ export const launchWindowsInstaller = async (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isWindowsInstallFailed(output) || !isWindowsInstallSuccessful(output)) {
|
if (
|
||||||
|
isWindowsInstallFailed(output) ||
|
||||||
|
!isWindowsInstallSuccessful(output)
|
||||||
|
) {
|
||||||
settleFailure(getInstallErrorMessage(null, output))
|
settleFailure(getInstallErrorMessage(null, output))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user