794 lines
29 KiB
TypeScript
794 lines
29 KiB
TypeScript
import * as querystring from "querystring";
|
|
import * as request from "request";
|
|
import {app, dialog} from "electron";
|
|
import * as fs from "fs-extra";
|
|
import * as ofs from "original-fs";
|
|
import * as os from "os";
|
|
import * as tar from "tar-stream";
|
|
import * as path from "path";
|
|
import * as zlib from "zlib";
|
|
import * as child_process from "child_process";
|
|
import * as progress from "request-progress";
|
|
import * as util from "util";
|
|
|
|
import {parseVersion, Version} from "../../shared/version";
|
|
|
|
import MessageBoxOptions = Electron.MessageBoxOptions;
|
|
import {Headers} from "tar-stream";
|
|
import {Arguments, processArguments} from "../../shared/process-arguments";
|
|
import * as electron from "electron";
|
|
import {PassThrough} from "stream";
|
|
import ErrnoException = NodeJS.ErrnoException;
|
|
import { default as validateUpdateConfig } from "./UpdateConfigFile.validator";
|
|
import { default as validateAppInfo } from "./AppInfoFile.validator";
|
|
import UpdateConfigFile from "./UpdateConfigFile";
|
|
import AppInfoFile from "./AppInfoFile";
|
|
|
|
export type UpdateStatsCallback = (message: string, progress: number) => void;
|
|
export type UpdateLogCallback = (type: "error" | "info", message: string) => void;
|
|
|
|
export function updateServerUrl() : string {
|
|
/* FIXME! */
|
|
return "https://clientapi.teaspeak.de/";
|
|
return processArguments.has_value(...Arguments.SERVER_URL) ? processArguments.value(...Arguments.SERVER_URL) : "https://clientapi.teaspeak.de/";
|
|
}
|
|
|
|
export interface UpdateVersion {
|
|
channel: string;
|
|
platform: string,
|
|
arch: string;
|
|
version: Version;
|
|
}
|
|
|
|
export interface UpdateData {
|
|
versions: UpdateVersion[];
|
|
updater_version: UpdateVersion;
|
|
}
|
|
|
|
let remoteVersionCacheTimestamp: number;
|
|
let remoteVersionCache: Promise<UpdateData>;
|
|
export async function fetchRemoteUpdateData() : Promise<UpdateData> {
|
|
if(remoteVersionCache && remoteVersionCacheTimestamp > Date.now() - 60 * 60 * 1000) {
|
|
return remoteVersionCache;
|
|
}
|
|
|
|
/* TODO: Validate remote response schema */
|
|
remoteVersionCacheTimestamp = Date.now();
|
|
return (remoteVersionCache = new Promise<UpdateData>((resolve, reject) => {
|
|
const request_url = updateServerUrl() + "/api.php?" + querystring.stringify({
|
|
type: "update-info"
|
|
});
|
|
console.log("request: %s", request_url);
|
|
request.get(request_url, {
|
|
timeout: 2000
|
|
}, (error, response, body) => {
|
|
if(response.statusCode !== 200) {
|
|
setImmediate(reject, "Invalid status code (" + response.statusCode + (response.statusMessage ? "/" + response.statusMessage : "") + ")");
|
|
return;
|
|
}
|
|
if(!response) {
|
|
setImmediate(reject, "Missing response object");
|
|
return;
|
|
}
|
|
|
|
let data: any;
|
|
try {
|
|
data = JSON.parse(body);
|
|
} catch (_error) {
|
|
setImmediate(reject, "Failed to parse response");
|
|
return;
|
|
}
|
|
|
|
if(!data["success"]) {
|
|
setImmediate(reject, "Action failed (" + (data["msg"] || "unknown error") + ")");
|
|
return;
|
|
}
|
|
|
|
let resp: UpdateData = {} as any;
|
|
resp.versions = [];
|
|
for(const channel of Object.keys(data)) {
|
|
if(channel == "success") continue;
|
|
|
|
for(const entry of data[channel]) {
|
|
let version: UpdateVersion = {} as any;
|
|
version.channel = channel;
|
|
version.arch = entry["arch"];
|
|
version.platform = entry["platform"];
|
|
version.version = new Version(entry["version"]["major"], entry["version"]["minor"], entry["version"]["patch"], entry["version"]["build"], entry["version"]["timestamp"]);
|
|
if(version.channel == 'updater') {
|
|
resp.updater_version = version;
|
|
} else {
|
|
resp.versions.push(version);
|
|
}
|
|
}
|
|
}
|
|
|
|
setImmediate(resolve, resp);
|
|
});
|
|
})).catch(error => {
|
|
/* Don't cache errors */
|
|
remoteVersionCache = undefined;
|
|
remoteVersionCacheTimestamp = undefined;
|
|
return Promise.reject(error);
|
|
});
|
|
}
|
|
|
|
export async function availableRemoteChannels() : Promise<string[]> {
|
|
const versions = (await fetchRemoteUpdateData()).versions.map(e => e.channel);
|
|
|
|
return [...new Set(versions)];
|
|
}
|
|
|
|
export async function newestRemoteClientVersion(channel: string) : Promise<UpdateVersion | undefined> {
|
|
const data = await fetchRemoteUpdateData();
|
|
|
|
let currentVersion: UpdateVersion;
|
|
for(const version of data.versions) {
|
|
if(version.arch == os.arch() && version.platform == os.platform()) {
|
|
if(version.channel == channel) {
|
|
if(!currentVersion || version.version.newerThan(currentVersion.version)) {
|
|
currentVersion = version;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return currentVersion;
|
|
}
|
|
|
|
function getAppDataDirectory() : string {
|
|
return electron.app.getPath('userData');
|
|
}
|
|
|
|
function generateUpdateFilePath(channel: string, version: Version) : string {
|
|
let directory = fs.realpathSync(getAppDataDirectory());
|
|
|
|
const name = channel + "_" + version.major + "_" + version.minor + "_" + version.patch + "_" + version.build + ".tar";
|
|
return path.join(directory, "app_versions", name);
|
|
}
|
|
|
|
export interface ProgressState {
|
|
percent: number, // Overall percent (between 0 to 1)
|
|
speed: number, // The download speed in bytes/sec
|
|
size: {
|
|
total: number, // The total payload size in bytes
|
|
transferred: number// The transferred payload size in bytes
|
|
},
|
|
time: {
|
|
elapsed: number,// The total elapsed seconds since the start (3 decimals)
|
|
remaining: number // The remaining seconds to finish (3 decimals)
|
|
}
|
|
}
|
|
|
|
export async function downloadClientVersion(channel: string, version: Version, status: (state: ProgressState) => any, callbackLog: UpdateLogCallback) : Promise<string> {
|
|
const targetFilePath = generateUpdateFilePath(channel, version);
|
|
|
|
if(fs.existsSync(targetFilePath)) {
|
|
callbackLog("info", "Removing old update file located at " + targetFilePath);
|
|
|
|
/* TODO test if this file is valid and can be used */
|
|
try {
|
|
await fs.remove(targetFilePath);
|
|
} catch(error) {
|
|
throw "Failed to remove old file: " + error;
|
|
}
|
|
}
|
|
|
|
try {
|
|
await fs.mkdirp(path.dirname(targetFilePath));
|
|
} catch(error) {
|
|
throw "Failed to make target directory: " + path.dirname(targetFilePath);
|
|
}
|
|
|
|
const requestUrl = updateServerUrl() + "/api.php?" + querystring.stringify({
|
|
type: "update-download",
|
|
platform: os.platform(),
|
|
arch: os.arch(),
|
|
version: version.toString(),
|
|
channel: channel
|
|
});
|
|
|
|
callbackLog("info", "Downloading version " + version.toString(false) + " to " + targetFilePath + " from " + updateServerUrl());
|
|
console.log("Downloading update from %s. (%s)", updateServerUrl(), requestUrl);
|
|
|
|
return new Promise<string>((resolve, reject) => {
|
|
let fired = false;
|
|
const fireFailed = (reason: string) => {
|
|
if(fired) { return; }
|
|
fired = true;
|
|
|
|
setImmediate(reject, reason);
|
|
};
|
|
|
|
let stream = progress(request.get(requestUrl, {
|
|
timeout: 10_000
|
|
}, (error, response, _body) => {
|
|
if(!response) {
|
|
fireFailed("Missing response object");
|
|
return;
|
|
}
|
|
|
|
if(response.statusCode != 200) {
|
|
fireFailed("Invalid HTTP response code: " + response.statusCode + (response.statusMessage ? "/" + response.statusMessage : ""));
|
|
return;
|
|
}
|
|
})).on('progress', status).on('error', error => {
|
|
console.warn("Encountered error within download pipe. Ignoring error: %o", error);
|
|
}).on('end', function () {
|
|
callbackLog("info", "Update downloaded.");
|
|
console.log("Update downloaded successfully. Waiting for write stream to finish.");
|
|
|
|
if(status) {
|
|
status({
|
|
percent: 1,
|
|
speed: 0,
|
|
size: { total: 0, transferred: 0},
|
|
time: { elapsed: 0, remaining: 0}
|
|
});
|
|
}
|
|
});
|
|
|
|
console.log("Decompressing update package while streaming!");
|
|
stream = stream.pipe(zlib.createGunzip());
|
|
stream.pipe(fs.createWriteStream(targetFilePath, {
|
|
autoClose: true
|
|
})).on('finish', () => {
|
|
console.log("Write stream has finished. Download successfully.");
|
|
if(!fired && (fired = true)) {
|
|
setImmediate(resolve, targetFilePath);
|
|
}
|
|
}).on('error', error => {
|
|
console.log("Write stream encountered an error while downloading update. Error: %o", error);
|
|
fireFailed("disk write error");
|
|
});
|
|
});
|
|
}
|
|
|
|
if(typeof(String.prototype.trim) === "undefined")
|
|
{
|
|
String.prototype.trim = function()
|
|
{
|
|
return String(this).replace(/^\s+|\s+$/g, '');
|
|
};
|
|
}
|
|
|
|
export async function ensureTargetFilesAreWriteable(updateFile: string) : Promise<string[]> {
|
|
const original_fs = require('original-fs');
|
|
if(!fs.existsSync(updateFile)) {
|
|
throw "Missing update file (" + updateFile + ")";
|
|
}
|
|
|
|
let parent_path = app.getAppPath();
|
|
if(parent_path.endsWith(".asar")) {
|
|
parent_path = path.join(parent_path, "..", "..");
|
|
parent_path = fs.realpathSync(parent_path);
|
|
}
|
|
|
|
const test_access = async (file: string, mode: number) => {
|
|
return await new Promise<NodeJS.ErrnoException>(resolve => original_fs.access(file, mode, resolve));
|
|
};
|
|
|
|
let code = await test_access(updateFile, original_fs.constants.R_OK);
|
|
if(code)
|
|
throw "Failed test read for update file. (" + updateFile + " results in " + code.code + ")";
|
|
|
|
const fstream = original_fs.createReadStream(updateFile);
|
|
const tar_stream = tar.extract();
|
|
|
|
const errors: string[] = [];
|
|
const tester = async (header: Headers) => {
|
|
const entry_path = path.normalize(path.join(parent_path, header.name));
|
|
if(header.type == "file") {
|
|
if(original_fs.existsSync(entry_path)) {
|
|
code = await test_access(entry_path, original_fs.constants.W_OK);
|
|
if(code)
|
|
errors.push("Failed to acquire write permissions for file " + entry_path + " (Code " + code.code + ")");
|
|
} else {
|
|
let directory = path.dirname(entry_path);
|
|
while(directory.length != 0 && !original_fs.existsSync(directory))
|
|
directory = path.normalize(path.join(directory, ".."));
|
|
|
|
code = await test_access(directory, original_fs.constants.W_OK);
|
|
if(code) errors.push("Failed to acquire write permissions for directory " + entry_path + " (Code " + code.code + ". Target directory " + directory + ")");
|
|
}
|
|
} else if(header.type == "directory") {
|
|
let directory = path.dirname(entry_path);
|
|
while(directory.length != 0 && !original_fs.existsSync(directory))
|
|
directory = path.normalize(path.join(directory, ".."));
|
|
|
|
code = await test_access(directory, original_fs.constants.W_OK);
|
|
if(code) errors.push("Failed to acquire write permissions for directory " + entry_path + " (Code " + code.code + ". Target directory " + directory + ")");
|
|
}
|
|
};
|
|
|
|
tar_stream.on('entry', (header: Headers, stream, next) => {
|
|
tester(header).catch(error => {
|
|
console.log("Emit out of tar_stream.on('entry' ...)");
|
|
tar_stream.emit('error', error);
|
|
}).then(() => {
|
|
stream.on('end', next);
|
|
stream.resume();
|
|
});
|
|
});
|
|
|
|
fstream.pipe(tar_stream);
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
tar_stream.on('finish', resolve);
|
|
tar_stream.on('error', error => { reject(error); });
|
|
});
|
|
} catch(error) {
|
|
throw "Failed to list files within tar: " + error;
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
namespace install_config {
|
|
export interface LockFile {
|
|
filename: string;
|
|
timeout: number;
|
|
"error-id": string;
|
|
}
|
|
export interface MoveFile {
|
|
source: string;
|
|
target: string;
|
|
"error-id": string;
|
|
}
|
|
export interface ConfigFile {
|
|
version: number;
|
|
|
|
backup: boolean;
|
|
"backup-directory": string;
|
|
|
|
"callback_file": string;
|
|
"callback_argument_fail": string;
|
|
"callback_argument_success": string;
|
|
|
|
moves: MoveFile[];
|
|
locks: LockFile[];
|
|
}
|
|
}
|
|
|
|
async function createUpdateInstallConfig(sourceRoot: string, targetRoot: string) : Promise<install_config.ConfigFile> {
|
|
console.log("Building update install config for target directory: %s. Update source: %o", targetRoot, sourceRoot);
|
|
const result: install_config.ConfigFile = { } as any;
|
|
|
|
result.version = 1;
|
|
|
|
result.backup = true;
|
|
|
|
{
|
|
const data = path.parse(sourceRoot);
|
|
result["backup-directory"] = path.join(data.dir, data.name + "_backup");
|
|
}
|
|
|
|
result["permission-test-directory"] = targetRoot;
|
|
result.callback_file = app.getPath("exe");
|
|
result.callback_argument_fail = "--no-single-instance --update-failed-new=";
|
|
result.callback_argument_success = "--no-single-instance --update-succeed-new=";
|
|
|
|
result.moves = [];
|
|
result.locks = [
|
|
{
|
|
"error-id": "main-exe-lock",
|
|
filename: app.getPath("exe"),
|
|
timeout: 5000
|
|
}
|
|
];
|
|
|
|
const ignoreFileList = [
|
|
"update-installer.exe", "update-installer"
|
|
];
|
|
|
|
const dirWalker = async (relative_path: string) => {
|
|
const source_directory = path.join(sourceRoot, relative_path);
|
|
const target_directory = path.join(targetRoot, relative_path);
|
|
|
|
let files: string[];
|
|
try {
|
|
files = await util.promisify(ofs.readdir)(source_directory);
|
|
} catch(error) {
|
|
console.warn("Failed to iterate over source directory \"%s\": %o", source_directory, error);
|
|
return;
|
|
}
|
|
|
|
for(const file of files) {
|
|
let shouldBeExcluded = false;
|
|
for(const ignoredFile of ignoreFileList) {
|
|
if(ignoredFile == file) {
|
|
console.debug("Ignoring file to update (%s/%s)", relative_path, file);
|
|
shouldBeExcluded = true;
|
|
break;
|
|
}
|
|
}
|
|
if(shouldBeExcluded) {
|
|
continue;
|
|
}
|
|
|
|
const source_file = path.join(source_directory, file);
|
|
const target_file = path.join(target_directory, file);
|
|
|
|
//TODO check if file content has changed else ignore?
|
|
|
|
const info = await util.promisify(ofs.stat)(source_file);
|
|
if(info.isDirectory()) {
|
|
await dirWalker(path.join(relative_path, file));
|
|
} else {
|
|
/* TODO: ensure its a file! */
|
|
result.moves.push({
|
|
"error-id": "move-file-" + result.moves.length,
|
|
source: source_file,
|
|
target: target_file
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
await dirWalker(".");
|
|
return result;
|
|
}
|
|
|
|
export async function extractUpdateFile(updateFile: string, callbackLog: UpdateLogCallback) : Promise<{ updateSourceDirectory: string, updateInstallerExecutable: string }> {
|
|
const temporaryDirectory = path.join(app.getPath("temp"), "teaclient_update_" + Math.random().toString(36).substring(7));
|
|
|
|
try {
|
|
await fs.mkdirp(temporaryDirectory)
|
|
} catch(error) {
|
|
console.error("failed to create update source directory (%s): %o", temporaryDirectory, error);
|
|
throw "failed to create update source directory";
|
|
}
|
|
|
|
callbackLog("info", "Extracting update to " + temporaryDirectory);
|
|
console.log("Extracting update file %s to %s", updateFile, temporaryDirectory);
|
|
|
|
let updateInstallerPath = undefined;
|
|
|
|
const updateFileStream = fs.createReadStream(updateFile);
|
|
const extract = tar.extract();
|
|
|
|
extract.on('entry', (header: Headers, stream: PassThrough, callback) => {
|
|
const extract = async (header: Headers, stream: PassThrough) => {
|
|
const targetFile = path.join(temporaryDirectory, header.name);
|
|
console.debug("Extracting entry %s of type %s to %s", header.name, header.type, targetFile);
|
|
|
|
if(header.type == "directory") {
|
|
await fs.mkdirp(targetFile);
|
|
} else if(header.type == "file") {
|
|
const targetPath = path.parse(targetFile);
|
|
|
|
{
|
|
const directory = targetPath.dir;
|
|
console.debug("Testing for directory: %s", directory);
|
|
if(!(await util.promisify(ofs.exists)(directory)) || !(await util.promisify(ofs.stat)(directory)).isDirectory()) {
|
|
console.log("Creating directory %s", directory);
|
|
try {
|
|
await fs.mkdirp(directory);
|
|
} catch(error) {
|
|
console.warn("failed to create directory for file %s", header.type);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
const write_stream = ofs.createWriteStream(targetFile);
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
stream.pipe(write_stream)
|
|
.on('error', reject)
|
|
.on('finish', resolve);
|
|
});
|
|
|
|
if(targetPath.name === "update-installer" || targetPath.name === "update-installer.exe") {
|
|
updateInstallerPath = targetFile;
|
|
callbackLog("info", "Found update installer at " + targetFile);
|
|
}
|
|
|
|
return; /* success */
|
|
} catch(error) {
|
|
console.error("Failed to extract update file %s: %o", header.name, error);
|
|
}
|
|
} else {
|
|
console.debug("Skipping this unknown file type");
|
|
}
|
|
stream.resume(); /* drain the stream */
|
|
};
|
|
|
|
extract(header, stream).catch(error => {
|
|
console.log("Ignoring file %s due to an error: %o", header.name, error);
|
|
}).then(() => {
|
|
callback();
|
|
});
|
|
});
|
|
|
|
|
|
updateFileStream.pipe(extract);
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
extract.on('finish', resolve);
|
|
extract.on('error', reject);
|
|
});
|
|
} catch(error) {
|
|
console.error("Failed to unpack update: %o", error);
|
|
throw "update unpacking failed";
|
|
}
|
|
|
|
if(typeof updateInstallerPath !== "string" || !(await fs.pathExists(updateInstallerPath))) {
|
|
throw "missing update installer executable within update package";
|
|
}
|
|
|
|
callbackLog("info", "Update successfully extracted");
|
|
return { updateSourceDirectory: temporaryDirectory, updateInstallerExecutable: updateInstallerPath }
|
|
}
|
|
|
|
let cachedAppInfo: AppInfoFile;
|
|
async function initializeAppInfo() {
|
|
let directory = app.getAppPath();
|
|
if(!directory.endsWith(".asar")) {
|
|
/* we're in a development version */
|
|
cachedAppInfo = {
|
|
version: 2,
|
|
clientVersion: {
|
|
major: 0,
|
|
minor: 0,
|
|
patch: 0,
|
|
buildIndex: 0,
|
|
timestamp: Date.now()
|
|
},
|
|
|
|
uiPackChannel: "release",
|
|
clientChannel: "release"
|
|
};
|
|
return;
|
|
}
|
|
|
|
cachedAppInfo = validateAppInfo(await fs.readJson(path.join(directory, "..", "..", "app-info.json")));
|
|
if(cachedAppInfo.version !== 2) {
|
|
cachedAppInfo = undefined;
|
|
throw "invalid app info version";
|
|
}
|
|
}
|
|
|
|
export function clientAppInfo() : AppInfoFile {
|
|
if(typeof cachedAppInfo !== "object") {
|
|
throw "app info not initialized";
|
|
}
|
|
|
|
return cachedAppInfo;
|
|
}
|
|
|
|
export async function currentClientVersion() : Promise<Version> {
|
|
if(processArguments.has_value(Arguments.UPDATER_LOCAL_VERSION)) {
|
|
return parseVersion(processArguments.value(Arguments.UPDATER_LOCAL_VERSION));
|
|
}
|
|
|
|
const info = clientAppInfo();
|
|
return new Version(info.clientVersion.major, info.clientVersion.minor, info.clientVersion.patch, info.clientVersion.buildIndex, info.clientVersion.timestamp);
|
|
}
|
|
|
|
let cachedUpdateConfig: UpdateConfigFile;
|
|
function updateConfigFile() : string {
|
|
return path.join(electron.app.getPath('userData'), "update-settings.json");
|
|
}
|
|
|
|
export async function initializeAppUpdater() {
|
|
try {
|
|
await initializeAppInfo();
|
|
} catch (error) {
|
|
console.error("Failed to parse app info: %o", error);
|
|
throw "Failed to parse app info file";
|
|
}
|
|
|
|
const config = updateConfigFile();
|
|
if(await fs.pathExists(config)) {
|
|
try {
|
|
cachedUpdateConfig = validateUpdateConfig(await fs.readJson(config));
|
|
if(cachedUpdateConfig.version !== 1) {
|
|
cachedUpdateConfig = undefined;
|
|
throw "invalid update config version";
|
|
}
|
|
} catch (error) {
|
|
console.warn("Failed to parse update config file: %o. Invalidating it.", error);
|
|
try {
|
|
await fs.rename(config, config + "." + Date.now());
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
|
|
if(!cachedUpdateConfig) {
|
|
cachedUpdateConfig = {
|
|
version: 1,
|
|
selectedChannel: "release"
|
|
}
|
|
}
|
|
}
|
|
|
|
export function updateConfig() {
|
|
if(typeof cachedUpdateConfig === "string") {
|
|
throw "app updater hasn't been initialized yet";
|
|
}
|
|
return cachedUpdateConfig;
|
|
}
|
|
|
|
export function saveUpdateConfig() {
|
|
const file = updateConfigFile();
|
|
fs.writeJson(file, cachedUpdateConfig).catch(error => {
|
|
console.error("Failed to save update config: %o", error);
|
|
});
|
|
}
|
|
|
|
/* Attention: The current channel might not be the channel the client has initially been loaded with! */
|
|
export function clientUpdateChannel() : string {
|
|
return updateConfig().selectedChannel;
|
|
}
|
|
|
|
export function setClientUpdateChannel(channel: string) {
|
|
if(updateConfig().selectedChannel == channel) {
|
|
return;
|
|
}
|
|
|
|
updateConfig().selectedChannel = channel;
|
|
saveUpdateConfig();
|
|
}
|
|
|
|
export async function availableClientUpdate() : Promise<UpdateVersion | undefined> {
|
|
const version = await newestRemoteClientVersion(clientAppInfo().clientChannel);
|
|
if(!version) { return undefined; }
|
|
|
|
const localVersion = await currentClientVersion();
|
|
return !localVersion.isDevelopmentVersion() && version.version.newerThan(localVersion) ? version : undefined;
|
|
}
|
|
|
|
/**
|
|
* @returns The callback to execute the update
|
|
*/
|
|
export async function prepareUpdateExecute(targetVersion: UpdateVersion, callbackStats: UpdateStatsCallback, callbackLog: UpdateLogCallback) : Promise<{ callbackExecute: () => void, callbackAbort: () => void }> {
|
|
let targetApplicationPath = app.getAppPath();
|
|
if(targetApplicationPath.endsWith(".asar")) {
|
|
console.log("App path points to ASAR file (Going up to root directory)");
|
|
targetApplicationPath = await fs.realpath(path.join(targetApplicationPath, "..", ".."));
|
|
} else {
|
|
throw "the source can't be updated";
|
|
}
|
|
|
|
callbackStats("Downloading update", 0);
|
|
|
|
const updateFilePath = await downloadClientVersion(targetVersion.channel, targetVersion.version, status => {
|
|
callbackStats("Downloading update", status.percent);
|
|
}, callbackLog);
|
|
|
|
/* TODO: Remove this step and let the actual updater so this. If this fails we'll already receiving appropiate error messages. */
|
|
if(os.platform() !== "win32") {
|
|
callbackLog("info", "Checking file permissions");
|
|
callbackStats("Checking file permissions", .25);
|
|
|
|
/* We must be on a unix based system */
|
|
try {
|
|
const inaccessiblePaths = await ensureTargetFilesAreWriteable(updateFilePath);
|
|
if(inaccessiblePaths.length > 0) {
|
|
console.log("Failed to access the following files:");
|
|
for(const fail of inaccessiblePaths) {
|
|
console.log(" - " + fail);
|
|
}
|
|
|
|
const executeCommand = "sudo " + path.normalize(app.getAppPath()) + " --update-execute";
|
|
throw "Failed to access target files.\nPlease execute this app with administrator (sudo) privileges.\nUse the following command:\n" + executeCommand;
|
|
}
|
|
} catch(error) {
|
|
console.warn("Failed to validate target file accessibility: %o", error);
|
|
}
|
|
} else {
|
|
/* the windows update already requests admin privileges */
|
|
}
|
|
|
|
callbackStats("Extracting update", .5);
|
|
const { updateSourceDirectory, updateInstallerExecutable } = await extractUpdateFile(updateFilePath, callbackLog);
|
|
|
|
callbackStats("Generating install config", .5);
|
|
|
|
callbackLog("info", "Generating install config");
|
|
let installConfig;
|
|
try {
|
|
installConfig = await createUpdateInstallConfig(updateSourceDirectory, targetApplicationPath);
|
|
} catch(error) {
|
|
console.error("Failed to build update installer config: %o", error);
|
|
throw "failed to build update installer config";
|
|
}
|
|
|
|
const installLogFile = path.join(updateSourceDirectory, "update-log.txt");
|
|
const installConfigFile = path.join(updateSourceDirectory, "update_install.json");
|
|
console.log("Writing config to %s", installConfigFile);
|
|
try {
|
|
await fs.writeJSON(installConfigFile, installConfig);
|
|
} catch(error) {
|
|
console.error("Failed to write update install config file: %s", error);
|
|
throw "failed to write update install config file";
|
|
}
|
|
|
|
callbackLog("info", "Generating config generated at " + installConfigFile);
|
|
|
|
let executeCallback: () => void;
|
|
if(os.platform() == "linux") {
|
|
console.log("Executing update install on linux");
|
|
|
|
//We have to unpack it later
|
|
executeCallback = () => {
|
|
console.log("Executing command %s with args %o", updateInstallerExecutable, [installLogFile, installConfigFile]);
|
|
try {
|
|
let result = child_process.spawnSync(updateInstallerExecutable, [installLogFile, installConfigFile]);
|
|
if(result.status != 0) {
|
|
console.error("Failed to execute update installer! Return code: %d", result.status);
|
|
dialog.showMessageBox({
|
|
buttons: ["update now", "remind me later"],
|
|
title: "Update available",
|
|
message:
|
|
"Failed to execute update installer\n" +
|
|
"Installer exited with code " + result.status
|
|
} as MessageBoxOptions);
|
|
}
|
|
} catch(error) {
|
|
console.error("Failed to execute update installer (%o)", error);
|
|
if("errno" in error) {
|
|
const errno = error as ErrnoException;
|
|
if(errno.errno == os.constants.errno.EPERM) {
|
|
dialog.showMessageBox({
|
|
buttons: ["quit"],
|
|
title: "Update execute failed",
|
|
message: "Failed to execute update installer. (No permissions)\nPlease execute the client with admin privileges!"
|
|
} as MessageBoxOptions);
|
|
return;
|
|
}
|
|
dialog.showMessageBox({
|
|
buttons: ["quit"],
|
|
title: "Update execute failed",
|
|
message: "Failed to execute update installer.\nError: " + errno.message
|
|
} as MessageBoxOptions);
|
|
return;
|
|
}
|
|
dialog.showMessageBox({
|
|
buttons: ["quit"],
|
|
title: "Update execute failed",
|
|
message: "Failed to execute update installer.\nLookup console for more detail"
|
|
} as MessageBoxOptions);
|
|
return;
|
|
}
|
|
|
|
if(electron.app.hasSingleInstanceLock())
|
|
electron.app.releaseSingleInstanceLock();
|
|
|
|
const ids = child_process.execSync("pgrep TeaClient").toString().split(os.EOL).map(e => e.trim()).reverse().join(" ");
|
|
console.log("Executing %s", "kill -9 " + ids);
|
|
child_process.execSync("kill -9 " + ids);
|
|
};
|
|
} else {
|
|
console.log("Executing update install on windows");
|
|
|
|
executeCallback = () => {
|
|
console.log("Executing command %s with args %o", updateInstallerExecutable, [installLogFile, installConfigFile]);
|
|
|
|
try {
|
|
const pipe = child_process.spawn(updateInstallerExecutable, [installLogFile, installConfigFile], {
|
|
detached: true,
|
|
shell: true,
|
|
cwd: path.dirname(app.getAppPath()),
|
|
stdio: "ignore"
|
|
});
|
|
pipe.unref();
|
|
app.quit();
|
|
} catch(error) {
|
|
console.dir(error);
|
|
electron.dialog.showErrorBox("Failed to finalize update", "Failed to finalize update.\nInvoking the update-installer.exe failed.\nLookup the console for more details.");
|
|
}
|
|
};
|
|
}
|
|
|
|
callbackStats("Update successfully prepared", 1);
|
|
callbackLog("info", "Update successfully prepared");
|
|
|
|
return {
|
|
callbackExecute: executeCallback,
|
|
callbackAbort: () => {
|
|
/* TODO: Cleanup */
|
|
}
|
|
}
|
|
} |