All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Changed the installer file extension from `.msi` to `.exe` in both `appupdate.js` and `winappupdate.js` to align with the new installation format. - Refactored the validation function to check for Windows executables instead of MSI packages, enhancing compatibility with the updated installer format.
170 lines
4.2 KiB
JavaScript
170 lines
4.2 KiB
JavaScript
import { spawn } from 'child_process'
|
|
import { promises as fs } from 'fs'
|
|
import os from 'os'
|
|
import path from 'path'
|
|
|
|
const MZ_HEADER = Buffer.from([0x4d, 0x5a])
|
|
const DEBUG_PREFIX = '[app-update][win-progress]'
|
|
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
|
|
const debugLog = () => {}
|
|
|
|
const isValidWindowsExecutable = async (filePath) => {
|
|
const handle = await fs.open(filePath, 'r')
|
|
try {
|
|
const header = Buffer.alloc(MZ_HEADER.length)
|
|
await handle.read(header, 0, header.length, 0)
|
|
return header.equals(MZ_HEADER)
|
|
} finally {
|
|
await handle.close()
|
|
}
|
|
}
|
|
|
|
const prepareInstallerPath = async (installerPath) => {
|
|
const fileName = path.basename(installerPath)
|
|
const updateDir = path.join(
|
|
os.homedir(),
|
|
'AppData',
|
|
'Local',
|
|
'FarmControl',
|
|
'Updates'
|
|
)
|
|
await fs.mkdir(updateDir, { recursive: true })
|
|
|
|
const stablePath = path.join(updateDir, fileName)
|
|
await fs.copyFile(installerPath, stablePath)
|
|
|
|
const resolvedPath = await fs.realpath(stablePath)
|
|
const stats = await fs.stat(resolvedPath)
|
|
|
|
if (!stats.isFile() || stats.size === 0) {
|
|
throw new Error('Update installer file is missing or empty.')
|
|
}
|
|
|
|
if (!(await isValidWindowsExecutable(resolvedPath))) {
|
|
throw new Error(
|
|
'Downloaded update is not a valid Windows installer. The file may be corrupted or incomplete.'
|
|
)
|
|
}
|
|
|
|
return resolvedPath
|
|
}
|
|
|
|
const readInstallerLog = async (logPath) => {
|
|
try {
|
|
return await fs.readFile(logPath, 'utf8')
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
export const launchWindowsInstaller = async (
|
|
app,
|
|
installerPath,
|
|
webContents,
|
|
{ sendProgress, getInstallErrorMessage }
|
|
) => {
|
|
const resolvedPath = await prepareInstallerPath(installerPath)
|
|
const logPath = path.join(path.dirname(resolvedPath), 'install.log')
|
|
|
|
debugLog('prepared installer', {
|
|
installerPath,
|
|
resolvedPath,
|
|
logPath
|
|
})
|
|
|
|
sendProgress(webContents, {
|
|
phase: 'installing',
|
|
percent: 0,
|
|
message: 'Installing update...'
|
|
})
|
|
|
|
await fs.unlink(logPath).catch(() => {})
|
|
|
|
await sleep(2000)
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let processOutput = ''
|
|
const startedAt = Date.now()
|
|
|
|
const installerArgs = ['/S', `/LOG=${logPath}`]
|
|
|
|
debugLog('spawning NSIS installer', {
|
|
installerPath: resolvedPath,
|
|
args: installerArgs,
|
|
elapsedMs: Date.now() - startedAt
|
|
})
|
|
|
|
const installerProcess = spawn(resolvedPath, installerArgs, {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
windowsHide: true
|
|
})
|
|
|
|
installerProcess.stdout?.on('data', (data) => {
|
|
processOutput += data.toString('utf8')
|
|
})
|
|
|
|
installerProcess.stderr?.on('data', (data) => {
|
|
processOutput += data.toString('utf8')
|
|
})
|
|
|
|
installerProcess.on('spawn', () => {
|
|
debugLog('installer spawned', {
|
|
pid: installerProcess.pid,
|
|
elapsedMs: Date.now() - startedAt
|
|
})
|
|
})
|
|
|
|
installerProcess.on('error', (error) => {
|
|
console.error(`${DEBUG_PREFIX} installer spawn error:`, error)
|
|
|
|
const message = error?.message || 'Failed to start update installer.'
|
|
sendProgress(webContents, {
|
|
phase: 'error',
|
|
percent: null,
|
|
message
|
|
})
|
|
reject(error)
|
|
})
|
|
|
|
installerProcess.on('exit', async (code, signal) => {
|
|
const logOutput = await readInstallerLog(logPath)
|
|
const output = [processOutput, logOutput].filter(Boolean).join('\n')
|
|
|
|
debugLog('installer exited', {
|
|
code,
|
|
signal,
|
|
elapsedMs: Date.now() - startedAt,
|
|
logOutputLength: logOutput.length,
|
|
processOutputLength: processOutput.length,
|
|
outputPreview: output.slice(0, 500).replace(/\s+/g, ' ')
|
|
})
|
|
|
|
debugLog('keeping install log', { logPath })
|
|
|
|
if (code !== 0) {
|
|
const message =
|
|
getInstallErrorMessage(null, output) ||
|
|
`Update installer failed with exit code ${code ?? 'unknown'}.`
|
|
sendProgress(webContents, {
|
|
phase: 'error',
|
|
percent: null,
|
|
message
|
|
})
|
|
reject(new Error(message))
|
|
return
|
|
}
|
|
|
|
sendProgress(webContents, {
|
|
phase: 'installing',
|
|
percent: 100,
|
|
message: 'Installation complete. Restarting Farm Control...'
|
|
})
|
|
|
|
debugLog('installer completed successfully')
|
|
resolve()
|
|
})
|
|
})
|
|
}
|