876 lines
33 KiB
TypeScript
876 lines
33 KiB
TypeScript
import * as querystring from "querystring";
|
|
import * as request from "request";
|
|
import {app, dialog, ipcMain} 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 {parse_version, Version} from "../../shared/version";
|
|
|
|
import Timer = NodeJS.Timer;
|
|
import MessageBoxOptions = Electron.MessageBoxOptions;
|
|
import {Headers} from "tar-stream";
|
|
import {Arguments, process_args} from "../../shared/process-arguments";
|
|
import * as electron from "electron";
|
|
import {PassThrough} from "stream";
|
|
import * as _main_windows from "../main_window";
|
|
import ErrnoException = NodeJS.ErrnoException;
|
|
import {EPERM} from "constants";
|
|
import * as winmgr from "../window";
|
|
|
|
const is_debug = false;
|
|
export function server_url() : string {
|
|
const default_path = is_debug ? "http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/client-api/environment/" : "http://clientapi.teaspeak.de/";
|
|
return process_args.has_value(...Arguments.SERVER_URL) ? process_args.value(...Arguments.SERVER_URL) : default_path;
|
|
}
|
|
|
|
export interface UpdateVersion {
|
|
channel: string;
|
|
platform: string,
|
|
arch: string;
|
|
version: Version;
|
|
}
|
|
|
|
export interface UpdateData {
|
|
versions: UpdateVersion[];
|
|
updater_version: UpdateVersion;
|
|
}
|
|
|
|
let version_cache: UpdateData = undefined;
|
|
export async function load_data(allow_cached: boolean = true) : Promise<UpdateData> {
|
|
if(version_cache && allow_cached) return Promise.resolve(version_cache);
|
|
|
|
return new Promise<UpdateData>((resolve, reject) => {
|
|
const request_url = server_url() + "/api.php?" + querystring.stringify({
|
|
type: "update-info"
|
|
});
|
|
console.log("request: %s", request_url);
|
|
request.get(request_url, {
|
|
timeout: 2000
|
|
}, (error, response, body) => {
|
|
if(!response || response.statusCode != 200) {
|
|
let info;
|
|
try {
|
|
info = JSON.parse(body) || {msg: error};
|
|
} catch(e) {
|
|
info = {msg: "!-- failed to parse json --!"};
|
|
}
|
|
setImmediate(reject, "Invalid status code (" + (response || {statusCode: -1}).statusCode + " | " + (info || {msg: "undefined"}).msg + ")");
|
|
return;
|
|
}
|
|
const data = JSON.parse(body);
|
|
if(!data) {
|
|
setImmediate(reject, "Invalid response");
|
|
return;
|
|
}
|
|
if(!data["success"]) {
|
|
setImmediate(reject, "Action failed (" + data["msg"] + ")");
|
|
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);
|
|
}
|
|
}
|
|
resolve(resp);
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function newest_version(current_version: Version, channel?: string) : Promise<UpdateVersion | undefined> {
|
|
if(!app.getAppPath().endsWith(".asar")) {
|
|
throw "You cant run an update when you're executing the source code!";
|
|
}
|
|
const data = await load_data();
|
|
let had_data = false;
|
|
for(const version of data.versions) {
|
|
if(version.arch == os.arch() && version.platform == os.platform()) {
|
|
if(!channel || version.channel == channel) {
|
|
if(!current_version || version.version.newer_than(current_version))
|
|
return version;
|
|
else
|
|
had_data = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!had_data)
|
|
throw "Missing data";
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* @param update_file The input file from where the update will get installed
|
|
* @return The target executable file
|
|
*/
|
|
export async function extract_updater(update_file: string) : Promise<string> {
|
|
if(!fs.existsSync(update_file)) throw "Missing update file!";
|
|
|
|
let update_installer = app.getPath('temp') + "/teaclient-update-installer-" + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
if(os.platform() == "win32")
|
|
update_installer += ".exe";
|
|
|
|
const source = fs.createReadStream(update_file);
|
|
const extract = tar.extract();
|
|
await new Promise((resolve, reject) => {
|
|
let updater_found = false;
|
|
source.on('end', () => {
|
|
if(!updater_found) {
|
|
console.error("Failed to extract the updater (Updater hasn't been found!)");
|
|
reject("Updater hasn't been found in bundle");
|
|
}
|
|
|
|
resolve();
|
|
});
|
|
|
|
extract.on('entry', (header: Headers, stream, callback) => {
|
|
stream.on('end', callback);
|
|
console.log("Got entry " + header.name);
|
|
|
|
if(header.name == "./update-installer" || header.name == "./update-installer.exe") {
|
|
console.log("Found updater! (" + header.size + " bytes)");
|
|
console.log("Extracting to %s", update_installer);
|
|
const s = fs.createWriteStream(update_installer);
|
|
stream.pipe(s).on('finish', event => {
|
|
console.log("Updater extracted and written!");
|
|
updater_found = true;
|
|
resolve();
|
|
}).on('error', event => {
|
|
console.error("Failed write update file: %o", event);
|
|
reject("failed to write file")
|
|
});
|
|
} else {
|
|
stream.resume(); //Drain the stream
|
|
}
|
|
});
|
|
|
|
|
|
source.pipe(extract);
|
|
});
|
|
|
|
return update_installer;
|
|
}
|
|
|
|
export async function update_updater() : Promise<void> {
|
|
//TODO here
|
|
return Promise.resolve();
|
|
}
|
|
|
|
function data_directory() : string {
|
|
return electron.app.getPath('userData');
|
|
}
|
|
|
|
function get_update_file(channel: string, version: Version) : string {
|
|
let _path = fs.realpathSync(data_directory());
|
|
|
|
const name = channel + "_" + version.major + "_" + version.minor + "_" + version.patch + "_" + version.build + ".tar";
|
|
return path.join(_path, "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 download_version(channel: string, version: Version, status?: (state: ProgressState) => any) : Promise<string> {
|
|
const target_path = get_update_file(channel, version);
|
|
console.log("Downloading version %s to %s", version.toString(false), target_path);
|
|
if(fs.existsSync(target_path)) {
|
|
/* TODO test if this file is valid and can be used */
|
|
try {
|
|
await fs.remove(target_path);
|
|
} catch(error) {
|
|
throw "Failed to remove old file: " + error;
|
|
}
|
|
}
|
|
|
|
try {
|
|
await fs.mkdirp(path.dirname(target_path));
|
|
} catch(error) {
|
|
throw "Failed to make target directory: " + path.dirname(target_path);
|
|
}
|
|
|
|
const url = server_url() + "/api.php?" + querystring.stringify({
|
|
type: "update-download",
|
|
platform: os.platform(),
|
|
arch: os.arch(),
|
|
version: version.toString(),
|
|
channel: channel
|
|
});
|
|
console.log("Downloading update from %s. (%s)", server_url(), url);
|
|
return new Promise<string>((resolve, reject) => {
|
|
let fired = false;
|
|
let stream = progress(request.get(url, {
|
|
timeout: 2000
|
|
}, (error, response, body) => {
|
|
if(!response || response.statusCode != 200) {
|
|
let info;
|
|
try {
|
|
info = JSON.parse(body)
|
|
} catch(e) {
|
|
info = {"msg": "!-- failed to parse json --!"};
|
|
}
|
|
if(!fired && (fired = true))
|
|
setImmediate(reject, "Invalid status code (" + (response || {statusCode: -1}).statusCode + "|" + (info || {"msg": "undefined"}).msg + ")");
|
|
return;
|
|
}
|
|
})).on('progress', _state => status ? status(_state) : {}).on('error', error => {
|
|
console.warn("Encountered error within download pipe. Ignoring error: %o", error);
|
|
}).on('end', function () {
|
|
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(target_path, {
|
|
autoClose: true
|
|
})).on('finish', () => {
|
|
console.log("Write stream has finished. Download successfully.");
|
|
if(!fired && (fired = true))
|
|
setImmediate(resolve, target_path);
|
|
}).on('error', error => {
|
|
console.log("Write stream encountered an error while downloading update. Error: %o", error);
|
|
if(!fired && (fired = true))
|
|
setImmediate(reject,"failed to write");
|
|
});
|
|
});
|
|
}
|
|
|
|
if(typeof(String.prototype.trim) === "undefined")
|
|
{
|
|
String.prototype.trim = function()
|
|
{
|
|
return String(this).replace(/^\s+|\s+$/g, '');
|
|
};
|
|
}
|
|
|
|
export async function test_file_accessibility(update_file: string) : Promise<string[]> {
|
|
if(os.platform() === "win32")
|
|
return []; /* within windows the update installer request admin privileges if required */
|
|
|
|
const original_fs = require('original-fs');
|
|
if(!fs.existsSync(update_file))
|
|
throw "Missing update file (" + update_file + ")";
|
|
|
|
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(update_file, original_fs.constants.R_OK);
|
|
if(code)
|
|
throw "Failed test read for update file. (" + update_file + " results in " + code.code + ")";
|
|
|
|
const fstream = original_fs.createReadStream(update_file);
|
|
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 build_install_config(source_root: string, target_root: string) : Promise<install_config.ConfigFile> {
|
|
console.log("Building update install config for target directory: %s. Update source: %o", target_root, source_root);
|
|
const result: install_config.ConfigFile = { } as any;
|
|
|
|
result.version = 1;
|
|
|
|
result.backup = true;
|
|
|
|
{
|
|
const data = path.parse(source_root);
|
|
result["backup-directory"] = path.join(data.dir, data.name + "_backup");
|
|
}
|
|
|
|
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 ignore_list = [
|
|
"update-installer.exe", "update-installer"
|
|
];
|
|
|
|
const dir_walker = async (relative_path: string) => {
|
|
const source_directory = path.join(source_root, relative_path);
|
|
const target_directory = path.join(target_root, 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 _exclude = false;
|
|
for(const exclude of ignore_list) {
|
|
if(exclude == file) {
|
|
console.debug("Ignoring file to update (%s/%s)", relative_path, file);
|
|
_exclude = true;
|
|
break;
|
|
}
|
|
}
|
|
if(_exclude)
|
|
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 dir_walker(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 dir_walker(".");
|
|
return result;
|
|
}
|
|
|
|
export async function execute_update(update_file: string, restart_callback: (callback: () => void) => any) : Promise<void> {
|
|
let application_path = app.getAppPath();
|
|
if(application_path.endsWith(".asar")) {
|
|
console.log("App path points to ASAR file (Going up to root directory)");
|
|
application_path = await fs.realpath(path.join(application_path, "..", ".."));
|
|
} else if(await fs.pathExists(application_path) && (await fs.stat(application_path)).isFile())
|
|
application_path = path.dirname(application_path);
|
|
|
|
console.log("Located target app path: %s", application_path);
|
|
console.log("Using update file: %s", update_file);
|
|
|
|
const temp_directory = path.join(app.getPath("temp"), "teaclient_update_" + Math.random().toString(36).substring(7));
|
|
let updater_executable;
|
|
{
|
|
console.log("Preparing update source directory at %s", temp_directory);
|
|
try {
|
|
await fs.mkdirp(temp_directory)
|
|
} catch(error) {
|
|
console.error("failed to create update source directory: %o", error);
|
|
throw "failed to create update source directory";
|
|
}
|
|
|
|
const source = fs.createReadStream(update_file);
|
|
const extract = tar.extract();
|
|
|
|
extract.on('entry', (header: Headers, stream: PassThrough, callback) => {
|
|
const extract = async (header: Headers, stream: PassThrough) => {
|
|
const target_file = path.join(temp_directory, header.name);
|
|
console.debug("Extracting entry %s of type %s to %s", header.name, header.type, target_file);
|
|
|
|
if(header.type == "directory") {
|
|
await fs.mkdirp(target_file);
|
|
} else if(header.type == "file") {
|
|
const target_finfo = path.parse(target_file);
|
|
{
|
|
const directory = target_finfo.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(target_file);
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
stream.pipe(write_stream)
|
|
.on('error', reject)
|
|
.on('finish', resolve);
|
|
});
|
|
|
|
if(target_finfo.name === "update-installer" || target_finfo.name === "update-installer.exe") {
|
|
updater_executable = target_file;
|
|
console.log("Found update installer: %s", target_file);
|
|
}
|
|
|
|
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();
|
|
});
|
|
});
|
|
|
|
|
|
source.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(updater_executable) !== "string" || !(await fs.pathExists(updater_executable)))
|
|
throw "missing update installer executable within update package";
|
|
|
|
/* the "new" environment should now be available at 'temp_directory' */
|
|
console.log("Update unpacked successfully. Building update extractor file.");
|
|
|
|
let install_config;
|
|
try {
|
|
install_config = await build_install_config(temp_directory, application_path);
|
|
} catch(error) {
|
|
console.error("Failed to build update installer config: %o", error);
|
|
throw "failed to build update installer config";
|
|
}
|
|
|
|
const log_file = path.join(temp_directory, "update-log.txt");
|
|
const config_file = path.join(temp_directory, "update_install.json");
|
|
console.log("Writing config to %s", config_file);
|
|
try {
|
|
await fs.writeJSON(config_file, install_config);
|
|
} catch(error) {
|
|
console.error("Failed to write update install config file: %s", error);
|
|
throw "failed to write update install config file";
|
|
}
|
|
|
|
if(os.platform() == "linux") {
|
|
console.log("Executing update install on linux");
|
|
|
|
//We have to unpack it later
|
|
const rest_callback = () => {
|
|
console.log("Executing command %s with args %o", updater_executable, [log_file, config_file]);
|
|
try {
|
|
let result = child_process.spawnSync(updater_executable, [log_file, config_file]);
|
|
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 == 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);
|
|
};
|
|
restart_callback(rest_callback);
|
|
} else {
|
|
console.log("Executing update install on windows");
|
|
|
|
//We have to unpack it later
|
|
const rest_callback = () => {
|
|
console.log("Executing command %s with args %o", updater_executable, [log_file, config_file]);
|
|
|
|
try {
|
|
const pipe = child_process.spawn(updater_executable, [log_file, config_file], {
|
|
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.");
|
|
}
|
|
};
|
|
restart_callback(rest_callback);
|
|
}
|
|
}
|
|
|
|
export async function current_version() : Promise<Version> {
|
|
if(process_args.has_value(Arguments.UPDATER_LOCAL_VERSION))
|
|
return parse_version(process_args.value(Arguments.UPDATER_LOCAL_VERSION));
|
|
|
|
let parent_path = app.getAppPath();
|
|
if(parent_path.endsWith(".asar")) {
|
|
parent_path = path.join(parent_path, "..", "..");
|
|
parent_path = fs.realpathSync(parent_path);
|
|
}
|
|
try {
|
|
const info = await fs.readJson(path.join(parent_path, "app_version.json"));
|
|
let result = parse_version(info["version"]);
|
|
result.timestamp = info["timestamp"];
|
|
return result;
|
|
} catch (error) {
|
|
console.log("Got no version!");
|
|
return new Version(0, 0, 0, 0, 0);
|
|
}
|
|
}
|
|
|
|
async function minawait<T>(object: Promise<T>, time: number) : Promise<T> {
|
|
const begin = Date.now();
|
|
const r = await object;
|
|
const end = Date.now();
|
|
if(end - begin < time)
|
|
await new Promise(resolve => setTimeout(resolve, time + begin - end));
|
|
return r;
|
|
}
|
|
|
|
export let update_restart_pending = false;
|
|
export async function execute_graphical(channel: string, ask_install: boolean) : Promise<Boolean> {
|
|
const electron = require('electron');
|
|
|
|
const ui_debug = process_args.has_flag(Arguments.UPDATER_UI_DEBUG);
|
|
const window = new electron.BrowserWindow({
|
|
show: false,
|
|
width: ui_debug ? 1200 : 600,
|
|
height: ui_debug ? 800 : 400,
|
|
|
|
webPreferences: {
|
|
devTools: true,
|
|
nodeIntegration: true,
|
|
javascript: true
|
|
}
|
|
});
|
|
|
|
window.loadFile(path.join(path.dirname(module.filename), "ui", "index.html"));
|
|
if(ui_debug) {
|
|
window.webContents.openDevTools();
|
|
}
|
|
await new Promise(resolve => window.on('ready-to-show', resolve));
|
|
window.show();
|
|
await winmgr.apply_bounds('update-installer', window);
|
|
winmgr.track_bounds('update-installer', window);
|
|
|
|
const current_vers = await current_version();
|
|
console.log("Current version: " + current_vers.toString(true));
|
|
|
|
console.log("Showed");
|
|
const set_text = text => window.webContents.send('status-update-text', text);
|
|
const set_error = text => window.webContents.send('status-error', text);
|
|
const set_progress = progress => window.webContents.send('status-update', progress);
|
|
const await_exit = () => { return new Promise(resolve => window.on('closed', resolve))};
|
|
const await_version_confirm = version => {
|
|
const id = "version-accept-" + Date.now();
|
|
window.webContents.send('status-confirm-update', id, current_vers, version);
|
|
return new Promise((resolve, reject) => {
|
|
window.on('closed', () => resolve(false));
|
|
ipcMain.once(id, (event, result) => {
|
|
console.log("Got response %o", result);
|
|
resolve(result);
|
|
});
|
|
});
|
|
};
|
|
const await_confirm_execute = () => {
|
|
const id = "status-confirm-execute-" + Date.now();
|
|
window.webContents.send('status-confirm-execute', id);
|
|
return new Promise((resolve, reject) => {
|
|
window.on('closed', () => resolve(false));
|
|
ipcMain.once(id, (event, result) => {
|
|
console.log("Got response %o", result);
|
|
resolve(result);
|
|
});
|
|
});
|
|
};
|
|
|
|
set_text("Loading data");
|
|
let version: UpdateVersion;
|
|
try {
|
|
version = await minawait(newest_version(process_args.has_flag(Arguments.UPDATER_ENFORCE) ? undefined : current_vers, channel), 3000);
|
|
} catch (error) {
|
|
set_error("Failed to get newest information:<br>" + error);
|
|
await await_exit();
|
|
return false;
|
|
}
|
|
console.log("Got version %o", version);
|
|
|
|
if(!version) {
|
|
set_error("You're already on the newest version!");
|
|
await await_exit();
|
|
return false;
|
|
}
|
|
|
|
if(ask_install) {
|
|
try {
|
|
const test = await await_version_confirm(version.version);
|
|
if(!test) {
|
|
window.close();
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
console.dir(error);
|
|
window.close();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
set_text("Updating to version " + version.version.toString() + "<br>Downloading....");
|
|
let update_path: string;
|
|
try {
|
|
update_path = await download_version(version.channel, version.version, status => { setImmediate(set_progress, status.percent); });
|
|
} catch (error) {
|
|
set_error("Failed to download version: <br>" + error);
|
|
console.error(error);
|
|
await await_exit();
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const inaccessible = await test_file_accessibility(update_path);
|
|
if(inaccessible.length > 0) {
|
|
console.log("Failed to access the following files:");
|
|
for(const fail of inaccessible)
|
|
console.log(" - " + fail);
|
|
|
|
if(os.platform() == "linux") {
|
|
set_error("Failed to access target files.<br>Please execute this app with administrator (sudo) privileges.<br>Use the following command:<br><p>" +
|
|
"sudo " + path.normalize(app.getAppPath()) + " --update-execute=\"" + path.normalize(update_path) + "\"</p>");
|
|
await await_exit();
|
|
return false;
|
|
} else if(os.platform() == "win32") {
|
|
/* the updater asks for admin rights anyway :/ */
|
|
}
|
|
}
|
|
} catch(error) {
|
|
set_error("Failed to access target files.<br>You may need to execute the TeaClient as Administrator!<br>Error: " + error);
|
|
await await_exit();
|
|
return false;
|
|
}
|
|
|
|
if(!await await_confirm_execute()) {
|
|
window.close();
|
|
return false;
|
|
}
|
|
|
|
set_text("Extracting update installer...<br>Please wait");
|
|
try {
|
|
await extract_updater(update_path);
|
|
} catch(error) {
|
|
console.error("Failed to update the updater! (%o)", error);
|
|
set_error("Failed to update the update installer.\nUpdate failed!");
|
|
await await_exit();
|
|
return false;
|
|
}
|
|
set_text("Executing update...<br>Please wait");
|
|
|
|
try {
|
|
await execute_update(update_path, callback => {
|
|
_main_windows.set_prevent_instant_close(true);
|
|
update_restart_pending = true;
|
|
window.close();
|
|
callback();
|
|
});
|
|
} catch (error) {
|
|
dialog.showErrorBox("Update error", "Failed to execute update!\n" + error);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export let update_question_open = false;
|
|
async function check_update(channel: string) {
|
|
let version: UpdateVersion;
|
|
try {
|
|
version = await newest_version(await current_version(), channel);
|
|
} catch(error) {
|
|
console.warn("failed check for newer versions!");
|
|
console.error(error);
|
|
return;
|
|
}
|
|
if(version) {
|
|
update_question_open = true;
|
|
dialog.showMessageBox({
|
|
buttons: ["update now", "remind me later"],
|
|
title: "TeaClient: Update available",
|
|
message:
|
|
"There is an update available!\n" +
|
|
"Should we update now?\n" +
|
|
"\n" +
|
|
"Current version: " + (await current_version()).toString() + "\n" +
|
|
"Target version: " + version.version.toString()
|
|
} as MessageBoxOptions).then(result => {
|
|
if(result.response == 0) {
|
|
execute_graphical(channel, false).then(() => {
|
|
update_question_open = false;
|
|
});
|
|
} else {
|
|
update_question_open = false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
let update_task: Timer;
|
|
export function start_auto_update_check() {
|
|
if(update_task) return;
|
|
update_task = setInterval(check_update, 2 * 60 * 60 * 1000);
|
|
setImmediate(check_update);
|
|
}
|
|
|
|
export function stop_auto_update_check() {
|
|
clearInterval(update_task);
|
|
update_task = undefined;
|
|
}
|
|
|
|
export async function selected_channel() : Promise<string> {
|
|
return process_args.has_value(Arguments.UPDATER_CHANNEL) ? process_args.value(Arguments.UPDATER_CHANNEL) : "release";
|
|
} |