All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Added support for silent installation mode. - Implemented detailed progress logging during installation phases, including status updates and percentage completion. - Refactored the handling of previous installations to ensure smoother updates and in-app upgrade processes. - Updated error handling for installation failures, providing clearer feedback to users.
261 lines
7.1 KiB
JavaScript
261 lines
7.1 KiB
JavaScript
import { createWriteStream, promises as fs } from "node:fs";
|
|
import http from "node:http";
|
|
import https from "node:https";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { Utils } from "electrobun/bun";
|
|
import { launchMacInstaller } from "./macappupdate.js";
|
|
import { launchWindowsInstaller } from "./winappupdate.js";
|
|
import { scheduleAppRestart } from "./updater-runner.js";
|
|
|
|
const SUPPORTED_TARGETS = {
|
|
darwin: {
|
|
extension: ".pkg",
|
|
osMatchers: ["darwin", "mac", "macos", "osx"],
|
|
},
|
|
win32: {
|
|
extension: ".exe",
|
|
osMatchers: ["win32", "win", "windows"],
|
|
},
|
|
};
|
|
|
|
let runningUpdate = null;
|
|
|
|
const getArtifactName = (artifact) =>
|
|
String(artifact?.fileName || artifact?.relativePath || artifact?.url || "");
|
|
|
|
const normalizeArch = (arch) => {
|
|
if (arch === "x64" || arch === "amd64") return "x64";
|
|
if (arch === "arm64" || arch === "aarch64") return "arm64";
|
|
return arch;
|
|
};
|
|
|
|
const artifactMatchesPlatform = (artifact, target, platform, arch) => {
|
|
const name = getArtifactName(artifact).toLowerCase();
|
|
const normalizedArch = normalizeArch(arch);
|
|
const artifactArch = normalizeArch(String(artifact?.arch || "").toLowerCase());
|
|
const artifactPlatform = String(
|
|
artifact?.platform || artifact?.os || artifact?.target || "",
|
|
).toLowerCase();
|
|
|
|
if (!name.endsWith(target.extension)) return false;
|
|
if (!artifact?.url) return false;
|
|
|
|
const matchesArch =
|
|
artifactArch === normalizedArch ||
|
|
name.includes(`-${normalizedArch}`) ||
|
|
name.includes(`_${normalizedArch}`) ||
|
|
name.includes(`.${normalizedArch}.`) ||
|
|
name.includes(normalizedArch);
|
|
|
|
const matchesOs =
|
|
!artifactPlatform ||
|
|
target.osMatchers.includes(artifactPlatform) ||
|
|
target.osMatchers.some((matcher) => name.includes(matcher)) ||
|
|
(platform === "darwin" && name.includes("mac")) ||
|
|
(platform === "win32" && name.includes("win"));
|
|
|
|
return matchesArch && matchesOs;
|
|
};
|
|
|
|
const selectUpdateArtifact = (
|
|
update,
|
|
platform = process.platform,
|
|
arch = process.arch,
|
|
) => {
|
|
const target = SUPPORTED_TARGETS[platform];
|
|
if (!target) {
|
|
throw new Error(`App updates are not supported on ${platform}.`);
|
|
}
|
|
|
|
const artifacts = Array.isArray(update?.artifacts) ? update.artifacts : [];
|
|
const matchingArtifact = artifacts.find((artifact) =>
|
|
artifactMatchesPlatform(artifact, target, platform, arch),
|
|
);
|
|
const fallbackArtifact = artifacts.find((artifact) => {
|
|
const name = getArtifactName(artifact).toLowerCase();
|
|
return artifact?.url && name.endsWith(target.extension);
|
|
});
|
|
|
|
if (!matchingArtifact && !fallbackArtifact) {
|
|
throw new Error(
|
|
`No ${target.extension} update artifact found for ${platform}/${arch}.`,
|
|
);
|
|
}
|
|
|
|
return matchingArtifact || fallbackArtifact;
|
|
};
|
|
|
|
const getInstallErrorMessage = (error, output = "") => {
|
|
const combined = `${output}\n${error?.message || ""}`.trim();
|
|
|
|
if (
|
|
/cancel/i.test(combined) ||
|
|
/did not grant permission/i.test(combined) ||
|
|
/user canceled/i.test(combined)
|
|
) {
|
|
return "Update installation was cancelled.";
|
|
}
|
|
|
|
if (/incorrect/i.test(combined)) {
|
|
return "The administrator password was incorrect.";
|
|
}
|
|
|
|
if (
|
|
/1625/.test(combined) ||
|
|
/forbidden by system policy/i.test(combined) ||
|
|
/Non-assigned apps are disabled/i.test(combined)
|
|
) {
|
|
return "Update installation was blocked by system policy.";
|
|
}
|
|
|
|
return combined || "Failed to install update.";
|
|
};
|
|
|
|
const getDownloadUrl = (url, redirectCount = 0) =>
|
|
new Promise((resolve, reject) => {
|
|
if (redirectCount > 5) {
|
|
reject(new Error("Too many redirects while downloading update."));
|
|
return;
|
|
}
|
|
|
|
const parsedUrl = new URL(url);
|
|
const client = parsedUrl.protocol === "https:" ? https : http;
|
|
const request = client.get(parsedUrl, (response) => {
|
|
const location = response.headers.location;
|
|
|
|
if (response.statusCode >= 300 && response.statusCode < 400 && location) {
|
|
response.resume();
|
|
resolve(
|
|
getDownloadUrl(
|
|
new URL(location, parsedUrl).toString(),
|
|
redirectCount + 1,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
resolve({ response, url: parsedUrl.toString() });
|
|
});
|
|
|
|
request.on("error", reject);
|
|
});
|
|
|
|
const downloadArtifact = async (artifact, destinationPath, sendProgress) => {
|
|
const { response } = await getDownloadUrl(artifact.url);
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
response.resume();
|
|
throw new Error(`Update download failed with HTTP ${response.statusCode}.`);
|
|
}
|
|
|
|
const totalBytes =
|
|
Number.parseInt(response.headers["content-length"], 10) || 0;
|
|
let downloadedBytes = 0;
|
|
|
|
await new Promise((resolve, reject) => {
|
|
const output = createWriteStream(destinationPath);
|
|
|
|
response.on("data", (chunk) => {
|
|
downloadedBytes += chunk.length;
|
|
const percent = totalBytes
|
|
? Math.round((downloadedBytes / totalBytes) * 100)
|
|
: null;
|
|
|
|
sendProgress({
|
|
phase: "downloading",
|
|
percent,
|
|
downloadedBytes,
|
|
totalBytes,
|
|
message: totalBytes
|
|
? `Downloading update (${percent}%)`
|
|
: "Downloading update",
|
|
});
|
|
});
|
|
|
|
response.on("error", reject);
|
|
output.on("error", reject);
|
|
output.on("finish", resolve);
|
|
response.pipe(output);
|
|
});
|
|
};
|
|
|
|
const launchInstallerAndRestart = async (
|
|
mainWindow,
|
|
installerPath,
|
|
sendProgress,
|
|
) => {
|
|
const installerHelpers = { sendProgress, getInstallErrorMessage };
|
|
|
|
if (process.platform === "darwin") {
|
|
await launchMacInstaller(
|
|
mainWindow,
|
|
installerPath,
|
|
sendProgress,
|
|
installerHelpers,
|
|
);
|
|
} else if (process.platform === "win32") {
|
|
await launchWindowsInstaller(
|
|
mainWindow,
|
|
installerPath,
|
|
sendProgress,
|
|
installerHelpers,
|
|
);
|
|
} else {
|
|
throw new Error(`App updates are not supported on ${process.platform}.`);
|
|
}
|
|
|
|
scheduleAppRestart();
|
|
Utils.quit();
|
|
};
|
|
|
|
const runAppUpdate = async (mainWindow, update, sendProgress) => {
|
|
const artifact = selectUpdateArtifact(update);
|
|
const tempDirectory = await fs.mkdtemp(
|
|
path.join(os.tmpdir(), "farmcontrol-update-"),
|
|
);
|
|
const artifactName = path.basename(getArtifactName(artifact));
|
|
const installerPath = path.join(tempDirectory, artifactName);
|
|
|
|
sendProgress({
|
|
phase: "preparing",
|
|
percent: 0,
|
|
artifact,
|
|
message: "Preparing update download",
|
|
});
|
|
|
|
await downloadArtifact(artifact, installerPath, sendProgress);
|
|
|
|
sendProgress({
|
|
phase: "downloaded",
|
|
percent: 100,
|
|
downloadedBytes: null,
|
|
totalBytes: null,
|
|
artifact,
|
|
message: "Update downloaded",
|
|
});
|
|
|
|
await launchInstallerAndRestart(mainWindow, installerPath, sendProgress);
|
|
};
|
|
|
|
export function startAppUpdate(mainWindow, update, sendProgress) {
|
|
if (runningUpdate) return runningUpdate;
|
|
|
|
runningUpdate = runAppUpdate(mainWindow, update, sendProgress)
|
|
.then(() => ({ ok: true }))
|
|
.catch((error) => {
|
|
sendProgress({
|
|
phase: "error",
|
|
percent: null,
|
|
message: error?.message || "Failed to update app.",
|
|
});
|
|
throw error;
|
|
})
|
|
.finally(() => {
|
|
runningUpdate = null;
|
|
});
|
|
|
|
return runningUpdate;
|
|
}
|