Updates for 1.5.0

This commit is contained in:
WolverinDEV 2020-12-02 18:08:49 +01:00
parent 53367bafa5
commit 467d228e23
80 changed files with 3615 additions and 1938 deletions

View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
npx typescript-json-validator ./modules/core/ui-loader/CacheFile.ts || exit 1
npx typescript-json-validator modules/core/ui-loader/ShippedFileInfo.ts || exit 1
npx typescript-json-validator modules/core/app-updater/UpdateConfigFile.ts || exit 1
npx typescript-json-validator modules/core/app-updater/AppInfoFile.ts || exit 1

2
github

@ -1 +1 @@
Subproject commit 30d1bc01979c59d3d869f3be733b8849b173b42c
Subproject commit 989bdd62182ba2d4ad040c4177d3ab72eb10e408

View File

@ -1,19 +1,19 @@
import {Options} from "electron-packager";
import * as packager from "electron-packager"
const pkg = require('../package.json');
const dev_dependencies = Object.keys(pkg.devDependencies);
import * as fs from "fs-extra";
import * as path_helper from "path";
import {parse_version} from "../modules/shared/version";
import {parseVersion} from "../modules/shared/version";
import * as child_process from "child_process";
import * as os from "os";
import * as querystring from "querystring";
import request = require("request");
import * as deployer from "./deploy";
import AppInfoFile from "../modules/core/app-updater/AppInfoFile";
let options: Options = {} as any;
let version = parse_version(pkg.version);
let version = parseVersion(pkg.version);
version.timestamp = Date.now();
options.dir = '.';
@ -29,7 +29,7 @@ if(!pkg.dependencies['electron']) {
options["version-string"] = {
'CompanyName': 'TeaSpeak',
'LegalCopyright': '© 2018-2020 Markus Hadenfeldt All Rights Reserved',
'LegalCopyright': options.appCopyright,
'FileDescription' : 'TeaSpeak-Client',
'OriginalFilename' : 'TeaClient.exe',
'FileVersion' : pkg.version,
@ -37,17 +37,13 @@ options["version-string"] = {
'ProductName' : 'TeaSpeak-Client',
'InternalName' : 'TeaClient.exe'
};
options.electronVersion = pkg.dependencies['electron'];
options.protocols = [{name: "TeaSpeak - Connect", schemes: ["teaserver"]}];
options.overwrite = true;
options.derefSymlinks = true;
options.buildVersion = version.toString(true);
options.asar = {
unpackDir: "teaclient-unpacked"
};
interface ProjectEntry {
type: ProjectEntryType;
}
@ -114,6 +110,24 @@ project_files.push({
} as ProjectDirectory);
if(process.argv.length < 4) {
console.error("Missing process argument:");
console.error("<win32/linux> <release/beta>");
process.exit(1);
}
switch (process.argv[3]) {
case "release":
case "beta":
break;
default:
console.error("Invalid release channel: %o", process.argv[3]);
process.exit(1);
break;
}
if (process.argv[2] == "linux") {
options.arch = "x64";
options.platform = "linux";
@ -127,7 +141,7 @@ if (process.argv[2] == "linux") {
process.exit(1);
}
const path_validator = (path: string) => {
const packagePathValidator = (path: string) => {
path = path.replace(/\\/g,"/");
const kIgnoreFile = true;
@ -195,7 +209,7 @@ options.ignore = path => {
if(path.length == 0)
return false; //Dont ignore root paths
const ignore_path = path_validator(path);
const ignore_path = packagePathValidator(path);
if(!ignore_path) {
console.log(" + " + path);
} else {
@ -245,7 +259,6 @@ async function copy_striped(source: string, target: string, symbol_directory: st
{
console.log("Striping file");
//TODO: Keep node module names!
const strip_command = await exec("strip -s " + target, {
maxBuffer: 1024 * 1024 * 512
});
@ -284,15 +297,14 @@ interface UIVersion {
filename?: string;
}
async function create_default_ui_pack(target_directory: string) {
async function downloadBundledUiPack(channel: string, targetDirectory: string) {
const remote_url = "http://clientapi.teaspeak.dev/";
const channel = "release";
const file = path_helper.join(target_directory, "default_ui.tar.gz");
const file = path_helper.join(targetDirectory, "bundled-ui.tar.gz");
console.log("Creating default UI pack. Downloading from %s (channel: %s)", remote_url, channel);
await fs.ensureDir(target_directory);
await fs.ensureDir(targetDirectory);
let ui_info: UIVersion;
let bundledUiInfo: UIVersion;
await new Promise((resolve, reject) => {
request.get(remote_url + "api.php?" + querystring.stringify({
type: "ui-download",
@ -301,10 +313,11 @@ async function create_default_ui_pack(target_directory: string) {
}), {
timeout: 5000
}).on('response', function(response) {
if(response.statusCode != 200)
if(response.statusCode != 200) {
reject("Failed to download UI files (Status code " + response.statusCode + ")");
}
ui_info = {
bundledUiInfo = {
channel: channel,
version: response.headers["x-ui-version"] as string,
git_hash: response.headers["x-ui-git-ref"] as string,
@ -317,10 +330,10 @@ async function create_default_ui_pack(target_directory: string) {
}).pipe(fs.createWriteStream(file)).on('finish', resolve);
});
if(!ui_info)
if(!bundledUiInfo)
throw "failed to generate ui info!";
await fs.writeJson(path_helper.join(target_directory, "default_ui_info.json"), ui_info);
await fs.writeJson(path_helper.join(targetDirectory, "bundled-ui.json"), bundledUiInfo);
console.log("UI-Pack downloaded!");
}
@ -334,20 +347,31 @@ new Promise((resolve, reject) => packager(options, (err, appPaths) => err ? reje
await create_native_addons(path_helper.join(app_paths[0], "resources", "natives"), "build/symbols");
return app_paths;
}).then(async app_paths => {
await create_default_ui_pack(path_helper.join(app_paths[0], "resources", "ui"));
await downloadBundledUiPack(process.argv[3], path_helper.join(app_paths[0], "resources", "ui"));
return app_paths;
}).then(async appPaths => {
///native/build/linux_amd64
path = appPaths[0];
if(process.argv[2] == "linux") {
await copy_striped(options.dir + "/native/build/exe/update-installer", path + "/update-installer", "build/symbols");
} else if (process.argv[2] == "win32") {
await copy_striped(options.dir + "/native/build/exe/update-installer.exe", path + "/update-installer.exe", "build/symbols");
}
await fs.writeJson(path + "/app_version.json", {
version: version.toString(true),
timestamp: version.timestamp
});
await fs.writeJson(path + "/app-info.json", {
version: 2,
clientVersion: {
timestamp: version.timestamp,
buildIndex: version.build,
patch: version.patch,
minor: version.minor,
major: version.major
},
clientChannel: process.argv[3],
uiPackChannel: process.argv[3]
} as AppInfoFile);
return appPaths;
}).then(async app_path => {
console.log("Fixing versions file");

View File

@ -47,8 +47,10 @@ declare namespace node_ssh {
let instance: node_ssh.Instance;
export async function setup() {
if(instance)
if(instance) {
throw "already initiaized";
}
instance = new _node_ssh();
try {
await instance.connect({
@ -96,8 +98,10 @@ function version_string(version: Version) {
export async function latest_version(platform: PlatformSpecs) {
const path = "versions/" + platform_path(platform);
if(!instance)
if(!instance) {
throw "Invalid instance";
}
const sftp = await instance.requestSFTP();
try {
if(!sftp)

View File

@ -128,7 +128,7 @@ export async function write_version(file: string, platform: string, arch: string
await fs.writeJson(file, versions);
}
export async function deploy(platform: string, arch: string, channel: string, version: Version, update_file: string, install_file: string, install_suffix: string) {
await new Promise((resolve, reject) => {
await new Promise(resolve => {
const url = (process.env["teaclient_deploy_url"] || "http://clientapi.teaspeak.de/") + "api.php";
console.log("Requesting " + url);
console.log("Uploading update file " + update_file);

View File

@ -1,6 +1,6 @@
const installer = require("electron-installer-debian");
import * as packager from "./package";
import {parse_version, Version} from "../modules/shared/version";
import {parseVersion, Version} from "../modules/shared/version";
const package_path = "build/TeaClient-linux-x64/";
const filename_update = "TeaClient-linux-x64.tar.gz";
@ -42,7 +42,7 @@ if(process.argv.length < 3) {
let version: Version;
const alive = setInterval(() => {}, 1000);
packager.pack_info(package_path).then(package_info => {
options.options.version = (version = parse_version(package_info["version"])).toString();
options.options.version = (version = parseVersion(package_info["version"])).toString();
options.dest = "build/output/" + process.argv[2] + "/" + options.options.version + "/";
console.log('Creating package for version ' + options.options.version + ' (this may take a while)');

View File

@ -1,7 +1,7 @@
import * as packager from "./package";
import * as deployer from "./deploy";
import * as glob from "glob";
import {parse_version, Version} from "../modules/shared/version";
import {parseVersion, Version} from "../modules/shared/version";
const fs = require("fs-extra");
const path = require("path");
@ -89,7 +89,8 @@ packager.pack_info(package_path).then(async info => {
return info;
}).then(async _info => {
info = _info;
version = parse_version(_info["version"]);
version = parseVersion(_info["version"]);
version.timestamp = Date.now();
dest_path = "build/output/" + process.argv[2] + "/" + version.toString() + "/";
await packager.pack_update(package_path, dest_path + "TeaClient-windows-x64.tar.gz");
}).then(async () => {
@ -102,17 +103,22 @@ packager.pack_info(package_path).then(async info => {
console.log("Deploying PDB files");
const files = [];
for(const file of await fs.readdir(symbol_binary_path)) {
if(!file.endsWith(".node"))
if(!file.endsWith(".node")) {
continue;
}
let file_name = path.basename(file);
if(file_name.endsWith(".node"))
if(file_name.endsWith(".node")) {
file_name = file_name.substr(0, file_name.length - 5);
}
const binary_path = path.join(symbol_binary_path, file);
const pdb_path = path.join(symbol_pdb_path, file_name + ".pdb");
if(!fs.existsSync(pdb_path)) {
console.warn("Missing PDB file for binary %s", file);
continue;
}
files.push({
binary: binary_path,
pdb: pdb_path

View File

@ -68,19 +68,19 @@ function compile_native() {
eval ${_command}
check_err_exit ${project_name} "Failed create build targets!"
cmake --build `pwd` --target update_installer -- ${CMAKE_MAKE_OPTIONS}
cmake --build "$(pwd)" --target update_installer -- ${CMAKE_MAKE_OPTIONS}
check_err_exit ${project_name} "Failed build teaclient update installer!"
cmake --build `pwd` --target teaclient_connection -- ${CMAKE_MAKE_OPTIONS}
cmake --build "$(pwd)" --target teaclient_connection -- ${CMAKE_MAKE_OPTIONS}
check_err_exit ${project_name} "Failed build teaclient connection!"
cmake --build `pwd` --target teaclient_crash_handler -- ${CMAKE_MAKE_OPTIONS}
cmake --build "$(pwd)" --target teaclient_crash_handler -- ${CMAKE_MAKE_OPTIONS}
check_err_exit ${project_name} "Failed build teaclient crash handler!"
cmake --build `pwd` --target teaclient_ppt -- ${CMAKE_MAKE_OPTIONS}
cmake --build "$(pwd)" --target teaclient_ppt -- ${CMAKE_MAKE_OPTIONS}
check_err_exit ${project_name} "Failed build teaclient ppt!"
cmake --build `pwd` --target teaclient_dns -- ${CMAKE_MAKE_OPTIONS}
cmake --build "$(pwd)" --target teaclient_dns -- ${CMAKE_MAKE_OPTIONS}
check_err_exit ${project_name} "Failed to build teaclient dns!"
end_task "${project_name}_native" "Native extensions compiled"
@ -89,10 +89,10 @@ function compile_native() {
function package_client() {
begin_task "${project_name}_package" "Packaging client"
if [[ ${build_os_type} == "win32" ]]; then
npm run build-windows-64
npm run build-windows-64 "${teaclient_deploy_channel}"
check_err_exit ${project_name} "Failed to package client!"
else
npm run build-linux-64
npm run build-linux-64 "${teaclient_deploy_channel}"
check_err_exit ${project_name} "Failed to package client!"
fi
end_task "${project_name}_package" "Client package created"
@ -110,10 +110,10 @@ function deploy_client() {
}
if [[ ${build_os_type} == "win32" ]]; then
npm run package-windows-64 ${teaclient_deploy_channel}
npm run package-windows-64 "${teaclient_deploy_channel}"
check_err_exit ${project_name} "Failed to deploying client!"
else
npm run package-linux-64 ${teaclient_deploy_channel}
npm run package-linux-64 "${teaclient_deploy_channel}"
check_err_exit ${project_name} "Failed to deploying client!"
fi
end_task "${project_name}_package" "Client successfully deployed!"

View File

@ -46,8 +46,9 @@ if(process_arguments.length > 0 && process_arguments[0] === "crash-handler") {
setTimeout(() => app.exit(0), 2000);
} else {
if(process_arguments.length > 0 && process_arguments[0] == "--main-crash-handler")
if(process_arguments.length > 0 && process_arguments[0] == "--main-crash-handler") {
crash_handler.initialize_handler("main", is_electron_run);
}
/* app execute */
{

View File

@ -1,8 +1,8 @@
import {app} from "electron";
import {app, BrowserWindow} from "electron";
import * as crash_handler from "../crash_handler";
import * as loader from "./ui-loader/graphical";
let appReferences = 0;
let windowOpen = false;
/**
* Normally the app closes when all windows have been closed.
@ -17,9 +17,9 @@ export function dereferenceApp() {
testAppState();
}
function testAppState() {
if(appReferences > 0) { return; }
if(windowOpen) { return; }
console.log("All windows have been closed, closing app.");
app.quit();
@ -29,16 +29,20 @@ function initializeAppListeners() {
app.on('quit', () => {
console.debug("Shutting down app.");
crash_handler.finalize_handler();
loader.ui.cleanup();
console.log("App has been finalized.");
});
app.on('window-all-closed', () => {
windowOpen = false;
console.log("All windows have been closed. Manual app reference count: %d", appReferences);
testAppState();
});
app.on("browser-window-created", () => {
windowOpen = true;
})
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.

View File

@ -0,0 +1,21 @@
export interface AppInfoFile {
version: 2,
clientVersion: {
major: number,
minor: number,
patch: number,
buildIndex: number,
timestamp: number
},
/* The channel where the client has been downloaded from */
clientChannel: string,
/* The channel where UI - Packs should be downloaded from */
uiPackChannel: string
}
export default AppInfoFile;

View File

@ -0,0 +1,74 @@
/* tslint:disable */
// generated by typescript-json-validator
import {inspect} from 'util';
import Ajv = require('ajv');
import AppInfoFile from './AppInfoFile';
export const ajv = new Ajv({"allErrors":true,"coerceTypes":false,"format":"fast","nullable":true,"unicode":true,"uniqueItems":true,"useDefaults":true});
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'));
export {AppInfoFile};
export const AppVersionFileSchema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"defaultProperties": [
],
"properties": {
"clientVersion": {
"defaultProperties": [
],
"properties": {
"buildIndex": {
"type": "number"
},
"major": {
"type": "number"
},
"minor": {
"type": "number"
},
"patch": {
"type": "number"
},
"timestamp": {
"type": "number"
}
},
"required": [
"buildIndex",
"major",
"minor",
"patch",
"timestamp"
],
"type": "object"
},
"uiPackChannel": {
"type": "string"
},
"version": {
"enum": [
2
],
"type": "number"
}
},
"required": [
"clientVersion",
"uiPackChannel",
"version"
],
"type": "object"
};
export type ValidateFunction<T> = ((data: unknown) => data is T) & Pick<Ajv.ValidateFunction, 'errors'>
export const isAppVersionFile = ajv.compile(AppVersionFileSchema) as ValidateFunction<AppInfoFile>;
export default function validate(value: unknown): AppInfoFile {
if (isAppVersionFile(value)) {
return value;
} else {
throw new Error(
ajv.errorsText(isAppVersionFile.errors!.filter((e: any) => e.keyword !== 'if'), {dataVar: 'AppVersionFile'}) +
'\n\n' +
inspect(value),
);
}
}

View File

@ -0,0 +1,6 @@
export interface UpdateConfigFile {
version: number,
selectedChannel: string
}
export default UpdateConfigFile;

View File

@ -0,0 +1,41 @@
/* tslint:disable */
// generated by typescript-json-validator
import {inspect} from 'util';
import Ajv = require('ajv');
import UpdateConfigFile from './UpdateConfigFile';
export const ajv = new Ajv({"allErrors":true,"coerceTypes":false,"format":"fast","nullable":true,"unicode":true,"uniqueItems":true,"useDefaults":true});
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'));
export {UpdateConfigFile};
export const UpdateConfigFileSchema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"defaultProperties": [
],
"properties": {
"selectedChannel": {
"type": "string"
},
"version": {
"type": "number"
}
},
"required": [
"selectedChannel",
"version"
],
"type": "object"
};
export type ValidateFunction<T> = ((data: unknown) => data is T) & Pick<Ajv.ValidateFunction, 'errors'>
export const isUpdateConfigFile = ajv.compile(UpdateConfigFileSchema) as ValidateFunction<UpdateConfigFile>;
export default function validate(value: unknown): UpdateConfigFile {
if (isUpdateConfigFile(value)) {
return value;
} else {
throw new Error(
ajv.errorsText(isUpdateConfigFile.errors!.filter((e: any) => e.keyword !== 'if'), {dataVar: 'UpdateConfigFile'}) +
'\n\n' +
inspect(value),
);
}
}

View File

@ -1,43 +1,50 @@
import {BrowserWindow} from "electron";
import {BrowserWindow, dialog} from "electron";
import * as electron from "electron";
import * as path from "path";
import * as url from "url";
let changelog_window: BrowserWindow;
export function open() {
if(changelog_window) {
changelog_window.focus();
let changeLogWindow: BrowserWindow;
export function openChangeLog() {
if(changeLogWindow) {
changeLogWindow.focus();
return;
}
changelog_window = new BrowserWindow({
changeLogWindow = new BrowserWindow({
show: false
});
changelog_window.setMenu(null);
changeLogWindow.setMenu(null);
let file = "";
let file;
{
const app_path = electron.app.getAppPath();
if(app_path.endsWith(".asar"))
file = path.join(path.dirname(app_path), "..", "ChangeLog.txt");
else
file = path.join(app_path, "github", "ChangeLog.txt"); /* We've the source master :D */
const appPath = electron.app.getAppPath();
if(appPath.endsWith(".asar")) {
file = path.join(path.dirname(appPath), "..", "ChangeLog.txt");
} else {
file = path.join(appPath, "github", "ChangeLog.txt"); /* We've the source ;) */
}
}
changelog_window.loadURL(url.pathToFileURL(file).toString());
changelog_window.setTitle("TeaClient ChangeLog");
changelog_window.on('ready-to-show', () => {
changelog_window.show();
changeLogWindow.loadURL(url.pathToFileURL(file).toString()).catch(error => {
console.error("Failed to open changelog: %o", error);
dialog.showErrorBox("Failed to open the ChangeLog", "Failed to open the changelog file.\nLookup the console for more details.");
closeChangeLog();
});
changelog_window.on('close', () => {
changelog_window = undefined;
changeLogWindow.setTitle("TeaClient ChangeLog");
changeLogWindow.on('ready-to-show', () => {
changeLogWindow.show();
});
changeLogWindow.on('close', () => {
changeLogWindow = undefined;
});
}
export function close() {
if(changelog_window) {
changelog_window.close();
changelog_window = undefined;
export function closeChangeLog() {
if(changeLogWindow) {
changeLogWindow.close();
changeLogWindow = undefined;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,134 +1,38 @@
import {BrowserWindow, app, dialog} from "electron";
import {BrowserWindow, app, dialog, MessageBoxOptions} from "electron";
import * as path from "path";
export let is_debug: boolean;
export let allow_dev_tools: boolean;
import {Arguments, processArguments} from "../../shared/process-arguments";
import * as updater from "./../app-updater";
import * as loader from "./../ui-loader";
import * as url from "url";
import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
import {referenceApp, dereferenceApp} from "../AppInstance";
import {closeURLPreview, openURLPreview} from "../url-preview";
import {
getLoaderWindow,
hideAppLoaderWindow,
setAppLoaderStatus,
showAppLoaderWindow
} from "../windows/app-loader/controller/AppLoader";
import {loadUiPack} from "../ui-loader/Loader";
import {loadLocalUiCache} from "../ui-loader/Cache";
import {showMainWindow} from "../windows/main-window/controller/MainWindow";
import {showUpdateWindow} from "../windows/client-updater/controller/ClientUpdate";
import {
clientUpdateChannel,
currentClientVersion,
availableClientUpdate,
setClientUpdateChannel,
initializeAppUpdater
} from "../app-updater";
import * as app_updater from "../app-updater";
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
export let mainWindow: BrowserWindow = null;
function spawnMainWindow(rendererEntryPoint: string) {
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
console.log("Allowing untrusted certificate for %o", url);
event.preventDefault();
callback(true);
});
// Create the browser window.
console.log("Spawning main window");
referenceApp(); /* main browser window references the app */
mainWindow = new BrowserWindow({
width: 800,
height: 600,
minHeight: 600,
minWidth: 600,
show: false,
webPreferences: {
webSecurity: false,
nodeIntegrationInWorker: true,
nodeIntegration: true,
preload: path.join(__dirname, "preload.js")
},
icon: path.join(__dirname, "..", "..", "resources", "logo.ico"),
});
mainWindow.webContents.on('devtools-closed', () => {
console.log("Dev tools destroyed!");
});
mainWindow.on('closed', () => {
app.releaseSingleInstanceLock();
closeURLPreview().then(undefined);
mainWindow = null;
dereferenceApp();
});
mainWindow.loadURL(url.pathToFileURL(loader.ui.preloading_page(rendererEntryPoint)).toString()).catch(error => {
console.error("Failed to load UI entry point: %o", error);
handleUILoadingError("UI entry point failed to load");
});
mainWindow.once('ready-to-show', () => {
mainWindow.show();
loadWindowBounds('main-window', mainWindow).then(() => {
startTrackWindowBounds('main-window', mainWindow);
mainWindow.focus();
loader.ui.cleanup();
if(allow_dev_tools && !mainWindow.webContents.isDevToolsOpened())
mainWindow.webContents.openDevTools();
});
});
mainWindow.webContents.on('new-window', (event, url_str, frameName, disposition, options, additionalFeatures) => {
if(frameName.startsWith("__modal_external__")) {
return;
}
event.preventDefault();
try {
let url: URL;
try {
url = new URL(url_str);
} catch(error) {
throw "failed to parse URL";
}
{
let protocol = url.protocol.endsWith(":") ? url.protocol.substring(0, url.protocol.length - 1) : url.protocol;
if(protocol !== "https" && protocol !== "http") {
throw "invalid protocol (" + protocol + "). HTTP(S) are only supported!";
}
}
console.log("Got new window " + frameName);
openURLPreview(url_str).then(() => {});
} catch(error) {
console.error("Failed to open preview window for URL %s: %o", url_str, error);
dialog.showErrorBox("Failed to open preview", "Failed to open preview URL: " + url_str + "\nError: " + error);
}
});
mainWindow.webContents.on('crashed', () => {
console.error("UI thread crashed! Closing app!");
if(!processArguments.has_flag(Arguments.DEBUG)) {
mainWindow.close();
}
});
}
function handleUILoadingError(message: string) {
referenceApp();
console.log("Caught loading error: %s", message);
if(mainWindow) {
mainWindow.close();
mainWindow = undefined;
}
dialog.showMessageBox({
type: "error",
buttons: ["exit"],
title: "A critical error happened while loading TeaClient!",
message: (message || "no error").toString()
}).then(dereferenceApp);
loader.ui.cancel();
}
export function execute() {
export async function execute() {
console.log("Main app executed!");
is_debug = processArguments.has_flag(...Arguments.DEBUG);
@ -138,35 +42,84 @@ export function execute() {
console.log("Arguments: %o", processArguments);
}
setAppLoaderStatus("Bootstrapping", 0);
await showAppLoaderWindow();
await initializeAppUpdater();
/* TODO: Remove this (currently required somewhere within the renderer) */
const version = await app_updater.currentClientVersion();
global["app_version_client"] = version.toString();
/* FIXME! */
await showUpdateWindow();
return;
setAppLoaderStatus("Checking for updates", .1);
try {
if(processArguments.has_value(Arguments.UPDATER_CHANNEL)) {
setClientUpdateChannel(processArguments.value(Arguments.UPDATER_CHANNEL));
}
const newVersion = await availableClientUpdate();
if(newVersion) {
setAppLoaderStatus("Awaiting update", .15);
const result = await dialog.showMessageBox(getLoaderWindow(), {
buttons: ["Update now", "No thanks"],
title: "Update available!",
message:
"There is an update available!\n" +
"Should we update now?\n" +
"\n" +
"Current version: " + (await currentClientVersion()).toString() + "\n" +
"Target version: " + newVersion.version.toString(true)
} as MessageBoxOptions);
if(result.response === 0) {
/* TODO: Execute update! */
await showUpdateWindow();
hideAppLoaderWindow();
return;
}
}
} catch (error) {
console.warn("Failed to check for a client update: %o", error);
}
setAppLoaderStatus("Initialize backend", .2);
console.log("Setting up render backend");
require("../render-backend");
console.log("Spawn loading screen");
loader.ui.execute_loader().then(async (entry_point: string) => {
/* test if the updater may have an update found */
let awaiting_update_set = false;
while(updater.update_question_open) {
if(!awaiting_update_set) {
awaiting_update_set = true;
loader.ui.show_await_update();
console.log("Awaiting update stuff to be finished");
}
await new Promise(resolve => setTimeout(resolve, 100));
let uiEntryPoint;
try {
setAppLoaderStatus("Loading ui cache", .25);
await loadLocalUiCache();
uiEntryPoint = await loadUiPack((message, index) => {
setAppLoaderStatus(message, index * .75 + .25);
});
} catch (error) {
hideAppLoaderWindow();
console.error("Failed to load ui: %o", error);
if(mainWindow) {
mainWindow.close();
mainWindow = undefined;
}
if(updater.update_restart_pending)
return undefined;
await dialog.showMessageBox({
type: "error",
buttons: ["exit"],
title: "A critical error happened while loading TeaClient!",
message: (error || "no error").toString()
});
return;
}
return entry_point;
}).then((entry_point: string) => {
referenceApp(); /* because we've no windows when we close the loader UI */
loader.ui.cleanup(); /* close the window */
if(!uiEntryPoint) {
throw "missing ui entry point";
}
if(entry_point) //has not been canceled
spawnMainWindow(entry_point);
else {
handleUILoadingError("Missing UI entry point");
}
dereferenceApp();
}).catch(handleUILoadingError);
setAppLoaderStatus("Starting client", 100);
await showMainWindow(uiEntryPoint);
hideAppLoaderWindow();
}

View File

@ -1,26 +1,23 @@
import * as electron from "electron";
import * as app_updater from "./app-updater";
import {app, Menu} from "electron";
import MessageBoxOptions = electron.MessageBoxOptions;
import {processArguments, parseProcessArguments, Arguments} from "../shared/process-arguments";
import {open as open_changelog} from "./app-updater/changelog";
import {openChangeLog as openChangeLog} from "./app-updater/changelog";
import * as crash_handler from "../crash_handler";
import {initializeSingleInstance} from "./MultiInstanceHandler";
import "./AppInstance";
import {dereferenceApp, referenceApp} from "./AppInstance";
import {showUpdateWindow} from "./windows/client-updater/controller/ClientUpdate";
async function handleAppReady() {
Menu.setApplicationMenu(null);
if(processArguments.has_value("update-execute")) {
console.log("Executing update " + processArguments.value("update-execute"));
await app_updater.execute_update(processArguments.value("update-execute"), callback => {
console.log("Update preconfig successful. Extracting update. (The client should start automatically)");
app.quit();
setImmediate(callback);
});
console.log("Showing update window");
await showUpdateWindow();
return;
} else if(processArguments.has_value("update-failed-new") || processArguments.has_value("update-succeed-new")) {
const success = processArguments.has_value("update-succeed-new");
@ -58,7 +55,7 @@ async function handleAppReady() {
})[] = [];
if(success) {
open_changelog();
openChangeLog();
type = "info";
title = "Update succeeded!";
@ -99,7 +96,7 @@ async function handleAppReady() {
buttons.push({
key: "Retry update",
callback: async () => {
await app_updater.execute_graphical(await app_updater.selected_channel(), false);
await showUpdateWindow();
return true;
}
});
@ -138,13 +135,11 @@ async function handleAppReady() {
}
try {
const version = await app_updater.current_version();
global["app_version_client"] = version.toString();
const main = require("./main-window");
main.execute();
app_updater.start_auto_update_check();
referenceApp();
await main.execute();
dereferenceApp();
} catch (error) {
console.error(error);
await electron.dialog.showMessageBox({

View File

@ -4,12 +4,13 @@ import * as electron from "electron";
import ipcMain = electron.ipcMain;
import BrowserWindow = electron.BrowserWindow;
import {open as open_changelog} from "../app-updater/changelog";
import {openChangeLog as open_changelog} from "../app-updater/changelog";
import * as updater from "../app-updater";
import {execute_connect_urls} from "../MultiInstanceHandler";
import {processArguments} from "../../shared/process-arguments";
import "./ExternalModal";
import {showUpdateWindow} from "../windows/client-updater/controller/ClientUpdate";
ipcMain.on('basic-action', (event, action, ...args: any[]) => {
const window = BrowserWindow.fromWebContents(event.sender);
@ -19,7 +20,7 @@ ipcMain.on('basic-action', (event, action, ...args: any[]) => {
} else if(action === "open-changelog") {
open_changelog();
} else if(action === "check-native-update") {
updater.selected_channel().then(channel => updater.execute_graphical(channel, true));
showUpdateWindow().then(undefined);
} else if(action === "open-dev-tools") {
window.webContents.openDevTools();
} else if(action === "reload-window") {

View File

@ -0,0 +1,97 @@
import * as path from "path";
import * as fs from "fs-extra";
import * as electron from "electron";
import validateCacheFile from "./CacheFile.validator";
import CacheFile, {UIPackInfo} from "./CacheFile";
let localUiCacheInstance: CacheFile;
async function doLoad() {
const file = path.join(uiCachePath(), "data.json");
if(!(await fs.pathExists(file))) {
console.debug("Missing UI cache file. Creating a new one.");
/* we've no cache */
return;
}
const anyData = await fs.readJSON(file);
try {
if(anyData["version"] !== 3) {
throw "unsupported version " + anyData["version"];
}
localUiCacheInstance = validateCacheFile(anyData);
} catch (error) {
if(error?.message?.startsWith("CacheFile")) {
/* We have no need to fully print the read data */
error = "\n- " + error.message.split("\n")[0].split(", ").join("\n- ");
} else if(error?.message) {
error = error.message;
} else if(typeof error !== "string") {
console.error(error);
}
console.warn("Current Ui cache file seems to be invalid. Renaming it and creating a new one: %s", error);
try {
await fs.rename(file, path.join(uiCachePath(), "data.json." + Date.now()));
} catch (error) {
console.warn("Failed to invalidate old ui cache file: %o", error);
}
}
}
/**
* Will not throw or return undefined!
*/
export async function loadLocalUiCache() {
if(localUiCacheInstance) {
throw "ui cache has already been loaded";
}
try {
await doLoad();
} catch (error) {
console.warn("Failed to load UI cache file: %o. This will cause loss of the file content.", error);
}
if(!localUiCacheInstance) {
localUiCacheInstance = {
version: 3,
cachedPacks: []
}
}
}
export function localUiCache() : CacheFile {
if(typeof localUiCacheInstance !== "object") {
throw "missing local ui cache";
}
return localUiCacheInstance;
}
/**
* Will not throw anything
*/
export async function saveLocalUiCache() {
const file = path.join(uiCachePath(), "data.json");
try {
if(!(await fs.pathExists(path.dirname(file)))) {
await fs.mkdirs(path.dirname(file));
}
await fs.writeJson(file, localUiCacheInstance);
} catch (error) {
console.error("Failed to save UI cache file. This will may cause some data loss: %o", error);
}
}
export function uiCachePath() {
return path.join(electron.app.getPath('userData'), "cache", "ui");
}
export function uiPackCachePath(version: UIPackInfo) : string {
return path.join(uiCachePath(), version.channel + "_" + version.versions_hash + "_" + version.timestamp + ".tar.gz");
}

View File

@ -0,0 +1,35 @@
export interface CacheFile {
version: number; /* currently 2 */
cachedPacks: CachedUIPack[];
}
export interface UIPackInfo {
timestamp: number; /* build timestamp */
version: string; /* not really used anymore */
versions_hash: string; /* used, identifies the version. Its the git hash. */
channel: string;
requiredClientVersion: string; /* minimum version from the client required for the pack */
}
export interface CachedUIPack {
downloadTimestamp: number;
localFilePath: string;
localChecksum: string | "none"; /* sha512 of the locally downloaded file. */
//TODO: Get the remote checksum and compare them instead of the local one
packInfo: UIPackInfo;
status: {
type: "valid"
} | {
type: "invalid",
timestamp: number,
reason: string
}
}
export default CacheFile;

View File

@ -0,0 +1,145 @@
/* tslint:disable */
// generated by typescript-json-validator
import {inspect} from 'util';
import Ajv = require('ajv');
import CacheFile from './CacheFile';
export const ajv = new Ajv({"allErrors":true,"coerceTypes":false,"format":"fast","nullable":true,"unicode":true,"uniqueItems":true,"useDefaults":true});
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'));
export {CacheFile};
export const CacheFileSchema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"defaultProperties": [
],
"definitions": {
"CachedUIPack": {
"defaultProperties": [
],
"properties": {
"downloadTimestamp": {
"type": "number"
},
"localChecksum": {
"type": "string"
},
"localFilePath": {
"type": "string"
},
"packInfo": {
"$ref": "#/definitions/UIPackInfo"
},
"status": {
"anyOf": [
{
"defaultProperties": [
],
"properties": {
"type": {
"enum": [
"valid"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"defaultProperties": [
],
"properties": {
"reason": {
"type": "string"
},
"timestamp": {
"type": "number"
},
"type": {
"enum": [
"invalid"
],
"type": "string"
}
},
"required": [
"reason",
"timestamp",
"type"
],
"type": "object"
}
]
}
},
"required": [
"downloadTimestamp",
"localChecksum",
"localFilePath",
"packInfo",
"status"
],
"type": "object"
},
"UIPackInfo": {
"defaultProperties": [
],
"properties": {
"channel": {
"type": "string"
},
"requiredClientVersion": {
"type": "string"
},
"timestamp": {
"type": "number"
},
"version": {
"type": "string"
},
"versions_hash": {
"type": "string"
}
},
"required": [
"channel",
"requiredClientVersion",
"timestamp",
"version",
"versions_hash"
],
"type": "object"
}
},
"properties": {
"cachedPacks": {
"items": {
"$ref": "#/definitions/CachedUIPack"
},
"type": "array"
},
"version": {
"type": "number"
}
},
"required": [
"cachedPacks",
"version"
],
"type": "object"
};
export type ValidateFunction<T> = ((data: unknown) => data is T) & Pick<Ajv.ValidateFunction, 'errors'>
export const isCacheFile = ajv.compile(CacheFileSchema) as ValidateFunction<CacheFile>;
export default function validate(value: unknown): CacheFile {
if (isCacheFile(value)) {
return value;
} else {
throw new Error(
ajv.errorsText(isCacheFile.errors!.filter((e: any) => e.keyword !== 'if'), {dataVar: 'CacheFile'}) +
'\n\n' +
inspect(value),
);
}
}

View File

@ -0,0 +1,322 @@
import {is_debug} from "../main-window";
import * as moment from "moment";
import * as fs from "fs-extra";
import * as os from "os";
import * as path from "path";
import * as zlib from "zlib";
import * as tar from "tar-stream";
import {Arguments, processArguments} from "../../shared/process-arguments";
import {parseVersion} from "../../shared/version";
import * as electron from "electron";
import MessageBoxOptions = Electron.MessageBoxOptions;
import {clientAppInfo, currentClientVersion, executeGraphicalClientUpdate} from "../app-updater";
import {CachedUIPack, UIPackInfo} from "./CacheFile";
import {localUiCache, saveLocalUiCache} from "./Cache";
import {shippedClientUi} from "./Shipped";
import {downloadUiPack, queryRemoteUiPacks} from "./Remote";
import * as url from "url";
export const remoteUiUrl = () => {
const default_path = is_debug ? "http://localhost/home/TeaSpeak/Web-Client/client-api/environment/" : "https://clientapi.teaspeak.de/";
return processArguments.has_value(...Arguments.SERVER_URL) ? processArguments.value(...Arguments.SERVER_URL) : default_path;
};
let temporaryDirectoryPromise: Promise<string>;
function generateTemporaryDirectory() : Promise<string> {
if(temporaryDirectoryPromise) {
return temporaryDirectoryPromise;
}
return (temporaryDirectoryPromise = fs.mkdtemp(path.join(os.tmpdir(), "TeaClient-")).then(path => {
process.on('exit', () => {
try {
if(fs.pathExistsSync(path)) {
fs.removeSync(path);
}
} catch (e) {
console.warn("Failed to delete temp directory: %o", e);
}
});
global["browser-root"] = path;
console.log("Local browser path: %s", path);
return path;
}));
}
async function unpackLocalUiPack(version: CachedUIPack) : Promise<string> {
const targetDirectory = await generateTemporaryDirectory();
if(!await fs.pathExists(targetDirectory)) {
throw "failed to create temporary directory";
}
const gunzip = zlib.createGunzip();
const extract = tar.extract();
let fpipe: fs.ReadStream;
try {
fpipe = fs.createReadStream(version.localFilePath);
} catch (error) {
console.error("Failed to open UI pack at %s: %o", version.localFilePath, error);
throw "failed to open UI pack";
}
extract.on('entry', function(header: tar.Headers, stream, next) {
if(header.type == 'file') {
const targetFile = path.join(targetDirectory, header.name);
if(!fs.existsSync(path.dirname(targetFile))) {
fs.mkdirsSync(path.dirname(targetFile));
}
stream.on('end', () => setImmediate(next));
const wfpipe = fs.createWriteStream(targetFile);
stream.pipe(wfpipe);
} else if(header.type == 'directory') {
if(fs.existsSync(path.join(targetDirectory, header.name))) {
setImmediate(next);
}
fs.mkdirs(path.join(targetDirectory, header.name)).catch(error => {
console.warn("Failed to create unpacking dir " + path.join(targetDirectory, header.name));
console.error(error);
}).then(() => setImmediate(next));
} else {
console.warn("Invalid ui tar ball entry type (" + header.type + ")");
return;
}
});
const finishPromise = new Promise((resolve, reject) => {
gunzip.on('error', event => {
reject(event);
});
extract.on('finish', resolve);
extract.on('error', event => {
if(!event) return;
reject(event);
});
fpipe.pipe(gunzip).pipe(extract);
});
try {
await finishPromise;
} catch(error) {
console.error("Failed to extract UI files to %s: %o", targetDirectory, error);
throw "failed to unpack the UI pack";
}
return targetDirectory;
}
async function streamFilesFromDevServer(_channel: string, _callbackStatus: (message: string, index: number) => any) : Promise<string> {
return remoteUiUrl() + "index.html";
}
async function loadBundledUiPack(channel: string, callbackStatus: (message: string, index: number) => any) : Promise<string> {
callbackStatus("Query local UI pack info", .33);
const bundledUi = await shippedClientUi();
if(!bundledUi) {
throw "client has no bundled UI pack";
}
callbackStatus("Unpacking bundled UI", .66);
const result = await unpackLocalUiPack(bundledUi);
callbackStatus("Local UI pack loaded", 1);
console.log("Loaded bundles UI pack successfully. Version: {timestamp: %d, hash: %s}", bundledUi.packInfo.timestamp, bundledUi.packInfo.versions_hash);
return url.pathToFileURL(path.join(result, "index.html")).toString();
}
async function loadCachedOrRemoteUiPack(channel: string, callbackStatus: (message: string, index: number) => any, ignoreNewVersionTimestamp: boolean) : Promise<string> {
callbackStatus("Fetching info", 0);
const bundledUi = await shippedClientUi();
const clientVersion = await currentClientVersion();
let availableCachedVersions: CachedUIPack[] = localUiCache().cachedPacks.filter(e => {
if(e.status.type !== "valid") {
return false;
}
if(bundledUi) {
/* remove all cached ui packs which are older than our bundled one */
if(e.packInfo.timestamp <= bundledUi.downloadTimestamp) {
return false;
}
}
if(e.packInfo.channel !== channel) {
/* ui-pack is for another channel */
return false;
}
const requiredVersion = parseVersion(e.packInfo.requiredClientVersion);
return clientVersion.isDevelopmentVersion() || clientVersion.newerThan(requiredVersion) || clientVersion.equals(requiredVersion);
});
if(processArguments.has_flag(Arguments.UPDATER_UI_NO_CACHE)) {
console.log("Ignoring local UI cache");
availableCachedVersions = [];
}
let remoteVersionDropped = false;
/* fetch the remote versions */
executeRemoteLoader: {
callbackStatus("Loading remote info", .25);
let remoteVersions: UIPackInfo[];
try {
remoteVersions = await queryRemoteUiPacks();
} catch (error) {
console.error("Failed to query remote UI packs: %o", error);
break executeRemoteLoader;
}
callbackStatus("Parsing remote UI packs", .40);
const remoteVersion = remoteVersions.find(e => e.channel === channel);
if(!remoteVersion) {
console.info("Remote server has no ui packs for channel %o.", channel);
break executeRemoteLoader;
}
let newestLocalVersion = availableCachedVersions.map(e => e.packInfo.timestamp)
.reduce((a, b) => Math.max(a, b), bundledUi ? bundledUi.downloadTimestamp : 0);
console.log("Remote version %d, Local version %d", remoteVersion.timestamp, newestLocalVersion);
const requiredClientVersion = parseVersion(remoteVersion.requiredClientVersion);
if(requiredClientVersion.newerThan(clientVersion) && !is_debug) {
const result = await electron.dialog.showMessageBox({
type: "question",
message:
"Your client is outdated.\n" +
"Newer UI packs requires client " + remoteVersion.requiredClientVersion + "\n" +
"Do you want to update your client?",
title: "Client outdated!",
buttons: ["Update client", availableCachedVersions.length === 0 ? "Close client" : "Ignore and use last possible"]
} as MessageBoxOptions);
if(result.response == 0) {
if(!(await executeGraphicalClientUpdate(channel, false))) {
throw "Client outdated an suitable UI pack version has not been found";
} else {
return;
}
} else if(availableCachedVersions.length === 0) {
electron.app.exit(1);
return;
}
} else if(remoteVersion.timestamp <= newestLocalVersion && !ignoreNewVersionTimestamp) {
/* We've already a equal or newer version. Don't use the remote version */
/* if remote is older than current bundled version its not a drop since it could be used as a fallback */
remoteVersionDropped = !!bundledUi && remoteVersion.timestamp > bundledUi.downloadTimestamp;
} else {
/* update is possible because the timestamp is newer than out latest local version */
try {
console.log("Downloading UI pack version (%d) %s. Forced: %s. Newest local version: %d", remoteVersion.timestamp,
remoteVersion.versions_hash, ignoreNewVersionTimestamp ? "true" : "false", newestLocalVersion);
callbackStatus("Downloading new UI pack", .55);
availableCachedVersions.push(await downloadUiPack(remoteVersion));
} catch (error) {
console.error("Failed to download new UI pack: %o", error);
}
}
}
callbackStatus("Unpacking UI", .70);
availableCachedVersions.sort((a, b) => a.packInfo.timestamp - b.packInfo.timestamp);
/* Only invalidate the version if any other succeeded to load else we might fucked up (no permission to write etc) */
let invalidatedVersions: CachedUIPack[] = [];
const doVersionInvalidate = async () => {
if(invalidatedVersions.length > 0) {
for(const version of invalidatedVersions) {
version.status = { type: "invalid", reason: "failed to unpack", timestamp: Date.now() };
}
await saveLocalUiCache();
}
};
while(availableCachedVersions.length > 0) {
const pack = availableCachedVersions.pop();
console.log("Trying to load UI pack from %s (%s). Downloaded at %s",
moment(pack.packInfo.timestamp).format("llll"), pack.packInfo.versions_hash,
moment(pack.downloadTimestamp).format("llll"));
try {
const target = await unpackLocalUiPack(pack);
callbackStatus("UI pack loaded", 1);
await doVersionInvalidate();
return url.pathToFileURL(path.join(target, "index.html")).toString();
} catch (error) {
invalidatedVersions.push(pack);
console.log("Failed to unpack UI pack: %o", error);
}
}
if(remoteVersionDropped) {
/* try again, but this time enforce a remote download */
const result = await loadCachedOrRemoteUiPack(channel, callbackStatus, true);
await doVersionInvalidate(); /* new UI pack seems to be successfully loaded */
return result; /* if not succeeded an exception will be thrown */
}
throw "Failed to load any UI pack (local and remote)\nView the console for more details.";
}
enum UILoaderMethod {
PACK = 0,
BUNDLED_PACK = 1,
/* RAW_FILES = 2, System deprecated */
DEVELOP_SERVER = 3
}
/**
* @param statisticsCallback
* @returns the url of the ui pack entry point
*/
export async function loadUiPack(statisticsCallback: (message: string, index: number) => any) : Promise<string> {
const channel = clientAppInfo().uiPackChannel;
let enforcedLoadingMethod = parseInt(processArguments.has_value(Arguments.UPDATER_UI_LOAD_TYPE) ? processArguments.value(Arguments.UPDATER_UI_LOAD_TYPE) : "-1") as UILoaderMethod;
if(typeof UILoaderMethod[enforcedLoadingMethod] !== "undefined") {
switch (enforcedLoadingMethod) {
case UILoaderMethod.PACK:
return await loadCachedOrRemoteUiPack(channel, statisticsCallback, false);
case UILoaderMethod.BUNDLED_PACK:
return await loadBundledUiPack(channel, statisticsCallback);
case UILoaderMethod.DEVELOP_SERVER:
return await streamFilesFromDevServer(channel, statisticsCallback);
default:
console.warn("Invalid ui loader type %o. Skipping loader enforcement.", enforcedLoadingMethod);
}
}
let firstError;
try {
return await loadCachedOrRemoteUiPack(channel, statisticsCallback, false);
} catch(error) {
console.warn("Failed to load cached/remote UI pack: %o", error);
firstError = firstError || error;
}
try {
return await loadBundledUiPack(channel, statisticsCallback);
} catch(error) {
console.warn("Failed to load bundles UI pack: %o", error);
firstError = firstError || error;
}
throw firstError;
}

View File

@ -0,0 +1,125 @@
import {CachedUIPack, UIPackInfo} from "./CacheFile";
import * as request from "request";
import {remoteUiUrl} from "./Loader";
import * as fs from "fs-extra";
import {WriteStream} from "fs";
import {localUiCache, saveLocalUiCache, uiPackCachePath} from "./Cache";
import * as querystring from "querystring";
import * as path from "path";
const kDownloadTimeout = 30_000;
export async function queryRemoteUiPacks() : Promise<UIPackInfo[]> {
const url = remoteUiUrl() + "api.php?" + querystring.stringify({
type: "ui-info"
});
console.debug("Loading UI pack information (URL: %s)", url);
let body = await new Promise<string>((resolve, reject) => request.get(url, { timeout: kDownloadTimeout }, (error, response, body: string) => {
if(error) {
reject(error);
} else if(!response) {
reject("missing response object");
} else if(response.statusCode !== 200) {
reject(response.statusCode + " " + response.statusMessage);
} else if(!body) {
reject("missing body in response");
} else {
resolve(body);
}
}));
let response;
try {
response = JSON.parse(body);
} catch (error) {
console.error("Received unparsable response for UI pack info. Response: %s", body);
throw "failed to parse response";
}
if(!response["success"]) {
throw "request failed: " + (response["msg"] || "unknown error");
}
if(!Array.isArray(response["versions"])) {
console.error("Response object misses 'versions' tag or has an invalid value. Object: %o", response);
throw "response contains invalid data";
}
let uiVersions: UIPackInfo[] = [];
for(const entry of response["versions"]) {
uiVersions.push({
channel: entry["channel"],
versions_hash: entry["git-ref"],
version: entry["version"],
timestamp: parseInt(entry["timestamp"]) * 1000, /* server provices that stuff in seconds */
requiredClientVersion: entry["required_client"]
});
}
return uiVersions;
}
export async function downloadUiPack(version: UIPackInfo) : Promise<CachedUIPack> {
const targetFile = uiPackCachePath(version);
if(await fs.pathExists(targetFile)) {
try {
await fs.remove(targetFile);
} catch (error) {
console.error("Tried to download UI version %s, but we failed to delete the old file: %o", version.versions_hash, error);
throw "failed to remove the old file";
}
}
try {
await fs.mkdirp(path.dirname(targetFile));
} catch (error) {
console.error("Failed to create target UI pack download directory at %s: %o", path.dirname(targetFile), error);
throw "failed to create target directories";
}
await new Promise((resolve, reject) => {
let fstream: WriteStream;
try {
request.get(remoteUiUrl() + "api.php?" + querystring.stringify({
"type": "ui-download",
"git-ref": version.versions_hash,
"version": version.version,
"timestamp": Math.floor(version.timestamp / 1000), /* remote server has only the timestamp in seconds*/
"channel": version.channel
}), {
timeout: kDownloadTimeout
}).on('response', function(response: request.Response) {
if(response.statusCode != 200)
reject(response.statusCode + " " + response.statusMessage);
}).on('error', error => {
reject(error);
}).pipe(fstream = fs.createWriteStream(targetFile)).on('finish', () => {
try { fstream.close(); } catch (e) { }
resolve();
});
} catch (error) {
try { fstream.close(); } catch (e) { }
reject(error);
}
});
try {
const cache = await localUiCache();
const info: CachedUIPack = {
packInfo: version,
localFilePath: targetFile,
localChecksum: "none", //TODO!
status: { type: "valid" },
downloadTimestamp: Date.now()
};
cache.cachedPacks.push(info);
await saveLocalUiCache();
return info;
} catch (error) {
console.error("Failed to register downloaded UI pack to the UI cache: %o", error);
throw "failed to register downloaded UI pack to the UI cache";
}
}

View File

View File

@ -0,0 +1,52 @@
import {CachedUIPack} from "./CacheFile";
import * as fs from "fs-extra";
import * as path from "path";
import validate from "./ShippedFileInfo.validator";
import {app} from "electron";
async function doQueryShippedUi() {
const appPath = app.getAppPath();
if(!appPath.endsWith(".asar")) {
return undefined;
}
const basePath = path.join(path.dirname(appPath), "ui");
//console.debug("Looking for client shipped UI pack at %s", basePath);
if(!(await fs.pathExists(basePath))) {
return undefined;
}
const info = validate(await fs.readJson(path.join(basePath, "bundled-ui.json")));
return {
downloadTimestamp: info.timestamp * 1000,
status: { type: "valid" },
localChecksum: "none",
localFilePath: path.join(path.join(path.dirname(appPath), "ui"), info.filename),
packInfo: {
channel: info.channel,
requiredClientVersion: info.required_client,
timestamp: info.timestamp * 1000,
version: info.version,
versions_hash: info.git_hash
}
};
}
let queryPromise: Promise<CachedUIPack | undefined>;
/**
* This function will not throw.
*
* @returns the shipped client ui.
* Will return undefined if no UI has been shipped or it's an execution from source.
*/
export async function shippedClientUi() : Promise<CachedUIPack | undefined> {
if(queryPromise) {
return queryPromise;
}
return (queryPromise = doQueryShippedUi().catch(error => {
console.warn("Failed to query shipped client ui: %o", error);
return undefined;
}));
}

View File

@ -0,0 +1,10 @@
export interface ShippedFileInfo {
channel: string,
version: string,
git_hash: string,
required_client: string,
timestamp: number,
filename: string
}
export default ShippedFileInfo;

View File

@ -0,0 +1,57 @@
/* tslint:disable */
// generated by typescript-json-validator
import {inspect} from 'util';
import Ajv = require('ajv');
import ShippedFileInfo from './ShippedFileInfo';
export const ajv = new Ajv({"allErrors":true,"coerceTypes":false,"format":"fast","nullable":true,"unicode":true,"uniqueItems":true,"useDefaults":true});
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'));
export {ShippedFileInfo};
export const ShippedFileInfoSchema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"defaultProperties": [
],
"properties": {
"channel": {
"type": "string"
},
"filename": {
"type": "string"
},
"git_hash": {
"type": "string"
},
"required_client": {
"type": "string"
},
"timestamp": {
"type": "number"
},
"version": {
"type": "string"
}
},
"required": [
"channel",
"filename",
"git_hash",
"required_client",
"timestamp",
"version"
],
"type": "object"
};
export type ValidateFunction<T> = ((data: unknown) => data is T) & Pick<Ajv.ValidateFunction, 'errors'>
export const isShippedFileInfo = ajv.compile(ShippedFileInfoSchema) as ValidateFunction<ShippedFileInfo>;
export default function validate(value: unknown): ShippedFileInfo {
if (isShippedFileInfo(value)) {
return value;
} else {
throw new Error(
ajv.errorsText(isShippedFileInfo.errors!.filter((e: any) => e.keyword !== 'if'), {dataVar: 'ShippedFileInfo'}) +
'\n\n' +
inspect(value),
);
}
}

View File

@ -1,164 +0,0 @@
import * as electron from "electron";
import * as path from "path";
import {screen} from "electron";
import {Arguments, processArguments} from "../../shared/process-arguments";
import * as loader from "./loader";
import * as updater from "../app-updater";
import * as url from "url";
import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window";
export namespace ui {
let gui: electron.BrowserWindow;
let promise: Promise<String>;
let resolve: any;
let reject: any;
export function running() : boolean {
return promise !== undefined;
}
export function cancel() : boolean {
if(resolve)
resolve();
cleanup();
return true;
}
export function cleanup() {
if(gui) {
promise = undefined;
resolve = undefined;
gui.destroy();
gui = undefined;
reject = error => {
if(error)
console.error("Received error from loader after it had been closed... Error: %o", error);
};
}
}
async function load_files() {
const channel = await updater.selected_channel();
try {
const entry_point = await loader.load_files(channel, (status, index) => {
if(gui) {
gui.webContents.send('progress-update', status, index);
}
});
const resolved = () => {
resolve(entry_point);
promise = undefined;
resolve = undefined;
reject = error => {
if(error)
console.error("Received error from loader after it had been closed... Error: %o", error);
};
};
if(!processArguments.has_flag(...Arguments.DISABLE_ANIMATION))
setTimeout(resolved, 250);
else
setImmediate(resolved);
} catch (error) {
throw error;
}
}
export function show_await_update() {
if(gui)
gui.webContents.send('await-update');
}
function spawn_gui() {
if(gui) {
gui.focus();
return;
}
console.log("Open UI loader window.");
let dev_tools = false;
const WINDOW_WIDTH = 340 + (dev_tools ? 1000 : 0);
const WINDOW_HEIGHT = 400 + (process.platform == "win32" ? 40 : 0);
gui = new electron.BrowserWindow({
width: WINDOW_WIDTH,
height: WINDOW_HEIGHT,
frame: dev_tools,
resizable: dev_tools,
show: false,
autoHideMenuBar: true,
webPreferences: {
webSecurity: false,
nodeIntegrationInWorker: false,
nodeIntegration: true
}
});
gui.setMenu(null);
gui.loadURL(url.pathToFileURL(path.join(path.dirname(module.filename), "ui", "loading_screen.html")).toString())
gui.on('closed', () => {
if(resolve) {
resolve();
}
gui = undefined;
cleanup();
});
gui.on('ready-to-show', () => {
gui.show();
try {
let bounds = screen.getPrimaryDisplay()?.bounds;
let x, y;
if(bounds) {
x = (bounds.x | 0) + ((bounds.width | 0) - WINDOW_WIDTH) / 2;
y = (bounds.y | 0) + ((bounds.height | 0) - WINDOW_HEIGHT) / 2;
} else {
x = 0;
y = 0;
}
console.log("Setting UI position to %ox%o", x, y);
if(typeof x === "number" && typeof y === "number")
gui.setPosition(x, y);
} catch (error) {
console.warn("Failed to apply UI position: %o", error);
}
loadWindowBounds('ui-load-window', gui, undefined, { applySize: false }).then(() => {
startTrackWindowBounds('ui-load-window', gui);
const call_loader = () => load_files().catch(reject);
if(!processArguments.has_flag(...Arguments.DISABLE_ANIMATION))
setTimeout(call_loader, 1000);
else
setImmediate(call_loader);
if(dev_tools)
gui.webContents.openDevTools();
});
});
}
export async function execute_loader() : Promise<String> {
return promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject || (error => {
console.error("Failed to load UI files! Error: %o", error)
});
spawn_gui();
});
}
export function preloading_page(entry_point: string) : string {
global["browser-root"] = entry_point; /* setup entry point */
return path.join(path.dirname(module.filename), "ui", "preload_page.html");
}
}

View File

@ -1,2 +0,0 @@
export * from "./loader.js";
export * from "./graphical.js";

View File

@ -1,620 +0,0 @@
import {is_debug} from "../main-window";
import * as moment from "moment";
import * as request from "request";
import * as querystring from "querystring";
import * as fs from "fs-extra";
import * as os from "os";
const UUID = require('pure-uuid');
import * as path from "path";
import * as zlib from "zlib";
import * as tar from "tar-stream";
import {Arguments, processArguments} from "../../shared/process-arguments";
import {parse_version} from "../../shared/version";
import * as electron from "electron";
import MessageBoxOptions = Electron.MessageBoxOptions;
import {current_version, execute_graphical} from "../app-updater";
import * as local_ui_cache from "./local_ui_cache";
import {WriteStream} from "fs";
const TIMEOUT = 30000;
interface RemoteURL {
(): string;
cached?: string;
}
const remote_url: RemoteURL = () => {
if(remote_url.cached)
return remote_url.cached;
const default_path = is_debug ? "http://localhost/home/TeaSpeak/Web-Client/client-api/environment/" : "https://clientapi.teaspeak.de/";
return remote_url.cached = (processArguments.has_value(...Arguments.SERVER_URL) ? processArguments.value(...Arguments.SERVER_URL) : default_path);
};
export interface VersionedFile {
name: string,
hash: string,
path: string,
type: string,
local_url: () => Promise<string>
}
function generate_tmp() : Promise<string> {
if(generate_tmp.promise) return generate_tmp.promise;
return (generate_tmp.promise = fs.mkdtemp(path.join(os.tmpdir(), "TeaClient-")).then(path => {
process.on('exit', event => {
try {
if(fs.pathExistsSync(path))
fs.removeSync(path);
} catch (e) {
console.warn("Failed to delete temp directory: %o", e);
}
});
global["browser-root"] = path;
console.log("Local browser path: %s", path);
return Promise.resolve(path);
}));
}
namespace generate_tmp {
export let promise: Promise<string>;
}
function get_raw_app_files() : Promise<VersionedFile[]> {
return new Promise<VersionedFile[]>((resolve, reject) => {
const url = remote_url() + "api.php?" + querystring.stringify({
type: "files",
});
console.debug("Requesting file list from %s", url);
request.get(url, {
timeout: TIMEOUT
}, (error, response, body: string) => {
if(error) {
reject(error);
return;
}
if(!response) {
reject("missing response object");
return;
}
if(response.statusCode != 200) { setImmediate(reject, "invalid status code " + response.statusCode + " for " + url); return; }
if(parseInt(response.headers["info-version"] as string) != 1 && !processArguments.has_flag(Arguments.UPDATER_UI_IGNORE_VERSION)) { setImmediate(reject, "Invalid response version (" + response.headers["info-version"] + "). Update your app manually!"); return; }
if(!body) {
setImmediate(reject, "invalid body. (Missing)");
return;
}
let result: VersionedFile[] = [];
body.split("\n").forEach(entry => {
if(entry.length == 0) return;
let info = entry.split("\t");
if(info[0] == "type") return;
result.push({
type: info[0],
hash: info[1],
path: info[2],
name: info[3]
} as VersionedFile);
});
setImmediate(resolve, result);
});
});
}
async function download_raw_app_files() : Promise<VersionedFile[]> {
const local_temp_path = await generate_tmp();
return get_raw_app_files().then(response => {
for(let file of response) {
const full_path = path.join(local_temp_path, file.path, file.name);
file.local_url = () => fs.mkdirs(path.dirname(full_path)).then(() => new Promise<string>((resolve, reject) => {
const write_stream = fs.createWriteStream(full_path);
request.get(remote_url() + "api.php?" + querystring.stringify({
type: "file",
path: file.path,
name: file.name
}), {
timeout: TIMEOUT
}).on('response', function(response) {
if(response.statusCode != 200) {
setImmediate(reject, "invalid status code " + response.statusCode + " for file " + file.name + " (" + file.path + ")");
return;
}
}).on('complete', event => {
}).on('error', error => {
try { write_stream.close(); } catch (e) { }
setImmediate(reject, error);
}).pipe(write_stream)
.on('finish', event => {
try { write_stream.close(); } catch (e) { }
setImmediate(resolve, file.path + "/" + file.name);
}).on('error', error => {
try { write_stream.close(); } catch (e) { }
setImmediate(reject, error);
});
}));
}
return Promise.resolve(response);
}).catch(error => {
console.log("Failed to get file list: %o", error);
return Promise.reject("Failed to get file list (" + error + ")");
})
}
async function client_shipped_ui() : Promise<local_ui_cache.CachedUIPack | undefined> {
const app_path = electron.app.getAppPath();
if(!app_path.endsWith(".asar"))
return undefined;
const base_path = path.join(path.dirname(app_path), "ui");
//console.debug("Looking for client shipped UI pack at %s", base_path);
if(!(await fs.pathExists(base_path)))
return undefined;
const info: {
channel: string,
version: string,
git_hash: string,
required_client: string,
timestamp: number,
filename: string
} = await fs.readJson(path.join(base_path, "default_ui_info.json")) as any;
return {
download_timestamp: info.timestamp * 1000,
status: "valid",
invalid_reason: undefined,
local_checksum: "none",
local_file_path: path.join(path.join(path.dirname(app_path), "ui"), info.filename),
pack_info: {
channel: info.channel,
min_client_version: info.required_client,
timestamp: info.timestamp * 1000,
version: info.version,
versions_hash: info.git_hash
}
};
}
async function query_ui_pack_versions() : Promise<local_ui_cache.UIPackInfo[]> {
const url = remote_url() + "api.php?" + querystring.stringify({
type: "ui-info"
});
console.debug("Loading UI pack information (URL: %s)", url);
let body = await new Promise<string>((resolve, reject) => request.get(url, { timeout: TIMEOUT }, (error, response, body: string) => {
if(error)
reject(error);
else if(!response)
reject("missing response object");
else {
if(response.statusCode !== 200)
reject(response.statusCode + " " + response.statusMessage);
else if(!body)
reject("missing body in response");
else
resolve(body);
}
}));
let response;
try {
response = JSON.parse(body);
} catch (error) {
console.error("Received unparsable response for UI pack info. Response: %s", body);
throw "failed to parse response";
}
if(!response["success"])
throw "request failed: " + (response["msg"] || "unknown error");
if(!Array.isArray(response["versions"])) {
console.error("Response object misses 'versions' tag or has an invalid value. Object: %o", response);
throw "response contains invalid data";
}
let ui_versions: local_ui_cache.UIPackInfo[] = [];
for(const entry of response["versions"]) {
ui_versions.push({
channel: entry["channel"],
versions_hash: entry["git-ref"],
version: entry["version"],
timestamp: parseInt(entry["timestamp"]) * 1000, /* server provices that stuff in seconds */
min_client_version: entry["required_client"]
});
}
return ui_versions;
}
async function download_ui_pack(version: local_ui_cache.UIPackInfo) : Promise<local_ui_cache.CachedUIPack> {
const target_file = path.join(local_ui_cache.cache_path(), version.channel + "_" + version.versions_hash + "_" + version.timestamp + ".tar.gz");
if(await fs.pathExists(target_file)) {
try {
await fs.remove(target_file);
} catch (error) {
console.error("Tried to download UI version %s, but we failed to delete the old file: %o", version.versions_hash, error);
throw "failed to delete old file";
}
}
try {
await fs.mkdirp(path.dirname(target_file));
} catch (error) {
console.error("Failed to create target UI pack download directory at %s: %o", path.dirname(target_file), error);
throw "failed to create target directories";
}
await new Promise((resolve, reject) => {
let fstream: WriteStream;
try {
request.get(remote_url() + "api.php?" + querystring.stringify({
"type": "ui-download",
"git-ref": version.versions_hash,
"version": version.version,
"timestamp": Math.floor(version.timestamp / 1000), /* remote server has only the timestamp in seconds*/
"channel": version.channel
}), {
timeout: TIMEOUT
}).on('response', function(response: request.Response) {
if(response.statusCode != 200)
reject(response.statusCode + " " + response.statusMessage);
}).on('error', error => {
reject(error);
}).pipe(fstream = fs.createWriteStream(target_file)).on('finish', () => {
try { fstream.close(); } catch (e) { }
resolve();
});
} catch (error) {
try { fstream.close(); } catch (e) { }
reject(error);
}
});
try {
const cache = await local_ui_cache.load();
const info: local_ui_cache.CachedUIPack = {
pack_info: version,
local_file_path: target_file,
local_checksum: "none", //TODO!
invalid_reason: undefined,
status: "valid",
download_timestamp: Date.now()
};
cache.cached_ui_packs.push(info);
await local_ui_cache.save();
return info;
} catch (error) {
console.error("Failed to register downloaded UI pack to the UI cache: %o", error);
throw "failed to register downloaded UI pack to the UI cache";
}
}
async function ui_pack_usable(version: local_ui_cache.CachedUIPack) : Promise<boolean> {
if(version.status !== "valid") return false;
return await fs.pathExists(version.local_file_path);
}
async function unpack_local_ui_pack(version: local_ui_cache.CachedUIPack) : Promise<string> {
if(!await ui_pack_usable(version))
throw "UI pack has been invalidated";
const target_directory = await generate_tmp();
if(!await fs.pathExists(target_directory))
throw "failed to create temporary directory";
const gunzip = zlib.createGunzip();
const extract = tar.extract();
let fpipe: fs.ReadStream;
try {
fpipe = fs.createReadStream(version.local_file_path);
} catch (error) {
console.error("Failed to open UI pack at %s: %o", version.local_file_path, error);
throw "failed to open UI pack";
}
extract.on('entry', function(header: tar.Headers, stream, next) {
if(header.type == 'file') {
const target_file = path.join(target_directory, header.name);
if(!fs.existsSync(path.dirname(target_file))) fs.mkdirsSync(path.dirname(target_file));
stream.on('end', () => setImmediate(next));
const wfpipe = fs.createWriteStream(target_file);
stream.pipe(wfpipe);
} else if(header.type == 'directory') {
if(fs.existsSync(path.join(target_directory, header.name)))
setImmediate(next);
fs.mkdirs(path.join(target_directory, header.name)).catch(error => {
console.warn("Failed to create unpacking dir " + path.join(target_directory, header.name));
console.error(error);
}).then(() => setImmediate(next));
} else {
console.warn("Invalid ui tar ball entry type (" + header.type + ")");
return;
}
});
const finish_promise = new Promise((resolve, reject) => {
gunzip.on('error', event => {
reject(event);
});
extract.on('finish', resolve);
extract.on('error', event => {
if(!event) return;
reject(event);
});
fpipe.pipe(gunzip).pipe(extract);
});
try {
await finish_promise;
} catch(error) {
console.error("Failed to extract UI files to %s: %o", target_directory, error);
throw "failed to unpack the UI pack";
}
return target_directory;
}
async function load_files_from_dev_server(channel: string, stats_update: (message: string, index: number) => any) : Promise<String> {
stats_update("Fetching files", 0);
let files: VersionedFile[];
try {
files = await download_raw_app_files()
} catch (error) {
console.log("Failed to fetch raw UI file list: %o", error);
let msg;
if(error instanceof Error)
msg = error.message;
else if(typeof error === "string")
msg = error;
throw "failed to get file list" + (msg ? " (" + msg + ")" : "");
}
const max_simultaneously_downloads = 8;
let pending_files: VersionedFile[] = files.slice(0);
let current_downloads: {[key: string]: Promise<void>} = {};
const update_download_status = () => {
const indicator = (pending_files.length + Object.keys(current_downloads).length) / files.length;
stats_update("Downloading raw UI files", 1 - indicator);
};
update_download_status();
let errors: { file: VersionedFile; error: any }[] = [];
while(pending_files.length > 0) {
while(pending_files.length > 0 && Object.keys(current_downloads).length < max_simultaneously_downloads) {
const file = pending_files.pop();
current_downloads[file.hash] = file.local_url().catch(error => {
errors.push({ file: file, error: error});
}).then(() => {
delete current_downloads[file.hash];
});
}
update_download_status();
await Promise.race(Object.keys(current_downloads).map(e => current_downloads[e]));
if(errors.length > 0)
break;
}
/* await full finish */
while(Object.keys(current_downloads).length > 0) {
update_download_status();
await Promise.race(Object.keys(current_downloads).map(e => current_downloads[e]));
}
if(errors.length > 0) {
console.log("Failed to load UI files (%d):", errors.length);
for(const error of errors)
console.error(" - %s: %o", path.join(error.file.path + error.file.name), error.error);
throw "failed to download file " + path.join(errors[0].file.path + errors[0].file.name) + " (" + errors[0].error + ")\nView console for a full error report.";
}
console.log("Successfully loaded UI files from remote server.");
/* generate_tmp has already been called an its the file destination */
return path.join(await generate_tmp(), "index.html"); /* entry point */
}
async function stream_files_from_dev_server(channel: string, stats_update: (message: string, index: number) => any) : Promise<string> {
return remote_url() + "index.html";
}
async function load_bundles_ui_pack(channel: string, stats_update: (message: string, index: number) => any) : Promise<String> {
stats_update("Query local UI pack info", .33);
const bundles_ui = await client_shipped_ui();
if(!bundles_ui) throw "client has no bundled UI pack";
stats_update("Unpacking bundled UI", .66);
const result = await unpack_local_ui_pack(bundles_ui);
stats_update("Local UI pack loaded", 1);
console.log("Loaded bundles UI pack successfully. Version: {timestamp: %d, hash: %s}", bundles_ui.pack_info.timestamp, bundles_ui.pack_info.versions_hash);
return path.join(result, "index.html");
}
async function load_cached_or_remote_ui_pack(channel: string, stats_update: (message: string, index: number) => any, ignore_new_version_timestamp: boolean) : Promise<String> {
stats_update("Fetching info", 0);
const ui_cache = await local_ui_cache.load();
const bundles_ui = await client_shipped_ui();
const client_version = await current_version();
let available_versions: local_ui_cache.CachedUIPack[] = ui_cache.cached_ui_packs.filter(e => {
if(e.status !== "valid")
return false;
if(bundles_ui) {
if(e.pack_info.timestamp <= bundles_ui.download_timestamp)
return false;
}
const required_version = parse_version(e.pack_info.min_client_version);
return client_version.in_dev() || client_version.newer_than(required_version) || client_version.equals(required_version);
});
if(processArguments.has_flag(Arguments.UPDATER_UI_NO_CACHE)) {
console.log("Ignoring local UI cache");
available_versions = [];
}
let remote_version_dropped = false;
/* remote version gathering */
remote_loader: {
stats_update("Loading remote info", .25);
let remote_versions: local_ui_cache.UIPackInfo[];
try {
remote_versions = await query_ui_pack_versions();
} catch (error) {
if(available_versions.length === 0)
throw "failed to query remote UI packs: " + error;
console.error("Failed to query remote UI packs: %o", error);
break remote_loader;
}
stats_update("Parsing UI packs", .40);
const remote_version = remote_versions.find(e => e.channel === channel);
if(!remote_version && available_versions.length === 0)
throw "no UI pack available for channel " + channel;
let newest_local_version = available_versions.map(e => e.pack_info.timestamp).reduce((a, b) => Math.max(a, b), bundles_ui ? bundles_ui.download_timestamp : 0);
console.log("Remote version %d, Local version %d", remote_version.timestamp, newest_local_version);
const required_version = parse_version(remote_version.min_client_version);
if(required_version.newer_than(client_version) && !is_debug) {
const result = await electron.dialog.showMessageBox({
type: "question",
message:
"Your client is outdated.\n" +
"Newer UI packs (>= " + remote_version.version + ", " + remote_version.versions_hash + ") require client " + remote_version.min_client_version + "\n" +
"Do you want to update your client?",
title: "Client outdated!",
buttons: ["Update client", available_versions.length === 0 ? "Close client" : "Ignore and use last possible"]
} as MessageBoxOptions);
if(result.response == 0) {
if(!await execute_graphical(channel, true))
throw "Client outdated an no suitable UI pack versions found";
else
return;
} else {
if(available_versions.length === 0) {
electron.app.exit(1);
return;
}
}
} else if(remote_version.timestamp <= newest_local_version && !ignore_new_version_timestamp) {
/* We've already a equal or newer version. Don't use the remote version */
remote_version_dropped = !!bundles_ui && remote_version.timestamp > bundles_ui.download_timestamp; /* if remote is older than current bundled version its def. not a drop */
} else {
/* update is possible because the timestamp is newer than out latest local version */
try {
console.log("Downloading UI pack version (%d) %s. Forced: %s. Newest local version: %d", remote_version.timestamp,
remote_version.versions_hash, ignore_new_version_timestamp ? "true" : "false", newest_local_version);
stats_update("Downloading new UI pack", .55);
available_versions.push(await download_ui_pack(remote_version));
} catch (error) {
console.error("Failed to download new UI pack: %o", error);
}
}
}
stats_update("Unpacking UI", .70);
available_versions.sort((a, b) => a.pack_info.timestamp - b.pack_info.timestamp);
/* Only invalidate the version if any other succeeded to load. Else we might fucked up (no permission to write etc) */
let invalidate_versions: local_ui_cache.CachedUIPack[] = [];
const do_invalidate_versions = async () => {
if(invalidate_versions.length > 0) {
for(const version of invalidate_versions) {
version.invalid_reason = "failed to unpack";
version.status = "invalid";
}
await local_ui_cache.save();
}
};
while(available_versions.length > 0) {
const pack = available_versions.pop();
console.log("Trying to load UI pack from %s (%s). Downloaded at %s", moment(pack.pack_info.timestamp).format("llll"), pack.pack_info.versions_hash, moment(pack.download_timestamp).format("llll"));
try {
const target = await unpack_local_ui_pack(pack);
stats_update("UI pack loaded", 1);
await do_invalidate_versions();
return path.join(target, "index.html");
} catch (error) {
invalidate_versions.push(pack);
console.log("Failed to unpack UI pack: %o", error);
}
}
if(remote_version_dropped) {
/* try again, but this time enforce a remote download */
const result = await load_cached_or_remote_ui_pack(channel, stats_update, true);
await do_invalidate_versions(); /* new UI pack seems to be successfully loaded */
return result; /* if not succeeded an exception will be thrown */
}
throw "Failed to load any UI pack (local and remote)\nView the console for more details.\n";
}
enum UILoaderMethod {
PACK,
BUNDLED_PACK,
RAW_FILES,
DEVELOP_SERVER
}
export async function load_files(channel: string, stats_update: (message: string, index: number) => any) : Promise<String> {
let enforced_loading_method = parseInt(processArguments.has_value(Arguments.UPDATER_UI_LOAD_TYPE) ? processArguments.value(Arguments.UPDATER_UI_LOAD_TYPE) : "-1") as UILoaderMethod;
if(typeof UILoaderMethod[enforced_loading_method] !== "undefined") {
switch (enforced_loading_method) {
case UILoaderMethod.PACK:
return await load_cached_or_remote_ui_pack(channel, stats_update, false);
case UILoaderMethod.BUNDLED_PACK:
return await load_bundles_ui_pack(channel, stats_update);
case UILoaderMethod.RAW_FILES:
return await load_files_from_dev_server(channel, stats_update);
case UILoaderMethod.DEVELOP_SERVER:
return await stream_files_from_dev_server(channel, stats_update);
}
}
let first_error;
if(is_debug) {
try {
return await load_files_from_dev_server(channel, stats_update);
} catch(error) {
console.warn("Failed to load raw UI files: %o", error);
first_error = first_error || error;
}
}
try {
return await load_cached_or_remote_ui_pack(channel, stats_update, false);
} catch(error) {
console.warn("Failed to load cached/remote UI pack: %o", error);
first_error = first_error || error;
}
try {
return await load_bundles_ui_pack(channel, stats_update);
} catch(error) {
console.warn("Failed to load bundles UI pack: %o", error);
first_error = first_error || error;
}
throw first_error;
}

View File

@ -1,138 +0,0 @@
import * as path from "path";
import * as fs from "fs-extra";
import * as electron from "electron";
export namespace v1 {
/* main entry */
interface LocalUICache {
fetch_history?: FetchStatus;
versions?: LocalUICacheEntry[];
remote_index?: UIVersion[] | UIVersion;
remote_index_channel?: string; /* only set if the last status was a channel only*/
local_index?: UIVersion;
}
interface FetchStatus {
timestamp: number;
/**
* 0 = success
* 1 = connect fail
* 2 = internal fail
*/
status: number;
}
interface LocalUICacheEntry {
version: UIVersion;
download_timestamp: number;
tar_file: string;
checksum: string; /* SHA512 */
}
export interface UIVersion {
channel: string;
version: string;
git_hash: string;
timestamp: number;
required_client?: string;
filename?: string;
client_shipped?: boolean;
}
}
export interface CacheFile {
version: number; /* currently 2 */
cached_ui_packs: CachedUIPack[];
}
export interface UIPackInfo {
timestamp: number; /* build timestamp */
version: string; /* not really used anymore */
versions_hash: string; /* used, identifies the version. Its the git hash. */
channel: string;
min_client_version: string; /* minimum version from the client required for the pack */
}
export interface CachedUIPack {
download_timestamp: number;
local_file_path: string;
local_checksum: string | "none"; /* sha512 of the locally downloaded file. */
//TODO: Get the remote checksum and compare them instead of the local one
pack_info: UIPackInfo;
status: "valid" | "invalid";
invalid_reason?: string;
}
let cached_loading_promise_: Promise<CacheFile>;
let ui_cache_: CacheFile = {
version: 2,
cached_ui_packs: []
};
async function load_() : Promise<CacheFile> {
const file = path.join(cache_path(), "data.json");
try {
if(!(await fs.pathExists(file))) {
return ui_cache_;
}
const data = await fs.readJSON(file) as CacheFile;
if(!data) {
throw "invalid data object";
} else if(typeof data["version"] !== "number") {
throw "invalid versions tag";
} else if(data["version"] !== 2) {
console.warn("UI cache file contains an old version. Ignoring file and may override with newer version.");
return ui_cache_;
}
/* validating data */
if(!Array.isArray(data.cached_ui_packs)) {
throw "Invalid 'cached_ui_packs' entry within the UI cache file";
}
return (ui_cache_ = data as CacheFile);
} catch(error) {
console.warn("Failed to load UI cache file: %o. This will cause loss of the file content.", error);
return ui_cache_;
}
}
/**
* Will not throw or return undefined!
*/
export function load() : Promise<CacheFile> {
if(cached_loading_promise_) return cached_loading_promise_;
return (cached_loading_promise_ = load_());
}
export function unload() {
ui_cache_ = undefined;
cached_loading_promise_ = undefined;
}
/**
* Will not throw anything
*/
export async function save() {
const file = path.join(cache_path(), "data.json");
try {
if(!(await fs.pathExists(path.dirname(file))))
await fs.mkdirs(path.dirname(file));
await fs.writeJson(file, ui_cache_);
} catch (error) {
console.error("Failed to save UI cache file. This will may cause some data loss: %o", error);
}
}
export function cache_path() {
return path.join(electron.app.getPath('userData'), "cache", "ui");
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,22 +0,0 @@
const icp = require("electron").ipcRenderer;
interface Window {
$: JQuery;
}
(window as any).$ = require("jquery");
icp.on('progress-update', (event, status, count) => {
console.log("Process update \"%s\" to %d", status, count);
$("#current-status").text(status);
$(".container-bar .bar").css("width", (count * 100) + "%");
});
icp.on('await-update', (event) => {
console.log("Received update notification");
$(".container-bar .bar").css("width", "100%");
$("#loading-text").html("Awaiting client update response<br>(User input required)");
});
export {}

View File

@ -1,111 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>TeaClient</title>
<style type="text/css">
html, body {
background: #18BC9C;
user-select: none;
}
body {
text-align: center;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
margin-left: 18px;
margin-right: 18px;
display: flex;
flex-direction: column;
justify-content: center;
-ms-overflow-style: none;
}
img {
position: absolute;
display: block;
width: 200px;
height: 200px;
}
.smoke {
z-index: 2;
}
.logo {
z-index: 1;
}
.container-logo {
align-self: center;
position: relative;
display: inline-block;
width: 200px;
height: 200px;
}
.container-info a {
display: inline-block;
color: #FFFFFF;
font-family: "Arial",serif;
font-size: 20px;
}
.container-bar {
position: relative;
margin-top: 5px;
border: white solid 2px;
height: 18px;
}
.container-bar .bar {
z-index: 1;
position: absolute;
display: block;
background: whitesmoke;
border: none;
width: 0;
height: 100%;
}
#current-status {
margin-top: 3px;
font-size: 18px;
max-width: 100%;
width: 100%;
text-align: left;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
</style>
<script type="application/ecmascript">const exports = {};</script>
<script type="application/ecmascript" src="loader.js"></script>
</head>
<body>
<div class="container-logo">
<img class="logo" src="img/logo.svg" alt="logo">
<img class="smoke" src="img/smoke.png" alt="">
</div>
<div class="container-info">
<a id="loading-text">Loading... Please wait!</a>
<div class="container-bar">
<div class="bar"></div>
</div>
<a id="current-status">&nbsp;</a>
</div>
</body>
</html>

View File

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script type="application/javascript">
const remote = require('electron').remote;
const fs = require('fs-extra');
const target_file = remote.getGlobal("browser-root");
console.log("Navigate to %s", target_file);
if(fs.existsSync(target_file) || target_file.startsWith("http://") || target_file.startsWith("https://"))
window.location.href = target_file;
else {
console.error("Failed to find target file!");
if(!remote.getCurrentWebContents().isDevToolsOpened())
remote.getCurrentWebContents().openDevTools();
}
</script>
<title>TeaClient - loading files</title>
</head>
<body>
An unknown error happened!<br>
Please report this!
</body>
</html>

View File

@ -39,7 +39,7 @@ const html_overlay =
"text-align: center;\n" +
"line-height: 15px;\n" +
"z-index: 1000;\n" +
"text-decoration: none;'" +
"text-decoration: none;' " +
"class='button-close'>" +
"✖" +
"</a>" +
@ -64,7 +64,7 @@ let _inject_overlay = () => {
console.warn(log_prefix + "Failed to find close button for preview notice!");
} else {
for(const button of buttons) {
(<HTMLElement>button).onclick = _close_overlay;
(button as HTMLElement).onclick = _close_overlay;
}
}
}
@ -74,7 +74,7 @@ let _inject_overlay = () => {
console.warn(log_prefix + "Failed to find open button for preview notice!");
} else {
for(const element of buttons) {
(<HTMLElement>element).onclick = event => {
(element as HTMLElement).onclick = () => {
console.info(log_prefix + "Opening URL with default browser");
electron.remote.shell.openExternal(location.href, {
activate: true

View File

@ -0,0 +1,114 @@
import {loadWindowBounds, startTrackWindowBounds} from "../../../../shared/window";
import {BrowserWindow, dialog} from "electron";
import * as path from "path";
import * as url from "url";
import { screen } from "electron";
let kDeveloperTools = false;
let windowInstance: BrowserWindow;
let windowSpawnPromise: Promise<void>;
let currentStatus: string;
let currentProgress: number;
export async function showAppLoaderWindow() {
while(windowSpawnPromise) {
await windowSpawnPromise;
}
if(windowInstance) {
console.error("Just focus");
windowInstance.focus();
return;
}
windowSpawnPromise = spawnAppLoaderWindow().catch(error => {
console.error("Failed to open the app loader window: %o", error);
dialog.showErrorBox("Failed to open window", "Failed to open the app loader window.\nLookup the console for details.");
hideAppLoaderWindow();
});
/* do this after the assignment so in case the promise resolves instantly we still clear the assignment */
windowSpawnPromise.then(() => windowSpawnPromise = undefined);
await windowSpawnPromise;
}
export function getLoaderWindow() : BrowserWindow {
return windowInstance;
}
async function spawnAppLoaderWindow() {
console.debug("Opening app loader window.");
const kWindowWidth = 340 + (kDeveloperTools ? 1000 : 0);
const kWindowHeight = 400 + (process.platform == "win32" ? 40 : 0);
windowInstance = new BrowserWindow({
width: kWindowWidth,
height: kWindowHeight,
frame: kDeveloperTools,
resizable: kDeveloperTools,
show: false,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
}
});
windowInstance.setMenu(null);
windowInstance.on('closed', () => {
windowInstance = undefined;
});
if(kDeveloperTools) {
windowInstance.webContents.openDevTools();
}
await windowInstance.loadURL(url.pathToFileURL(path.join(__dirname, "..", "renderer", "index.html")).toString());
setAppLoaderStatus(currentStatus, currentProgress);
windowInstance.show();
try {
let bounds = screen.getPrimaryDisplay()?.bounds;
let x, y;
if(bounds) {
x = (bounds.x | 0) + ((bounds.width | 0) - kWindowWidth) / 2;
y = (bounds.y | 0) + ((bounds.height | 0) - kWindowHeight) / 2;
} else {
x = 0;
y = 0;
}
console.log("Setting app loader ui position to %ox%o", x, y);
if(typeof x === "number" && typeof y === "number") {
windowInstance.setPosition(x, y);
}
} catch (error) {
console.warn("Failed to apply app loader ui position: %o", error);
}
try {
await loadWindowBounds('ui-load-window', windowInstance, undefined, { applySize: false });
startTrackWindowBounds('ui-load-window', windowInstance);
} catch (error) {
console.warn("Failed to load and track window bounds: %o", error);
}
}
export function hideAppLoaderWindow() {
(async () => {
await windowSpawnPromise;
if(windowInstance) {
windowInstance.close();
windowInstance = undefined;
}
})();
}
export function setAppLoaderStatus(status: string, progress: number) {
currentStatus = status;
currentProgress = progress;
windowInstance?.webContents.send("progress-update", status, progress);
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><path d="M1.5,12c0,0 6.221,-4 14.5,-4c8.279,0 14.489,4 14.489,4l-14.489,14.667l-14.5,-14.667Z" style="fill:#be1515;"/>
<path d="M16.199,8.001l0.193,0.002l0.192,0.003l0.192,0.005l0.19,0.007l0.19,0.008l0.189,0.009l0.188,0.01l0.187,0.012l0.186,0.012l0.186,0.015l0.184,0.015l0.184,0.016l0.183,0.018l0.181,0.019l0.181,0.02l0.18,0.021l0.179,0.022l0.178,0.023l0.177,0.024l0.176,0.026l0.175,0.026l0.174,0.027l0.173,0.028l0.172,0.029l0.171,0.03l0.17,0.031l0.169,0.031l0.168,0.033l0.166,0.033l0.166,0.034l0.164,0.035l0.163,0.036l0.162,0.036l0.161,0.037l0.16,0.037l0.158,0.038l0.158,0.039l0.156,0.04l0.155,0.04l0.153,0.04l0.153,0.041l0.151,0.042l0.15,0.042l0.149,0.043l0.147,0.042l0.146,0.044l0.145,0.044l0.285,0.088l0.28,0.09l0.275,0.091l0.269,0.092l0.263,0.093l0.258,0.093l0.252,0.093l0.245,0.094l0.24,0.094l0.233,0.093l0.228,0.093l0.221,0.093l0.214,0.092l0.208,0.091l0.201,0.09l0.195,0.088l0.187,0.088l0.181,0.085l0.174,0.084l0.167,0.081l0.159,0.08l0.152,0.077l0.145,0.074l0.137,0.072l0.13,0.069l0.122,0.066l0.114,0.062l0.107,0.059l0.189,0.107l0.156,0.09l0.123,0.073l0.088,0.054l0.072,0.045l-14.5,14.667l-14.489,-14.667l0.072,-0.045l0.089,-0.054l0.122,-0.073l0.157,-0.09l0.188,-0.107l0.107,-0.059l0.114,-0.062l0.122,-0.066l0.13,-0.069l0.137,-0.072l0.145,-0.074l0.152,-0.077l0.159,-0.08l0.167,-0.081l0.174,-0.084l0.181,-0.085l0.187,-0.088l0.195,-0.088l0.201,-0.09l0.208,-0.091l0.215,-0.092l0.22,-0.093l0.228,-0.093l0.233,-0.093l0.24,-0.094l0.246,-0.094l0.251,-0.093l0.258,-0.093l0.263,-0.093l0.269,-0.092l0.275,-0.091l0.28,-0.09l0.285,-0.088l0.145,-0.044l0.146,-0.044l0.147,-0.042l0.149,-0.043l0.15,-0.042l0.151,-0.042l0.153,-0.041l0.153,-0.04l0.155,-0.04l0.156,-0.04l0.158,-0.039l0.158,-0.038l0.16,-0.037l0.161,-0.037l0.162,-0.036l0.163,-0.036l0.165,-0.035l0.165,-0.034l0.167,-0.033l0.167,-0.033l0.169,-0.031l0.17,-0.031l0.171,-0.03l0.172,-0.029l0.173,-0.028l0.174,-0.027l0.175,-0.026l0.176,-0.026l0.177,-0.024l0.178,-0.023l0.179,-0.022l0.18,-0.021l0.181,-0.02l0.182,-0.019l0.182,-0.018l0.184,-0.016l0.185,-0.015l0.185,-0.015l0.186,-0.012l0.187,-0.012l0.189,-0.01l0.188,-0.009l0.19,-0.008l0.191,-0.007l0.191,-0.005l0.192,-0.003l0.193,-0.002l0.194,-0.001l0.193,0.001Zm-0.38,1l-0.185,0.002l-0.185,0.003l-0.184,0.005l-0.183,0.006l-0.182,0.008l-0.182,0.008l-0.181,0.01l-0.18,0.011l-0.18,0.013l-0.178,0.013l-0.178,0.015l-0.177,0.016l-0.176,0.017l-0.175,0.018l-0.174,0.019l-0.174,0.021l-0.172,0.021l-0.172,0.022l-0.171,0.024l-0.169,0.024l-0.169,0.025l-0.168,0.027l-0.167,0.027l-0.166,0.028l-0.165,0.029l-0.164,0.029l-0.163,0.031l-0.162,0.031l-0.16,0.032l-0.16,0.033l-0.159,0.034l-0.158,0.034l-0.156,0.035l-0.156,0.036l-0.154,0.036l-0.153,0.037l-0.152,0.038l-0.151,0.038l-0.15,0.038l-0.149,0.04l-0.147,0.039l-0.146,0.04l-0.145,0.041l-0.144,0.041l-0.143,0.042l-0.141,0.042l-0.139,0.042l-0.277,0.086l-0.271,0.087l-0.266,0.088l-0.26,0.089l-0.255,0.089l-0.25,0.091l-0.243,0.09l-0.238,0.091l-0.232,0.091l-0.226,0.09l-0.22,0.09l-0.213,0.09l-0.208,0.089l-0.201,0.088l-0.194,0.086l-0.188,0.086l-0.181,0.084l-0.175,0.083l-0.167,0.08l-0.161,0.079l-0.153,0.076l-0.147,0.074l-0.139,0.072l-0.068,0.036l12.859,13.017l12.87,-13.017l-0.068,-0.036l-0.139,-0.072l-0.147,-0.074l-0.153,-0.076l-0.161,-0.079l-0.167,-0.08l-0.175,-0.083l-0.181,-0.084l-0.188,-0.086l-0.194,-0.086l-0.201,-0.088l-0.208,-0.089l-0.213,-0.09l-0.22,-0.09l-0.226,-0.09l-0.232,-0.091l-0.238,-0.091l-0.243,-0.09l-0.25,-0.091l-0.255,-0.089l-0.26,-0.089l-0.266,-0.088l-0.271,-0.087l-0.277,-0.086l-0.139,-0.042l-0.141,-0.042l-0.143,-0.042l-0.144,-0.041l-0.145,-0.041l-0.146,-0.04l-0.147,-0.039l-0.149,-0.04l-0.15,-0.038l-0.151,-0.038l-0.152,-0.038l-0.153,-0.037l-0.154,-0.036l-0.156,-0.036l-0.156,-0.035l-0.158,-0.034l-0.159,-0.034l-0.16,-0.033l-0.16,-0.032l-0.162,-0.031l-0.163,-0.031l-0.164,-0.029l-0.165,-0.029l-0.166,-0.028l-0.167,-0.027l-0.168,-0.027l-0.169,-0.025l-0.169,-0.024l-0.171,-0.024l-0.172,-0.022l-0.172,-0.021l-0.174,-0.021l-0.174,-0.019l-0.175,-0.018l-0.176,-0.017l-0.177,-0.016l-0.178,-0.015l-0.178,-0.013l-0.18,-0.013l-0.18,-0.011l-0.181,-0.01l-0.181,-0.008l-0.183,-0.008l-0.183,-0.006l-0.184,-0.005l-0.185,-0.003l-0.185,-0.002l-0.186,-0.001l-0.187,0.001Z"
style="fill:#ccd7e4;"/>
<path d="M1.511,12.204c0,0 6.21,-3.796 14.489,-3.796c8.279,0 14.489,3.796 14.489,3.796l-14.489,14.463l-14.489,-14.463Z"
style="fill:none;stroke:#ccd7e4;stroke-width:1px;"/>
<path d="M16.201,7.001l0.201,0.002l0.199,0.004l0.199,0.005l0.197,0.007l0.197,0.008l0.196,0.009l0.196,0.011l0.194,0.012l0.193,0.013l0.192,0.015l0.192,0.016l0.19,0.017l0.19,0.018l0.188,0.02l0.188,0.02l0.186,0.022l0.186,0.023l0.184,0.024l0.184,0.025l0.182,0.026l0.181,0.027l0.181,0.028l0.179,0.03l0.178,0.03l0.177,0.031l0.175,0.031l0.175,0.033l0.174,0.034l0.172,0.034l0.171,0.035l0.17,0.036l0.169,0.037l0.167,0.037l0.167,0.039l0.165,0.038l0.164,0.04l0.162,0.04l0.162,0.041l0.16,0.041l0.159,0.042l0.157,0.042l0.157,0.043l0.154,0.044l0.154,0.044l0.152,0.044l0.151,0.045l0.151,0.045l0.293,0.092l0.29,0.092l0.283,0.094l0.278,0.095l0.272,0.096l0.266,0.096l0.26,0.096l0.254,0.097l0.247,0.097l0.241,0.096l0.235,0.097l0.228,0.095l0.222,0.095l0.215,0.094l0.208,0.093l0.201,0.092l0.195,0.09l0.187,0.088l0.18,0.087l0.173,0.085l0.166,0.082l0.158,0.08l0.15,0.077l0.143,0.075l0.135,0.072l0.128,0.068l0.12,0.066l0.112,0.062l0.198,0.112l0.166,0.096l0.132,0.078l0.098,0.06l0.077,0.048l0.086,0.06l0.08,0.07l0.071,0.077l0.064,0.084l0.054,0.091l0.044,0.095l0.034,0.1l0.023,0.103l0.012,0.104l0.001,0.106l-0.01,0.105l-0.021,0.103l-0.031,0.1l-0.042,0.097l-0.052,0.092l-0.062,0.085l-0.07,0.079l-14.5,14.667l-0.078,0.071l-0.085,0.062l-0.091,0.053l-0.096,0.043l-0.1,0.033l-0.104,0.022l-0.104,0.011l-0.106,0l-0.105,-0.011l-0.103,-0.022l-0.1,-0.033l-0.096,-0.043l-0.091,-0.053l-0.085,-0.063l-0.078,-0.071l-14.489,-14.666l-0.07,-0.079l-0.062,-0.086l-0.051,-0.091l-0.043,-0.097l-0.031,-0.1l-0.021,-0.103l-0.01,-0.105l0.001,-0.105l0.012,-0.105l0.023,-0.103l0.034,-0.099l0.044,-0.096l0.054,-0.09l0.063,-0.085l0.072,-0.077l0.079,-0.069l0.087,-0.061l0.076,-0.048l0.099,-0.06l0.131,-0.078l0.166,-0.096l0.197,-0.112l0.113,-0.062l0.119,-0.065l0.128,-0.069l0.135,-0.072l0.142,-0.074l0.151,-0.078l0.158,-0.08l0.165,-0.082l0.173,-0.085l0.18,-0.086l0.187,-0.089l0.194,-0.09l0.201,-0.092l0.208,-0.093l0.214,-0.094l0.222,-0.095l0.228,-0.095l0.234,-0.096l0.241,-0.097l0.248,-0.097l0.253,-0.097l0.26,-0.096l0.265,-0.096l0.272,-0.096l0.278,-0.095l0.283,-0.094l0.289,-0.092l0.294,-0.091l0.15,-0.046l0.151,-0.045l0.152,-0.044l0.153,-0.044l0.155,-0.044l0.156,-0.042l0.158,-0.043l0.158,-0.042l0.16,-0.041l0.162,-0.041l0.162,-0.04l0.164,-0.04l0.165,-0.038l0.166,-0.039l0.168,-0.037l0.169,-0.037l0.169,-0.036l0.172,-0.035l0.172,-0.034l0.173,-0.034l0.175,-0.033l0.176,-0.031l0.176,-0.031l0.178,-0.03l0.179,-0.03l0.18,-0.028l0.182,-0.027l0.182,-0.026l0.183,-0.025l0.185,-0.024l0.185,-0.023l0.187,-0.022l0.187,-0.02l0.189,-0.02l0.189,-0.018l0.19,-0.017l0.192,-0.016l0.192,-0.015l0.193,-0.013l0.195,-0.012l0.195,-0.011l0.196,-0.009l0.197,-0.008l0.197,-0.007l0.199,-0.005l0.199,-0.004l0.201,-0.002l0.201,-0.001l0.201,0.001Zm-0.395,1l-0.193,0.002l-0.192,0.003l-0.191,0.005l-0.19,0.007l-0.19,0.008l-0.189,0.009l-0.188,0.01l-0.187,0.012l-0.186,0.012l-0.186,0.015l-0.184,0.015l-0.184,0.016l-0.183,0.018l-0.182,0.019l-0.18,0.02l-0.18,0.021l-0.179,0.022l-0.178,0.023l-0.177,0.024l-0.176,0.026l-0.175,0.026l-0.174,0.027l-0.173,0.028l-0.172,0.029l-0.171,0.03l-0.17,0.031l-0.169,0.031l-0.167,0.033l-0.167,0.033l-0.165,0.034l-0.164,0.035l-0.164,0.036l-0.162,0.036l-0.16,0.037l-0.16,0.037l-0.159,0.038l-0.157,0.039l-0.156,0.04l-0.155,0.04l-0.154,0.04l-0.152,0.041l-0.151,0.042l-0.15,0.042l-0.149,0.043l-0.147,0.042l-0.146,0.044l-0.144,0.044l-0.286,0.088l-0.28,0.09l-0.274,0.091l-0.269,0.092l-0.263,0.093l-0.258,0.093l-0.251,0.093l-0.246,0.094l-0.239,0.094l-0.234,0.093l-0.227,0.093l-0.221,0.093l-0.214,0.092l-0.208,0.091l-0.201,0.09l-0.194,0.088l-0.188,0.088l-0.181,0.085l-0.173,0.084l-0.167,0.081l-0.159,0.08l-0.152,0.077l-0.145,0.074l-0.137,0.072l-0.13,0.069l-0.122,0.066l-0.114,0.062l-0.106,0.059l-0.189,0.107l-0.156,0.09l-0.123,0.073l-0.088,0.054l-0.072,0.045l14.489,14.667l14.5,-14.667l-0.072,-0.045l-0.089,-0.054l-0.122,-0.073l-0.157,-0.09l-0.189,-0.107l-0.106,-0.059l-0.115,-0.062l-0.122,-0.066l-0.129,-0.069l-0.138,-0.072l-0.145,-0.074l-0.152,-0.077l-0.16,-0.08l-0.166,-0.081l-0.174,-0.084l-0.181,-0.085l-0.188,-0.088l-0.195,-0.088l-0.201,-0.09l-0.208,-0.091l-0.215,-0.092l-0.221,-0.093l-0.227,-0.093l-0.234,-0.093l-0.239,-0.094l-0.246,-0.094l-0.252,-0.093l-0.258,-0.093l-0.263,-0.093l-0.269,-0.092l-0.275,-0.091l-0.28,-0.09l-0.286,-0.088l-0.145,-0.044l-0.146,-0.044l-0.147,-0.042l-0.149,-0.043l-0.15,-0.042l-0.151,-0.042l-0.153,-0.041l-0.154,-0.04l-0.155,-0.04l-0.156,-0.04l-0.157,-0.039l-0.159,-0.038l-0.159,-0.037l-0.161,-0.037l-0.163,-0.036l-0.163,-0.036l-0.164,-0.035l-0.166,-0.034l-0.166,-0.033l-0.168,-0.033l-0.169,-0.031l-0.17,-0.031l-0.171,-0.03l-0.172,-0.029l-0.173,-0.028l-0.174,-0.027l-0.175,-0.026l-0.176,-0.026l-0.177,-0.024l-0.178,-0.023l-0.179,-0.022l-0.18,-0.021l-0.181,-0.02l-0.182,-0.019l-0.183,-0.018l-0.183,-0.016l-0.185,-0.015l-0.185,-0.015l-0.187,-0.012l-0.187,-0.012l-0.188,-0.01l-0.189,-0.009l-0.19,-0.008l-0.19,-0.007l-0.191,-0.005l-0.192,-0.003l-0.193,-0.002l-0.194,-0.001l-0.194,0.001Z"
style="fill:#425f80;"/>
<path d="M20.091,14c-2.372,-0.657 0,5.333 0,5.333" style="fill:none;stroke:#fff;stroke-width:0.5px;"/>
<path d="M30.489,12.204l-0.853,1.129l-13.636,2l-13.636,-2l-0.853,-1.129c-0.339,1.238 -0.511,2.514 -0.511,3.796c0,8.095 6.721,14.667 15,14.667c8.279,0 15,-6.572 15,-14.667c0,-1.282 -0.172,-2.558 -0.511,-3.796Z"
style="fill:#ccd7e4;"/>
<path d="M1.682,18c0,0 6.137,5.5 14.318,5.5c8.181,0 14.318,-4.5 14.318,-4.5"
style="fill:none;stroke:#819cbc;stroke-width:1px;"/>
<path d="M1.682,18c0,0 6.137,5.333 14.318,5.333c8.181,0 14.318,-5.333 14.318,-5.333"
style="fill:none;stroke:#819cbc;stroke-width:1px;"/>
<path d="M1.682,18c0,0 6.137,4.5 14.318,4.5c8.181,0 14.318,-5.5 14.318,-5.5"
style="fill:none;stroke:#819cbc;stroke-width:1px;"/>
<path d="M1.682,18c0,0 6.137,4.333 14.318,4.333c8.181,0 14.318,-6.333 14.318,-6.333"
style="fill:none;stroke:#819cbc;stroke-width:1px;"/>
<path d="M1.682,18c0,0 6.137,3.5 14.318,3.5c8.181,0 14.318,-6.5 14.318,-6.5"
style="fill:none;stroke:#819cbc;stroke-width:1px;"/>
<path d="M1.511,12c-0.339,1.207 -0.511,2.451 -0.511,3.701c0,7.892 6.721,14.299 15,14.299c8.279,0 15,-6.407 15,-14.299c0,-1.25 -0.172,-2.494 -0.511,-3.701"
style="fill:none;stroke:#425f80;stroke-width:2px;"/>
<path d="M1,14c0,8.008 6.716,14.5 15,14.5c8.284,0 15,-6.492 15,-14.5"
style="fill:none;stroke:#425f80;stroke-width:1.5px;"/>
<path d="M1.511,12c0,0 6.21,4 14.489,4c8.279,0 14.489,-4 14.489,-4"
style="fill:none;stroke:#425f80;stroke-width:2px;"/>
<path d="M24.029,29.121l-0.862,-0.424l-2.571,-7.595l0.433,-0.842l5.179,-1.676l0.861,0.424l2.571,7.594l-0.433,0.843l-5.178,1.676Z"
style="fill:#be1515;stroke:#fff;stroke-width:0.5px;"/>
<path d="M27.069,19.008l2.571,7.594l-0.433,0.843l-5.178,1.676l-0.862,-0.424l-2.571,-7.595l0.433,-0.842l5.179,-1.676l0.861,0.424Zm-5.339,2.076l-0.051,0.099l2.302,6.8l0.125,0.061l4.401,-1.424l0.05,-0.098l-2.302,-6.8l-0.125,-0.062l-4.4,1.424Z"
style="fill:#fff;"/>
<path d="M26.279,17.587l0.064,0.006l0.063,0.011l0.063,0.015l0.061,0.019l0.06,0.022l0.059,0.027l0.861,0.423l0.054,0.028l0.051,0.032l0.05,0.035l0.048,0.037l0.045,0.041l0.043,0.043l0.039,0.045l0.038,0.048l0.034,0.05l0.031,0.052l0.028,0.054l0.024,0.055l0.021,0.057l2.571,7.595l0.018,0.059l0.015,0.06l0.01,0.06l0.007,0.062l0.003,0.061l-0.001,0.062l-0.004,0.061l-0.009,0.061l-0.012,0.061l-0.016,0.059l-0.019,0.059l-0.023,0.057l-0.026,0.055l-0.433,0.843l-0.03,0.053l-0.032,0.05l-0.035,0.049l-0.038,0.047l-0.041,0.044l-0.044,0.042l-0.045,0.039l-0.049,0.036l-0.05,0.033l-0.052,0.03l-0.054,0.027l-0.055,0.024l-0.057,0.02l-5.178,1.676l-0.062,0.018l-0.062,0.014l-0.064,0.009l-0.064,0.006l-0.064,0.002l-0.064,-0.003l-0.064,-0.007l-0.063,-0.01l-0.062,-0.015l-0.062,-0.019l-0.06,-0.022l-0.058,-0.027l-0.862,-0.423l-0.053,-0.029l-0.052,-0.031l-0.05,-0.035l-0.047,-0.037l-0.045,-0.041l-0.043,-0.043l-0.04,-0.045l-0.037,-0.048l-0.034,-0.05l-0.031,-0.052l-0.028,-0.054l-0.025,-0.055l-0.021,-0.057l-2.571,-7.595l-0.018,-0.059l-0.014,-0.06l-0.011,-0.06l-0.006,-0.062l-0.003,-0.061l0,-0.062l0.005,-0.061l0.008,-0.061l0.012,-0.061l0.016,-0.059l0.02,-0.059l0.023,-0.057l0.026,-0.056l0.433,-0.842l0.029,-0.053l0.032,-0.051l0.036,-0.048l0.038,-0.047l0.041,-0.044l0.043,-0.042l0.046,-0.039l0.048,-0.036l0.05,-0.033l0.052,-0.03l0.054,-0.027l0.056,-0.024l0.056,-0.02l5.179,-1.676l0.061,-0.018l0.063,-0.014l0.063,-0.01l0.064,-0.005l0.064,-0.002l0.064,0.003Zm-5.25,2.673l-0.433,0.842l2.571,7.595l0.862,0.424l5.178,-1.676l0.433,-0.843l-2.571,-7.594l-0.861,-0.424l-5.179,1.676Z"/>
<path d="M23.833,20.055c-1.48,-5.562 -3.742,-6.055 -3.742,-6.055"
style="fill:none;stroke:#fff;stroke-width:0.5px;"/></svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>TeaClient</title>
<link type="text/css" rel="stylesheet" href="index.css" />
<script type="module" src="index.js"></script>
</head>
<body>
<div class="container-logo">
<img class="logo" src="img/logo.svg" alt="logo" draggable="false">
<img class="smoke" src="img/smoke.png" alt="" draggable="false">
</div>
<div class="container-info">
<a id="loading-text">Loading... Please wait!</a>
<div class="container-bar">
<div class="bar" id="progress-indicator"></div>
</div>
<a id="current-status">&nbsp;</a>
</div>
</body>
</html>

View File

@ -0,0 +1,88 @@
html, body {
background: #18BC9C;
user-select: none;
-webkit-app-region: drag;
}
body {
text-align: center;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
margin-left: 18px;
margin-right: 18px;
display: flex;
flex-direction: column;
justify-content: center;
-ms-overflow-style: none;
}
img {
position: absolute;
display: block;
width: 200px;
height: 200px;
}
.smoke {
z-index: 2;
}
.logo {
z-index: 1;
}
.container-logo {
align-self: center;
position: relative;
display: inline-block;
width: 200px;
height: 200px;
}
.container-info a {
display: inline-block;
color: #FFFFFF;
font-family: "Arial",serif;
font-size: 20px;
}
.container-bar {
position: relative;
margin-top: 5px;
border: white solid 2px;
height: 18px;
}
.container-bar .bar {
z-index: 1;
position: absolute;
display: block;
background: whitesmoke;
border: none;
width: 0;
height: 100%;
}
#current-status {
margin-top: 3px;
font-size: 18px;
max-width: 100%;
width: 100%;
text-align: left;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}

View File

@ -0,0 +1,32 @@
import { ipcRenderer } from "electron";
const currentStatus = document.getElementById("current-status") as HTMLDivElement;
const progressIndicator = document.getElementById("progress-indicator") as HTMLDivElement;
const setStatusText = (text: string) => {
if(currentStatus) {
currentStatus.innerHTML = text;
}
}
const setProgressIndicator = (value: number) => {
if(progressIndicator) {
progressIndicator.style.width = (value * 100) + "%";
}
}
ipcRenderer.on('progress-update', (event, status, count) => {
console.log("Process update \"%s\" to %d", status, count);
setStatusText(status);
setProgressIndicator(count);
});
ipcRenderer.on('await-update', (event) => {
console.log("Received update notification");
setProgressIndicator(1);
setStatusText("Awaiting client update response<br>(User input required)");
});
export = {};

View File

@ -0,0 +1,223 @@
import {BrowserWindow, dialog} from "electron";
import * as url from "url";
import * as path from "path";
import {loadWindowBounds, startTrackWindowBounds} from "../../../../shared/window";
import {hideAppLoaderWindow} from "../../app-loader/controller/AppLoader";
import {
availableRemoteChannels, clientAppInfo, clientUpdateChannel,
currentClientVersion,
newestRemoteClientVersion, prepareUpdateExecute, setClientUpdateChannel,
UpdateVersion
} from "../../../app-updater";
import {mainWindow} from "../../../main-window";
import {closeMainWindow} from "../../main-window/controller/MainWindow";
const kDeveloperTools = true;
let windowInstance: BrowserWindow;
let windowSpawnPromise: Promise<void>;
let currentRemoteUpdateVersion: UpdateVersion;
let updateInstallExecuteCallback;
let updateInstallAbortCallback;
export async function showUpdateWindow() {
while(windowSpawnPromise) {
await windowSpawnPromise;
}
if(windowInstance) {
windowInstance.focus();
return;
}
windowSpawnPromise = doSpawnWindow().catch(error => {
console.error("Failed to open the client updater window: %o", error);
dialog.showErrorBox("Failed to open window", "Failed to open the client updater window.\nLookup the console for details.");
hideAppLoaderWindow();
});
/* do this after the assignment so in case the promise resolves instantly we still clear the assignment */
windowSpawnPromise.then(() => windowSpawnPromise = undefined);
await windowSpawnPromise;
console.error("Window created");
}
const kZoomFactor = 1;
async function doSpawnWindow() {
const kWindowWidth = kZoomFactor * 580 + (kDeveloperTools ? 1000 : 0);
const kWindowHeight = kZoomFactor * 800 + (process.platform == "win32" ? 40 : 0);
windowInstance = new BrowserWindow({
width: kWindowWidth,
height: kWindowHeight,
frame: kDeveloperTools,
resizable: kDeveloperTools,
show: false,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
zoomFactor: kZoomFactor
}
});
fatalErrorHandled = false;
targetRemoteVersion = undefined;
currentRemoteUpdateVersion = undefined;
windowInstance.setMenu(null);
windowInstance.on('closed', () => {
windowInstance = undefined;
if(updateInstallAbortCallback) {
/* cleanup */
updateInstallAbortCallback();
}
updateInstallAbortCallback = undefined;
updateInstallExecuteCallback = undefined;
});
if(kDeveloperTools) {
windowInstance.webContents.openDevTools();
}
initializeIpc();
await windowInstance.loadURL(url.pathToFileURL(path.join(__dirname, "..", "renderer", "index.html")).toString());
windowInstance.show();
try {
await loadWindowBounds('client-updater', windowInstance, undefined, { applySize: false });
startTrackWindowBounds('client-updater', windowInstance);
} catch (error) {
console.warn("Failed to load and track window bounds");
}
}
let fatalErrorHandled = false;
async function handleFatalError(error: string, popupMessage?: string) {
/* Show only one error at the time */
if(fatalErrorHandled) { return; }
fatalErrorHandled = true;
windowInstance?.webContents.send("client-updater-set-error", error);
await dialog.showMessageBox(windowInstance, {
type: "error",
buttons: ["Ok"],
message: "A critical error happened:\n" + (popupMessage || error)
});
fatalErrorHandled = false;
}
async function sendLocalInfo() {
try {
const localVersion = await currentClientVersion();
if(localVersion.isDevelopmentVersion()) {
windowInstance?.webContents.send("client-updater-local-status", "InDev", Date.now());
} else {
windowInstance?.webContents.send("client-updater-local-status", localVersion.toString(false), localVersion.timestamp);
}
} catch (error) {
console.error("Failed to query/send the local client version: %o", error);
handleFatalError("Failed to query local version").then(undefined);
}
}
let targetRemoteVersion: UpdateVersion;
function initializeIpc() {
windowInstance.webContents.on("ipc-message", (event, channel, ...args) => {
switch (channel) {
case "client-updater-close":
closeUpdateWindow();
break;
case "client-updater-query-local-info":
sendLocalInfo().then(undefined);
break;
case "client-updater-query-remote-info":
newestRemoteClientVersion(clientUpdateChannel()).then(async result => {
currentRemoteUpdateVersion = result;
if(!result) {
await handleFatalError("No remote update info.");
return;
}
const localVersion = await currentClientVersion();
const updateAvailable = !localVersion.isDevelopmentVersion() && (result.version.newerThan(localVersion) || result.channel !== clientAppInfo().clientChannel);
targetRemoteVersion = updateAvailable ? result : undefined;
windowInstance?.webContents.send("client-updater-remote-status",
!localVersion.isDevelopmentVersion() && result.version.newerThan(localVersion),
result.version.toString(false),
result.version.timestamp
);
}).catch(async error => {
currentRemoteUpdateVersion = undefined;
console.error("Failed to query remote client version: %o", error);
await handleFatalError("Failed to query server info.", typeof error === "string" ? error : undefined);
});
break;
case "client-updater-query-channels":
availableRemoteChannels().then(channels => {
windowInstance?.webContents.send("client-updater-channel-info", channels, clientUpdateChannel());
}).catch(async error => {
console.error("Failed to query available channels %o", error);
await handleFatalError("Failed to query available channels.", typeof error === "string" ? error : undefined);
});
break;
case "client-updater-set-channel":
setClientUpdateChannel(args[0] || "release");
break;
case "execute-update":
doExecuteUpdate();
break;
case "install-update":
updateInstallExecuteCallback();
break;
default:
/* nothing to do */
break;
}
});
}
function doExecuteUpdate() {
windowInstance?.webContents.send("client-updater-execute");
if(!currentRemoteUpdateVersion) {
windowInstance?.webContents.send("client-updater-execute-finish", "Missing target version");
return;
}
closeMainWindow(true);
prepareUpdateExecute(currentRemoteUpdateVersion, (message, progress) => {
windowInstance?.webContents.send("client-updater-execute-progress", message, progress);
}, (type, message) => {
windowInstance?.webContents.send("client-updater-execute-log", type, message);
}).then(callbacks => {
updateInstallExecuteCallback = callbacks.callbackExecute;
updateInstallAbortCallback = callbacks.callbackAbort;
windowInstance?.webContents.send("client-updater-execute-finish");
}).catch(error => {
windowInstance?.webContents.send("client-updater-execute-finish", error);
});
}
export function closeUpdateWindow() {
(async () => {
await windowSpawnPromise;
if(windowInstance) {
windowInstance.close();
windowInstance = undefined;
}
})();
}

View File

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Updating app</title>
<script type="module" src="index.js"></script>
<link rel="stylesheet" href="index.css">
</head>
<body>
<div class="container">
<div class="logo">
<img alt="TeaSpeak - Updater" src="logo.png" draggable="false" />
</div>
<div class="body">
<div class="container-loading" id="container-info">
<div class="section local">
<div class="title">Client Version</div>
<div class="content">
<div class="row">
<div class="key">Client Version</div>
<div class="value" id="local-client-version"></div>
</div>
<div class="row">
<div class="key">Build Timestamp</div>
<div class="value" id="local-build-timestamp"></div>
</div>
<div class="row">
<div class="key">Channel</div>
<div class="value">
<!-- Label not used ;) -->
<label for="update-channel"></label>
<select id="update-channel">
<option value="loading" style="display: none" selected></option>
</select>
</div>
</div>
</div>
</div>
<div class="section remote">
<div class="title">Latest Version</div>
<div class="content">
<div class="row">
<div class="key">Client Version</div>
<div class="value" id="remote-client-version"></div>
</div>
<div class="row">
<div class="key">Build Timestamp</div>
<div class="value" id="remote-build-timestamp"></div>
</div>
</div>
</div>
<div class="update-availability-status" id="update-availability-status">
<div class="content unavailable">
<img src="unavailable.svg" alt="Update unavailable" />
<div>
<h2>Update unavailable!</h2>
<h3>You can't update your client.</h3>
</div>
</div>
<div class="content available">
<img src="update.svg" alt="Update available" />
<div>
<h2>Update available!</h2>
<h3>Update your client to 1.5.1.</h3>
</div>
</div>
<div class="content up2date">
<img src="up2date.svg" alt="Client up2date" />
<div>
<h2>No update available.</h2>
<h3>Your client is up to date!</h3>
</div>
</div>
<div class="content loading shown">
<h2>loading&nbsp;<div class="loading-dots"></div></h2>
</div>
</div>
</div>
<div class="container-executing" id="container-execute">
<div class="update-progress" id="update-execute-progress">
<div class="info">Loading client update</div>
<div class="bar-container type-normal">
<div class="filler" style="width: 50%"></div>
<div class="text">50%</div>
</div>
</div>
<div class="update-log" id="update-execute-log">
</div>
</div>
<div class="buttons">
<button class="btn btn-red" id="button-cancel">Cancel</button>
<button class="btn btn-green" id="button-submit">Update Client</button>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,407 @@
html:root {
--progress-bar-background: #242527;
--progress-bar-filler-normal: #4370a299;
--progress-bar-filler-error: #a1000099;
--progress-bar-filler-success: #2b854199;
}
* {
box-sizing: border-box;
outline: none;
}
html {
display: flex;
flex-direction: row;
justify-content: center;
user-select: none;
background: #2f2f35;
font-size: 12px;
width: 100vw;
height: 100vh;
position: relative;
}
$window-margin: 2em;
body {
display: flex;
flex-direction: row;
justify-content: center;
margin: 0;
position: absolute;
top: 1em;
right: 1em;
left: 1em;
bottom: 1.75em;
font-family: Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6em;
-webkit-app-region: drag;
}
.loading-dots {
width: 2em;
}
.container {
display: flex;
flex-direction: column;
justify-content: stretch;
}
.logo {
align-self: center;
width: 30em;
margin-left: -1em;
margin-right: -1em;
img {
height: 100%;
width: 100%;
}
}
.body {
flex-grow: 1;
flex-shrink: 1;
min-height: 6em;
display: flex;
flex-direction: column;
justify-content: flex-start;
-webkit-app-region: no-drag;
.buttons {
margin-top: auto;
display: flex;
flex-direction: row;
justify-content: space-between;
&.btn-green {
margin-left: auto;
}
}
}
.container-loading, .container-executing {
margin-top: 2em;
flex-grow: 1;
flex-shrink: 1;
min-height: 6em;
display: none;
flex-direction: column;
justify-content: stretch;
&.shown {
display: flex;
}
}
.section {
&.remote {
margin-top: 2em;
}
.title {
font-size: 1.2em;
color: #557edc;
text-transform: uppercase;
align-self: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.content {
color: #999;
display: flex;
flex-direction: column;
justify-content: flex-start;
.row {
display: flex;
flex-direction: row;
justify-content: space-between;
}
}
}
.update-availability-status {
position: relative;
display: flex;
flex-grow: 1;
.content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: row;
justify-content: center;
opacity: 0;
pointer-events: none;
&.shown {
pointer-events: all;
opacity: 1;
}
img {
width: 5em;
height: 5em;
margin-right: 1em;
align-self: center;
}
> div {
display: flex;
flex-direction: column;
justify-content: center;
* {
margin: 0;
}
}
h2 {
color: #999;
}
h3 {
margin-top: .1em;
color: #999;
}
&.available {
h2 { color: #1ca037 }
> img { margin-top: -.5em; }
}
&.loading {
h2 {
display: flex;
flex-direction: row;
font-weight: normal;
align-self: center;
}
}
&.unavailable {
h2 { color: #c90709 }
}
}
}
.update-progress {
display: flex;
flex-direction: column;
justify-content: flex-start;
.info {
color: #999;
font-size: 1.2em;
margin-bottom: .2em;
}
.bar-container {
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
height: 1.4em;
border-radius: 0.2em;
overflow: hidden;
background-color: var(--progress-bar-background);
-webkit-box-shadow: inset 0 0 2px 0 rgba(0, 0, 0, 0.75);
-moz-box-shadow: inset 0 0 2px 0 rgba(0, 0, 0, 0.75);
box-shadow: inset 0 0 2px 0 rgba(0, 0, 0, 0.75);
.filler {
position: absolute;
top: 0;
left: 0;
bottom: 0;
transition: .3s ease-in-out;
}
.text {
color: #999;
align-self: center;
z-index: 1;
}
&.type-normal {
.filler {
background-color: var(--progress-bar-filler-normal);
}
}
&.type-error {
.filler {
background-color: var(--progress-bar-filler-error);
}
}
&.type-success {
.filler {
background-color: var(--progress-bar-filler-success);
}
}
}
}
.update-log {
display: flex;
flex-direction: column;
justify-content: flex-start;
padding: .25em .5em;
border-radius: 0.2em;
flex-grow: 1;
flex-shrink: 1;
min-height: 2em;
margin-top: 1em;
margin-bottom: 1em;
overflow-x: hidden;
overflow-y: auto;
background-color: var(--progress-bar-background);
-webkit-box-shadow: inset 0 0 2px 0 rgba(0, 0, 0, 0.75);
-moz-box-shadow: inset 0 0 2px 0 rgba(0, 0, 0, 0.75);
box-shadow: inset 0 0 2px 0 rgba(0, 0, 0, 0.75);
color: #999;
user-select: text;
/* Scroll bar */
&::-webkit-scrollbar-track {
border-radius: .25em;
background-color: transparent;
cursor: pointer;
}
&::-webkit-scrollbar {
width: .5em;
height: .5em;
background-color: transparent;
cursor: pointer;
}
&::-webkit-scrollbar-thumb {
border-radius: .25em;
background-color: #555;
}
&::-webkit-scrollbar-corner {
//background: #19191b;
background-color: transparent;
}
/* End scroll bar */
.filler {
margin-top: auto;
}
.message {
display: inline-block;
word-break: break-all;
&.error {
color: #c90709;
}
&.centered {
align-self: center;
}
}
}
/* button look */
.btn {
cursor: pointer;
background-color: rgba(0, 0, 0, 0.5);
border-width: 0;
border-radius: .2em;
border-style: solid;
color: #7c7c7c;
padding: .25em 1em;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12);
&:hover {
background-color: #0a0a0a;
}
&:disabled {
box-shadow: none;
background-color: rgba(0, 0, 0, 0.27);
&:hover {
background-color: rgba(0, 0, 0, 0.27);
}
}
&.btn-success, &.btn-green {
border-bottom-width: 2px;
border-bottom-color: #389738;
}
&.btn-info, &.btn-blue {
border-bottom-width: 2px;
border-bottom-color: #386896;
}
&.btn-red {
border-bottom-width: 2px;
border-bottom-color: #973838;
}
transition: background-color .3s ease-in-out;
}
select {
outline: none;
background: transparent;
border: none;
color: #999;
}

View File

@ -0,0 +1,271 @@
import {
ipcRenderer
} from "electron";
import moment = require("moment");
const buttonCancel = document.getElementById("button-cancel");
const buttonSubmit = document.getElementById("button-submit");
const containerUpdateInfo = document.getElementById("container-info");
const containerUpdateExecute = document.getElementById("container-execute");
const updateStatusContainer = document.getElementById("update-availability-status");
const updateChannelSelect = document.getElementById("update-channel") as HTMLSelectElement;
const updateExecuteLog = document.getElementById("update-execute-log");
const updateExecuteProgress = document.getElementById("update-execute-progress");
let dotIndex = 0;
setInterval(() => {
dotIndex++;
let dots = ".";
for(let index = 0; index < dotIndex % 3; index++) { dots += "."; }
for(const dotContainer of document.getElementsByClassName("loading-dots")) {
dotContainer.innerHTML = dots;
}
}, 500);
const resetUpdateChannelDropdown = () => {
while(updateChannelSelect.options.length > 0) {
updateChannelSelect.options.remove(0);
}
for(const defaultOption of [{ text: "", value: "loading"}, {text: "???", value: "unknown" }]) {
const element = document.createElement("option");
element.text = defaultOption.text;
element.value = defaultOption.value;
element.style.display = "none";
updateChannelSelect.options.add(element);
}
updateChannelSelect.onchange = undefined;
updateChannelSelect.value = "loading";
}
ipcRenderer.on("client-updater-channel-info", (_event, available: string[], current: string) => {
resetUpdateChannelDropdown();
if(available.indexOf(current) === -1) {
available.push(current);
}
for(const channel of available) {
const element = document.createElement("option");
element.text = channel;
element.value = channel;
updateChannelSelect.options.add(element);
}
updateChannelSelect.value = current;
updateChannelSelect.onchange = () => {
const value = updateChannelSelect.value;
if(value === "loading" || value === "unknown") {
return;
}
console.error("Update channel changed to %o", value);
ipcRenderer.send("client-updater-set-channel", value);
initializeVersionsView(false);
}
});
ipcRenderer.on("client-updater-local-status", (_event, localVersion: string, buildTimestamp: number) => {
document.getElementById("local-client-version").innerHTML = localVersion;
document.getElementById("local-build-timestamp").innerHTML = moment(buildTimestamp).format("LTS, LL");
});
ipcRenderer.on("client-updater-set-error", (_event, message) => {
for(const child of updateStatusContainer.querySelectorAll(".shown")) {
child.classList.remove("shown");
}
const unavailableContainer = updateStatusContainer.querySelector(".unavailable");
if(unavailableContainer) {
unavailableContainer.classList.add("shown");
const h2 = unavailableContainer.querySelector("h2");
const h3 = unavailableContainer.querySelector("h3");
if(h2) {
h2.innerHTML = "Update failed!";
}
if(h3) {
h3.innerHTML = message;
}
}
/* TODO: Find out the current view and set the error */
buttonSubmit.style.display = "none";
buttonCancel.innerHTML = "Close";
});
const resetRemoteInfo = () => {
document.getElementById("remote-client-version").innerText = "";
document.getElementById("remote-build-timestamp").innerText = "";
}
ipcRenderer.on("client-updater-remote-status", (_event, updateAvailable: boolean, version: string, timestamp: number) => {
resetRemoteInfo();
for(const child of updateStatusContainer.querySelectorAll(".shown")) {
child.classList.remove("shown");
}
updateStatusContainer.querySelector(updateAvailable ? ".available" : ".up2date")?.classList.add("shown");
document.getElementById("remote-client-version").innerText = version;
document.getElementById("remote-build-timestamp").innerText = moment(timestamp).format("LTS, LL");
if(updateAvailable) {
const h3 = updateStatusContainer.querySelector(".available h3");
if(h3) {
h3.innerHTML = "Update your client to " + version + ".";
}
buttonSubmit.innerHTML = "Update Client";
buttonSubmit.style.display = null;
}
});
function currentLogDate() : string {
const now = new Date();
return "<" + ("00" + now.getHours()).substr(-2) + ":" + ("00" + now.getMinutes()).substr(-2) + ":" + ("00" + now.getSeconds()).substr(-2) + "> ";
}
let followBottom = true;
let followBottomAnimationFrame;
const logUpdateExecuteInfo = (type: "info" | "error", message: string, extraClasses?: string[]) => {
const element = document.createElement("div");
if(message.length === 0) {
element.innerHTML = "&nbsp;";
} else {
element.textContent = (!extraClasses?.length ? currentLogDate() + " " : "") + message;
}
element.classList.add("message", type, ...(extraClasses ||[]));
updateExecuteLog.appendChild(element);
if(!followBottomAnimationFrame && followBottom) {
followBottomAnimationFrame = requestAnimationFrame(() => {
followBottomAnimationFrame = undefined;
if(!followBottom) { return; }
updateExecuteLog.scrollTop = updateExecuteLog.scrollHeight;
});
}
}
updateExecuteLog.onscroll = () => {
const bottomOffset = updateExecuteLog.scrollTop + updateExecuteLog.clientHeight;
followBottom = bottomOffset + 50 > updateExecuteLog.scrollHeight;
};
ipcRenderer.on("client-updater-execute", () => initializeExecuteView());
ipcRenderer.on("client-updater-execute-log", (_event, type: "info" | "error", message: string) => {
message.split("\n").forEach(line => logUpdateExecuteInfo(type, line))
});
const setExecuteProgress = (status: "normal" | "error" | "success", message: string, progress: number) => {
const barContainer = updateExecuteProgress.querySelector(".bar-container") as HTMLDivElement;
if(barContainer) {
[...barContainer.classList].filter(e => e.startsWith("type-")).forEach(klass => barContainer.classList.remove(klass));
barContainer.classList.add("type-" + status);
}
const progressFiller = updateExecuteProgress.querySelector(".filler") as HTMLDivElement;
if(progressFiller) {
progressFiller.style.width = (progress * 100) + "%";
}
const progressText = updateExecuteProgress.querySelector(".text") as HTMLDivElement;
if(progressText) {
progressText.textContent = (progress * 100).toFixed() + "%";
}
const progressInfo = updateExecuteProgress.querySelector(".info") as HTMLDivElement;
if(progressInfo) {
progressInfo.textContent = message;
}
}
ipcRenderer.on("client-updater-execute-progress", (_event, message: string, progress: number) => setExecuteProgress("normal", message, progress));
ipcRenderer.on("client-updater-execute-finish", (_event, error: string | undefined) => {
logUpdateExecuteInfo("info", "");
logUpdateExecuteInfo("info", "Update result", ["centered"]);
logUpdateExecuteInfo("info", "");
buttonCancel.style.display = null;
if(error) {
/* Update failed */
logUpdateExecuteInfo("error", "Failed to execute update: " + error);
setExecuteProgress("error", "Update failed", 1);
buttonSubmit.textContent = "Retry";
buttonSubmit.style.display = null;
buttonSubmit.onclick = () => initializeVersionsView(true);
buttonCancel.textContent = "Close";
} else {
setExecuteProgress("success", "Update loaded", 1);
logUpdateExecuteInfo("info", "Update successfully loaded.");
logUpdateExecuteInfo("info", "Click \"Install Update\" to update your client.");
buttonSubmit.textContent = "Install Update";
buttonSubmit.style.display = null;
buttonSubmit.onclick = () => ipcRenderer.send("install-update");
buttonCancel.textContent = "Abort Update";
}
});
buttonCancel.onclick = () => {
ipcRenderer.send("client-updater-close");
};
const initializeExecuteView = () => {
while(updateExecuteLog.firstChild) {
updateExecuteLog.removeChild(updateExecuteLog.firstChild);
}
{
const filler = document.createElement("div");
filler.classList.add("filler");
updateExecuteLog.appendChild(filler);
}
setExecuteProgress("normal", "Loading client update", 0);
containerUpdateExecute.classList.add("shown");
containerUpdateInfo.classList.remove("shown");
buttonCancel.style.display = "none";
buttonSubmit.onclick = undefined;
}
const initializeVersionsView = (queryLocalInfo: boolean) => {
containerUpdateExecute.classList.remove("shown");
containerUpdateInfo.classList.add("shown");
for(const child of updateStatusContainer.querySelectorAll(".shown")) {
child.classList.remove("shown");
}
updateStatusContainer.querySelector(".loading")?.classList.add("shown");
resetUpdateChannelDropdown();
resetRemoteInfo();
if(queryLocalInfo) {
ipcRenderer.send("client-updater-query-local-info");
}
ipcRenderer.send("client-updater-query-channels");
ipcRenderer.send("client-updater-query-remote-info");
buttonSubmit.onclick = () => ipcRenderer.send("execute-update");
buttonSubmit.style.display = "none";
buttonCancel.innerHTML = "Close";
}
initializeVersionsView(true);
export = {};

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="client-delete" width="16"
height="16" viewBox="0 0 16 16" x="352" y="96">
<path fill="#c90709"
d="M13.658 2.855c0.187 0.187 0.283 0.409 0.278 0.666 0.001 0.254-0.092 0.474-0.278 0.661l-3.821 3.821 3.82 3.82c0.186 0.186 0.279 0.407 0.28 0.659-0.001 0.259-0.092 0.481-0.279 0.668l-0.509 0.509c-0.187 0.187-0.41 0.278-0.671 0.278-0.258-0.007-0.478-0.099-0.658-0.278l-3.819-3.82-3.819 3.82c-0.187 0.186-0.409 0.281-0.662 0.279-0.257 0.005-0.479-0.091-0.667-0.278l-0.509-0.509c-0.188-0.187-0.28-0.413-0.276-0.669 0.005-0.259 0.094-0.477 0.277-0.659l3.819-3.82-3.821-3.821c-0.187-0.187-0.28-0.408-0.282-0.662 0.002-0.258 0.095-0.478 0.283-0.665l0.509-0.509c0.187-0.188 0.406-0.281 0.664-0.282 0.254 0.003 0.475 0.096 0.662 0.282l3.821 3.821 3.821-3.821c0.186-0.186 0.407-0.279 0.661-0.278 0.257-0.005 0.478 0.090 0.665 0.278l0.509 0.509z"></path>
</svg>

After

Width:  |  Height:  |  Size: 992 B

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="client-apply" width="16"
height="16" viewBox="0 0 16 16" x="352" y="0">
<path fill="#1ca037"
d="M2.539 8.041c0.169-0.168 0.369-0.255 0.6-0.25 0.228-0.002 0.427 0.082 0.595 0.25l1.924 1.924 6.608-6.608c0.168-0.168 0.366-0.251 0.595-0.253 0.233 0.001 0.432 0.083 0.601 0.251l0.458 0.458c0.169 0.169 0.25 0.37 0.25 0.604-0.006 0.232-0.089 0.431-0.251 0.592 0 0-7.421 7.421-7.534 7.534s-0.352 0.351-0.726 0.351c-0.374 0-0.622-0.247-0.726-0.351s-2.851-2.851-2.851-2.851c-0.168-0.168-0.251-0.367-0.25-0.595-0.004-0.231 0.082-0.431 0.25-0.599l0.458-0.458z"></path>
</svg>

After

Width:  |  Height:  |  Size: 709 B

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="client-download" width="16"
height="16" viewBox="0 0 16 16" x="480" y="96">
<path fill="#a9aaac"
d="M3.723 12.269h8.565v-2.287h2.6v2.516l-0 0.025-0.001 0.030-0.001 0.029-0.001 0.028-0.002 0.029-0.002 0.029-0.002 0.029-0.002 0.029-0.003 0.027-0.003 0.029-0.004 0.028-0.004 0.028-0.004 0.029-0.004 0.027-0.005 0.028-0.005 0.027-0.005 0.028-0.006 0.028-0.006 0.027-0.006 0.028-0.007 0.027-0.007 0.027-0.007 0.027-0.007 0.027-0.008 0.027-0.008 0.027-0.008 0.026-0.008 0.026-0.009 0.027-0.009 0.026-0.009 0.026-0.010 0.026-0.010 0.026-0.011 0.026-0.011 0.025-0.011 0.025-0.011 0.026-0.011 0.025-0.012 0.025-0.012 0.025-0.012 0.025-0.013 0.025-0.012 0.024-0.013 0.025-0.014 0.025-0.014 0.024-0.014 0.024-0.014 0.024-0.014 0.023-0.015 0.024-0.015 0.024-0.015 0.023-0.016 0.023-0.016 0.022-0.016 0.023-0.017 0.023-0.017 0.022-0.017 0.021-0.017 0.022-0.018 0.021-0.018 0.021-0.018 0.021-0.019 0.021-0.018 0.020-0.019 0.021-0.019 0.020-0.019 0.020-0.020 0.020-0.020 0.020-0.020 0.019-0.020 0.019-0.021 0.019-0.021 0.019-0.021 0.018-0.021 0.018-0.022 0.018-0.023 0.018-0.022 0.017-0.022 0.017-0.023 0.017-0.023 0.017-0.023 0.016-0.023 0.016-0.024 0.016-0.024 0.016-0.024 0.015-0.024 0.015-0.024 0.015-0.024 0.014-0.025 0.014-0.025 0.014-0.025 0.014-0.026 0.013-0.026 0.013-0.026 0.013-0.027 0.013-0.026 0.012-0.027 0.012-0.027 0.012-0.027 0.011-0.026 0.010-0.028 0.011-0.027 0.010-0.028 0.010-0.029 0.009-0.027 0.009-0.028 0.009-0.029 0.008-0.030 0.008-0.028 0.007-0.029 0.007-0.030 0.007-0.029 0.006-0.029 0.006-0.029 0.005-0.030 0.005-0.030 0.005-0.030 0.004-0.031 0.004-0.030 0.004-0.029 0.003-0.031 0.003-0.031 0.002-0.030 0.002-0.031 0.002-0.031 0.001-0.031 0.001-0.026 0-9.58-0.030-0.030-0.005-0.029-0.005-0.029-0.006-0.029-0.006-0.030-0.007-0.029-0.007-0.028-0.007-0.030-0.008-0.029-0.008-0.028-0.008-0.028-0.009-0.029-0.009-0.028-0.010-0.027-0.010-0.028-0.011-0.027-0.011-0.026-0.011-0.027-0.012-0.027-0.012-0.026-0.012-0.027-0.013-0.026-0.013-0.026-0.013-0.026-0.013-0.025-0.013-0.026-0.014-0.025-0.015-0.024-0.014-0.024-0.014-0.025-0.015-0.024-0.015-0.024-0.016-0.024-0.016-0.023-0.016-0.022-0.016-0.023-0.017-0.023-0.017-0.021-0.017-0.022-0.018-0.023-0.018-0.021-0.018-0.021-0.018-0.021-0.018-0.021-0.019-0.021-0.019-0.020-0.019-0.020-0.019-0.020-0.020-0.020-0.020-0.019-0.020-0.019-0.020-0.018-0.020-0.019-0.021-0.019-0.021-0.018-0.021-0.017-0.020-0.018-0.022-0.018-0.022-0.017-0.022-0.016-0.022-0.016-0.022-0.016-0.023-0.016-0.023-0.016-0.023-0.016-0.023-0.015-0.023-0.015-0.024-0.015-0.024-0.014-0.023-0.014-0.024-0.014-0.024-0.014-0.025-0.013-0.024-0.013-0.024-0.013-0.025-0.012-0.025-0.012-0.025-0.012-0.025-0.011-0.025-0.011-0.025-0.011-0.026-0.011-0.026-0.010-0.026-0.010-0.025-0.010-0.026-0.010-0.026-0.009-0.026-0.009-0.026-0.009-0.026-0.008-0.027-0.008-0.027-0.008-0.027-0.007-0.026-0.007-0.027-0.007-0.028-0.006-0.027-0.006-0.027-0.006-0.028-0.006-0.027-0.005-0.027-0.005-0.028-0.005-0.028-0.004-0.028-0.004-0.028-0.004-0.028-0.003-0.028-0.003-0.028-0.003-0.029-0.002-0.028-0.002-0.029-0.002-0.029-0.002-0.028-0.001-0.030-0.001-0.029-0-0.028-0-0.025v-2.517h2.6v2.287z"></path>
<path fill="#7289da"
d="M4.766 5.863l3.358 4.119 3.359-4.119h-2.060v-3.798c0.001-0.257-0.090-0.475-0.27-0.661-0.178-0.184-0.403-0.278-0.668-0.278l-0.72-0c-0.265 0-0.489 0.090-0.668 0.274-0.181 0.178-0.27 0.402-0.271 0.666v3.797l-2.061 0z"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,111 @@
import {app, BrowserWindow, dialog} from "electron";
import {dereferenceApp, referenceApp} from "../../../AppInstance";
import {closeURLPreview, openURLPreview} from "../../../url-preview";
import {loadWindowBounds, startTrackWindowBounds} from "../../../../shared/window";
import {Arguments, processArguments} from "../../../../shared/process-arguments";
import {allow_dev_tools} from "../../../main-window";
import * as path from "path";
let windowInstance: BrowserWindow;
export async function showMainWindow(entryPointUrl: string) {
if(windowInstance) {
throw "main window already initialized";
}
// Create the browser window.
console.log("Spawning main window");
referenceApp(); /* main browser window references the app */
windowInstance = new BrowserWindow({
width: 800,
height: 600,
minHeight: 600,
minWidth: 600,
show: false,
webPreferences: {
webSecurity: false,
nodeIntegrationInWorker: true,
nodeIntegration: true,
preload: path.join(__dirname, "..", "renderer", "PreloadScript.js")
},
icon: path.join(__dirname, "..", "..", "..", "..", "resources", "logo.ico"),
});
windowInstance.webContents.on("certificate-error", (event, url, error, certificate, callback) => {
console.log("Allowing untrusted certificate for %o", url);
event.preventDefault();
callback(true);
});
windowInstance.on('closed', () => {
windowInstance = undefined;
app.releaseSingleInstanceLock();
closeURLPreview().then(undefined);
dereferenceApp();
});
windowInstance.webContents.on('new-window', (event, urlString, frameName, disposition, options, additionalFeatures) => {
if(frameName.startsWith("__modal_external__")) {
return;
}
event.preventDefault();
try {
let url: URL;
try {
url = new URL(urlString);
} catch(error) {
throw "failed to parse URL";
}
{
let protocol = url.protocol.endsWith(":") ? url.protocol.substring(0, url.protocol.length - 1) : url.protocol;
if(protocol !== "https" && protocol !== "http") {
throw "invalid protocol (" + protocol + "). HTTP(S) are only supported!";
}
}
openURLPreview(urlString).then(() => {});
} catch(error) {
console.error("Failed to open preview window for URL %s: %o", urlString, error);
dialog.showErrorBox("Failed to open preview", "Failed to open preview URL: " + urlString + "\nError: " + error);
}
});
windowInstance.webContents.on('crashed', () => {
console.error("UI thread crashed! Closing app!");
if(!processArguments.has_flag(Arguments.DEBUG)) {
windowInstance.close();
}
});
try {
await windowInstance.loadURL(entryPointUrl);
} catch (error) {
console.error("Failed to load UI entry point (%s): %o", entryPointUrl, error);
throw "failed to load entry point";
}
windowInstance.show();
loadWindowBounds('main-window', windowInstance).then(() => {
startTrackWindowBounds('main-window', windowInstance);
windowInstance.focus();
if(allow_dev_tools && !windowInstance.webContents.isDevToolsOpened()) {
windowInstance.webContents.openDevTools();
}
});
}
export function closeMainWindow(force: boolean) {
windowInstance?.close();
if(force) {
windowInstance?.destroy();
}
}

View File

@ -6,7 +6,7 @@ declare global {
}
}
window.__native_client_init_hook = () => require("../../renderer/index");
window.__native_client_init_hook = () => require("../../../../renderer/index");
window.__native_client_init_shared = webpackRequire => window["shared-require"] = webpackRequire;
export = {};

View File

@ -1,5 +1,5 @@
require("../shared/require").setup_require(module);
import {app, BrowserWindow, remote} from "electron";
import {app, BrowserWindow, dialog, remote} from "electron";
import * as path from "path";
import * as electron from "electron";
import * as os from "os";
@ -18,24 +18,24 @@ export function handle_crash_callback(args: string[]) {
}
console.log("Received crash dump callback. Arguments: %o", parameter);
let error = undefined;
let crash_file = undefined;
let error;
let crashFile;
if(parameter["success"] == true) {
/* okey we have an crash dump */
crash_file = parameter["dump_path"];
if(typeof(crash_file) === "string") {
crashFile = parameter["dump_path"];
if(typeof(crashFile) === "string") {
try {
crash_file = Buffer.from(crash_file, 'base64').toString();
crashFile = Buffer.from(crashFile, 'base64').toString();
} catch(error) {
console.warn("Failed to decode dump path: %o", error);
crash_file = undefined;
crashFile = undefined;
error = "failed to decode dump path!";
}
}
} else if(typeof(parameter["error"]) === "string") {
try {
error = Buffer.from(crash_file, 'base64').toString();
error = Buffer.from(parameter["error"], 'base64').toString();
} catch(error) {
console.warn("Failed to decode error: %o", error);
error = "failed to decode error";
@ -45,7 +45,7 @@ export function handle_crash_callback(args: string[]) {
}
app.on('ready', () => {
const crash_window = new BrowserWindow({
const crashWindow = new BrowserWindow({
show: false,
width: 1000,
height: 300 + (os.platform() === "win32" ? 50 : 0),
@ -56,30 +56,38 @@ export function handle_crash_callback(args: string[]) {
javascript: true
}
});
crash_window.on('focus', event => crash_window.flashFrame(false));
crashWindow.on('focus', event => crashWindow.flashFrame(false));
crash_window.setMenu(null);
crash_window.loadURL(url.pathToFileURL(path.join(path.dirname(module.filename), "ui", "index.html")).toString());
crash_window.on('ready-to-show', () => {
if(error)
crash_window.webContents.send('dump-error', error);
else if(!crash_file)
crash_window.webContents.send('dump-error', "Missing crash file");
else
crash_window.webContents.send('dump-url', crash_file);
crash_window.show();
crash_window.setProgressBar(1, {mode: "error"});
crash_window.flashFrame(true);
crashWindow.setMenu(null);
crashWindow.loadURL(url.pathToFileURL(path.join(path.dirname(module.filename), "ui", "index.html")).toString()).catch(error => {
dialog.showErrorBox("Crash window failed to load", "Failed to load the crash window.\nThis indicates that something went incredible wrong.\n\nError:\n" + error);
});
crashWindow.on('ready-to-show', () => {
if(error) {
crashWindow.webContents.send('dump-error', error);
} else if(!crashFile) {
crashWindow.webContents.send('dump-error', "Missing crash file");
} else {
crashWindow.webContents.send('dump-url', crashFile);
}
crashWindow.show();
crashWindow.setProgressBar(1, { mode: "error" });
crashWindow.flashFrame(true);
});
app.on('window-all-closed', () => {
process.exit(0);
});
});
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
}
export const handler = require( "teaclient_crash_handler");
if(typeof window === "object")
export const handler = require("teaclient_crash_handler");
if(typeof window === "object") {
(window as any).crash = handler;
}
export function initialize_handler(component_name: string, requires_file: boolean) {
const start_path = requires_file ? (" " + path.join(__dirname, "..", "..")) : "";

View File

@ -8,7 +8,7 @@
<body>
<div class="container">
<div class="container-header">
<img src="crash_logo.svg">
<img src="crash_logo.svg" alt="TeaClient - Crashed">
<div class="text">
<h1>Ooops, something went incredible wrong!</h1>
<h2>It seems like your TeaSpeak Client has been crashed.</h2>
@ -17,7 +17,7 @@
<div class="container-body">
<p>
Please report this crash to TeaSpeak and help improving the client!<br>
Official issue and bug tracker url: <a href="#" onclick="open_issue_tracker(); return false;">https://github.com/TeaSpeak/TeaClient/issues</a><br>
Official issue and bug tracker url: <a href="#" onclick="openIssueTracker(); return false;">https://github.com/TeaSpeak/TeaClient/issues</a><br>
<b>Attention:</b> Crash reports without a crash dump file will be ignored!
</p>
<p class="error-hide">

View File

@ -1,6 +1,6 @@
import { shell, ipcRenderer } from "electron";
function open_issue_tracker() {
function openIssueTracker() {
shell.openExternal("https://github.com/TeaSpeak/TeaClient/issues");
}

View File

@ -4,24 +4,17 @@ import {AddressTarget, ResolveOptions} from "tc-shared/dns";
import * as dns_handler from "tc-native/dns";
import {ServerAddress} from "tc-shared/tree/Server";
export async function resolve_address(address: ServerAddress, _options?: ResolveOptions) : Promise<AddressTarget> {
/* backwards compatibility */
if(typeof(address) === "string") {
address = {
host: address,
port: 9987
}
}
export function resolve_address(address: ServerAddress, _options?: ResolveOptions) : Promise<AddressTarget> {
return new Promise<AddressTarget>((resolve, reject) => {
dns_handler.resolve_cr(address.host, address.port, result => {
if(typeof(result) === "string")
if(typeof(result) === "string") {
reject(result);
else
} else {
resolve({
target_ip: result.host,
target_port: result.port
});
}
});
})
}

View File

@ -103,7 +103,7 @@ export class ObjectProxyClient<ObjectType extends ProxyInterface<ObjectType>> {
}) as any;
}
private handleIPCMessage(event: IpcRendererEvent, ...args: any[]) {
private handleIPCMessage(_event: IpcRendererEvent, ...args: any[]) {
const actionType = args[0];
if(actionType === "notify-event") {

View File

@ -18,7 +18,7 @@ export abstract class ProxiedClass<Interface extends { events?: ProxiedEvents<In
public readonly events: ProxiedEvents<Interface["events"]>;
public constructor(props: ProxiedClassProperties) {
protected constructor(props: ProxiedClassProperties) {
this.ownerWindowId = props.ownerWindowId;
this.instanceId = props.instanceId;
this.events = props.events;

View File

@ -67,7 +67,7 @@ export class ObjectProxyServer<ObjectType extends ProxyInterface<ObjectType>> {
private generateEventProxy(instanceId: string, owningWindowId: number) : {} {
const ipcChannel = this.ipcChannel;
return new Proxy({ }, {
get(target: { }, event: PropertyKey, receiver: any): any {
get(target: { }, event: PropertyKey, _receiver: any): any {
return (...args: any) => {
const window = BrowserWindow.fromId(owningWindowId);
if(!window) return;

View File

@ -1,15 +1,17 @@
export class Version {
major: number = 0;
minor: number = 0;
patch: number = 0;
build: number = 0;
timestamp: number = 0;
major: number;
minor: number;
patch: number;
build: number;
timestamp: number;
constructor(major: number, minor: number, patch: number, build: number, timestamp: number) {
this.major = major;
this.minor = minor;
this.patch = patch;
this.build = build;
this.timestamp = timestamp;
}
toString(timestamp: boolean = false) {
@ -17,10 +19,13 @@ export class Version {
result += this.major + ".";
result += this.minor + ".";
result += this.patch;
if(this.build > 0)
if(this.build > 0) {
result += "-" + this.build;
if(timestamp && this.timestamp > 0)
}
if(timestamp && this.timestamp > 0) {
result += " [" + this.timestamp + "]";
}
return result;
}
@ -33,12 +38,11 @@ export class Version {
if(other.minor != this.minor) return false;
if(other.patch != this.patch) return false;
if(other.build != this.build) return false;
if(other.timestamp != this.timestamp) return false;
return true;
return other.timestamp == this.timestamp;
}
newer_than(other: Version) : boolean {
newerThan(other: Version) : boolean {
if(other.major > this.major) return false;
else if(other.major < this.major) return true;
@ -54,13 +58,13 @@ export class Version {
return false;
}
in_dev() : boolean {
isDevelopmentVersion() : boolean {
return this.build == 0 && this.major == 0 && this.minor == 0 && this.patch == 0 && this.timestamp == 0;
}
}
//1.0.0-2 [1000]
export function parse_version(version: string) : Version {
export function parseVersion(version: string) : Version {
let result: Version = new Version(0, 0, 0, 0, 0);
const roots = version.split(" ");

View File

@ -9,7 +9,7 @@ import BrowserWindow = Electron.BrowserWindow;
import Rectangle = Electron.Rectangle;
let changedData: {[key: string]:Rectangle} = {};
let changedDataSaveTimeout: NodeJS.Timer;
let changedDataSaveTimeout: number;
export async function save_changes() {
clearTimeout(changedDataSaveTimeout);

View File

@ -144,7 +144,7 @@ set(REQUIRED_LIBRARIES
${LIBEVENT_STATIC_LIBRARIES}
${StringVariable_LIBRARIES_STATIC}
${DataPipes_LIBRARIES_STATIC} #Needs to be static because something causes ca bad function call when loaded in electron
DataPipes::core::static
${ThreadPool_LIBRARIES_STATIC}
${soxr_LIBRARIES_STATIC}
${fvad_LIBRARIES_STATIC}

View File

@ -5,7 +5,7 @@ using namespace std;
using namespace tc::audio::codec;
OpusConverter::OpusConverter(size_t c, size_t s, size_t f) : Converter(c, s, f) { }
OpusConverter::~OpusConverter() {}
OpusConverter::~OpusConverter() = default;
bool OpusConverter::valid() {
return this->encoder && this->decoder;

View File

@ -457,7 +457,7 @@ NAN_METHOD(AudioConsumerWrapper::_set_filter_mode) {
return;
}
auto value = info[0].As<v8::Number>()->ToInteger()->Value();
auto value = info[0].As<v8::Number>()->Int32Value(info.GetIsolate()->GetCurrentContext()).FromMaybe(0);
handle->filter_mode_ = (FilterMode) value;
}
@ -474,5 +474,5 @@ NAN_METHOD(AudioConsumerWrapper::toggle_rnnoise) {
return;
}
handle->rnnoise = info[0]->BooleanValue();
handle->rnnoise = info[0]->BooleanValue(info.GetIsolate());
}

View File

@ -276,7 +276,7 @@ NAN_METHOD(tc::audio::sounds::playback_sound_js) {
PlaybackSettings settings{};
settings.file = *Nan::Utf8String(file);
settings.volume = volume->Value();
settings.volume = (float) volume->Value();
if(!callback.IsEmpty()) {
if(!callback->IsFunction()) {
Nan::ThrowError("invalid callback function");

View File

@ -70,7 +70,10 @@ tc::audio::AudioOutput* global_audio_output;
Nan::Set(object, (uint32_t) value, Nan::New<v8::String>(key).ToLocalChecked());
NAN_MODULE_INIT(init) {
logger::initialize_node();
/* FIXME: Reenable */
//logger::initialize_node();
logger::initialize_raw();
#ifndef WIN32
logger::info(category::general, tr("Hello World from C. PPID: {}, PID: {}"), getppid(), getpid());
#else

View File

@ -631,10 +631,12 @@ void ServerConnection::close_connection() {
}
this->event_loop_execute_connection_close = false;
if(this->socket)
this->socket->finalize();
if(this->protocol_handler)
this->protocol_handler->do_close_connection();
if(this->socket) {
this->protocol_handler->do_close_connection();
}
if(this->protocol_handler) {
this->protocol_handler->do_close_connection();
}
this->socket = nullptr;
this->call_disconnect_result.call(0, true);

View File

@ -368,10 +368,11 @@ void VoiceConnection::process_packet(const std::shared_ptr<ts::protocol::ServerP
return;
}
if(packet->data().length() > 5)
client->process_packet(packet_id, packet->data().range(5), (codec::value) codec_id, flag_head);
else
client->process_packet(packet_id, pipes::buffer_view{nullptr, 0}, (codec::value) codec_id, flag_head);
if(packet->data().length() > 5) {
client->process_packet(packet_id, packet->data().range(5), (codec::value) codec_id, flag_head);
} else {
client->process_packet(packet_id, pipes::buffer_view{nullptr, 0}, (codec::value) codec_id, flag_head);
}
} else {
//TODO implement whisper
}

View File

@ -0,0 +1,23 @@
import * as path from "path";
import * as os from "os";
const Module = require("module");
const originalRequire = Module._load;
Module._load = (module, ...args) => {
if(module === "tc-native/connection") {
let build_type;
console.error(os.platform());
if(os.platform() === "win32") {
build_type = "win32_x64";
} else {
build_type = "linux_x64";
}
return originalRequire(path.join(__dirname, "..", "..", "..", "build", build_type, "teaclient_connection.node"), ...args);
} else {
return originalRequire(module, ...args);
}
};
export = {};

View File

@ -1,37 +1,38 @@
/// <reference path="../../exports/exports.d.ts" />
import "./RequireHandler";
module.paths.push("../../build/linux_x64");
import * as fs from "fs";
import * as handle from "teaclient_connection";
import {NativeServerConnection} from "teaclient_connection";
import * as handle from "tc-native/connection";
import {NativeServerConnection} from "tc-native/connection";
//remote_host: "51.68.181.92",
//remote_host: "94.130.236.135",
//remote_host: "54.36.232.11", /* the beast */
//remote_host: "79.133.54.207", /* gommehd.net */
const target_address = "51.68.181.92";
const target_address = "127.0.0.1";
const { host, port } = {
host: target_address.split(":")[0],
port: target_address.split(":").length > 1 ? parseInt(target_address.split(":")[1]) : 9987
};
class Bot {
connection: NativeServerConnection;
channel_ids: number[] = [];
knwonChannelIds: number[] = [];
client_id: number;
initialized: boolean;
private _interval = [];
private switchInterval = [];
private _timeouts = [];
connect() {
for(const interval of this._interval)
reset() {
this.connection = undefined;
for(const interval of this.switchInterval)
clearInterval(interval);
for(const timeouts of this._timeouts)
clearInterval(timeouts);
}
this.channel_ids = [];
connect() {
this.knwonChannelIds = [];
this.client_id = 0;
this.initialized = false;
@ -69,6 +70,7 @@ class Bot {
], []);
} else {
console.log("Bot connect failed: %o (%s) ", error, this.connection.error_message(error));
this.reset();
}
},
@ -77,12 +79,16 @@ class Bot {
});
this.connection.callback_command = (command, args, switches) => this.handle_command(command, args);
this.connection.callback_disconnect = () => this.disconnect();
this.connection.callback_disconnect = () => {
this.connection = undefined;
this.reset();
}
}
async disconnect() {
await new Promise(resolve => this.connection.disconnect("bb", resolve));
this.connection = undefined;
this.reset();
}
private handle_command(command: string, args: any[]) {
@ -90,35 +96,51 @@ class Bot {
this.client_id = parseInt(args[0]["aclid"]);
} else if(command == "channellistfinished"){
this.initialized = true;
this._interval.push(setInterval(() => this.switch_channel(), 1000));
this.switchInterval.push(setInterval(() => this.switch_channel(), 30_000 + Math.random() * 10_000));
} else if(command == "channellist") {
for(const element of args) {
this.channel_ids.push(parseInt(element["cid"]));
this.knwonChannelIds.push(parseInt(element["cid"]));
}
} else if(command == "notifychannelcreated") {
this.channel_ids.push(parseInt(args[0]["cid"]));
this.knwonChannelIds.push(parseInt(args[0]["cid"]));
} else if(command == "notifychanneldeleted") {
for(const arg of args) {
const channel_id = parseInt(arg["cid"]);
const index = this.channel_ids.indexOf(channel_id);
const index = this.knwonChannelIds.indexOf(channel_id);
if(index >= 0)
this.channel_ids.splice(index, 1);
this.knwonChannelIds.splice(index, 1);
}
}
}
private switch_channel() {
const target_channel = this.channel_ids[Math.floor((Math.random() * 100000) % this.channel_ids.length)];
const target_channel = this.knwonChannelIds[Math.floor((Math.random() * 100000) % this.knwonChannelIds.length)];
console.log("Switching to channel %d", target_channel);
this.connection.send_command("clientmove", [{clid: this.client_id, cid: target_channel}], []);
}
}
const bot_list = [];
for(let index = 0; index < 1; index++) {
const bot = new Bot();
bot_list.push(bot);
bot.connect();
}
const bot_list: Bot[] = [];
async function connectBots() {
for(let index = 0; index < 5; index++) {
const bot = new Bot();
bot_list.push(bot);
bot.connect();
await new Promise(resolve => setTimeout(resolve, 10_000));
}
}
setInterval(() => {
bot_list.forEach(connection => {
if(connection.connection) {
connection.connection.send_voice_data(new Uint8Array([1, 2, 3]), 5, false);
} else {
connection.connect();
}
});
}, 5);
connectBots().then(undefined);

View File

@ -1,18 +1,14 @@
/// <reference path="../../exports/exports.d.ts" />
console.log("HELLO WORLD");
module.paths.push("../../build/linux_x64");
module.paths.push("../../build/win32_x64");
import "./RequireHandler";
//LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libasan.so.5
const os = require('os');
//process.dlopen(module, '/usr/lib/x86_64-linux-gnu/libasan.so.5',
// os.constants.dlopen.RTLD_NOW);
import * as fs from "fs";
const kPreloadAsan = false;
if(kPreloadAsan) {
//LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libasan.so.5
const os = require('os');
// @ts-ignore
process.dlopen(module, '/usr/lib/x86_64-linux-gnu/libasan.so.5', os.constants.dlopen.RTLD_NOW);
}
const original_require = require;
require = (module => original_require(__dirname + "/../../../build/win32_x64/" + module + ".node")) as any;
import * as handle from "teaclient_connection";
require = original_require;
import * as handle from "tc-native/connection";
const connection_list = [];
const connection = handle.spawn_server_connection();
@ -132,38 +128,14 @@ const do_connect = (connection) => {
console.log("Received error: %o", arguments1);
return;
}
console.log("Command %s: %o", command, arguments1);
if(command === "channellistfinished") {
//115
//connection.send_command("clientgetvariables", [{ clid: 1 }], []);
//connection.send_command("channelsubscribeall", [], []);
connection.send_command("playlistsonglist", [{ playlist_id: '12' }], []);
/*
setInterval(() => {
connection.send_command("servergroupclientlist", [{ sgid: 2 }], []);
connection.send_command("servergrouppermlist", [{ sgid: 2 }], []);
}, 1000);
*/
}
console.log("Command %s: %o", command, arguments1);
};
connection._voice_connection.register_client(7);
//connection._voice_connection.register_client(2);
};
do_connect(connection);
/*
let _connections = [];
let i = 0;
let ii = setInterval(() => {
if(i++ > 35)
clearInterval(ii);
const c = handle.spawn_server_connection();
_connections.push(c);
do_connect(c);
}, 500);
*/
connection.callback_voice_data = (buffer, client_id, codec_id, flag_head, packet_id) => {
console.log("Received voice of length %d from client %d in codec %d (Head: %o | ID: %d)", buffer.byteLength, client_id, codec_id, flag_head, packet_id);
connection.send_voice_data(buffer, codec_id, flag_head);
@ -179,7 +151,7 @@ setInterval(() => {
/* keep the object alive */
setTimeout(() => {
connection.connected();
_connections.forEach(e => e.current_ping());
}, 1000);
connection_list.push(connection);
connection_list.push(connection);
export default {};

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"esModuleInterop": true
},
"include": [
"exports/exports.d.ts",
"test/js/"
]
}

View File

@ -56,6 +56,7 @@ bool config::load(std::string &error, const std::string &file) {
config::locking_files.push_back(entry);
}
}
{
json moves;
get(moves, value, "moves");
@ -68,6 +69,11 @@ bool config::load(std::string &error, const std::string &file) {
config::moving_actions.push_back(entry);
}
}
if(value.contains("permission-test-directory")) {
get(config::permission_test_directory, value, "permission-test-directory");
}
logger::debug("Loaded %d locking actions and %d moving actions", config::locking_files.size(), config::moving_actions.size());
return true;
}

View File

@ -4,6 +4,7 @@
#include <deque>
#include <string>
#include <memory>
#include <optional>
namespace config {
extern bool load(std::string& /* error */, const std::string& /* file */);
@ -33,6 +34,8 @@ namespace config {
_extern std::string callback_argument_fail;
_extern std::string callback_argument_success;
_extern std::optional<std::string> permission_test_directory;
_extern std::deque<std::shared_ptr<LockFile>> locking_files;
_extern std::deque<std::shared_ptr<MovingFile>> moving_actions;
}

View File

@ -285,4 +285,53 @@ void file::commit() {
}
return true;
}
#endif
#endif
#ifdef WIN32
bool CanAccessFolder(LPCTSTR folderName, DWORD genericAccessRights)
{
bool bRet = false;
DWORD length = 0;
if (!::GetFileSecurity(folderName, OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, nullptr, 0, &length) && ERROR_INSUFFICIENT_BUFFER == ::GetLastError()) {
auto security = static_cast<PSECURITY_DESCRIPTOR>(::malloc(length));
if (security && ::GetFileSecurity(folderName, OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, security, length, &length )) {
HANDLE hToken = NULL;
if (::OpenProcessToken( ::GetCurrentProcess(), TOKEN_IMPERSONATE | TOKEN_QUERY |
TOKEN_DUPLICATE | STANDARD_RIGHTS_READ, &hToken )) {
HANDLE hImpersonatedToken = NULL;
if (::DuplicateToken( hToken, SecurityImpersonation, &hImpersonatedToken )) {
GENERIC_MAPPING mapping = { 0xFFFFFFFF };
PRIVILEGE_SET privileges = { 0 };
DWORD grantedAccess = 0, privilegesLength = sizeof( privileges );
BOOL result = FALSE;
mapping.GenericRead = FILE_GENERIC_READ;
mapping.GenericWrite = FILE_GENERIC_WRITE;
mapping.GenericExecute = FILE_GENERIC_EXECUTE;
mapping.GenericAll = FILE_ALL_ACCESS;
::MapGenericMask( &genericAccessRights, &mapping );
if (::AccessCheck( security, hImpersonatedToken, genericAccessRights,
&mapping, &privileges, &privilegesLength, &grantedAccess, &result )) {
bRet = (result == TRUE);
}
::CloseHandle( hImpersonatedToken );
}
::CloseHandle( hToken );
}
::free( security );
}
}
return bRet;
}
#endif
bool file::directory_writeable(const std::string &path) {
#ifdef WIN32
return CanAccessFolder(path.c_str(), GENERIC_WRITE);
#else
/* TODO: Check for file permissions? Is this method even needed? */
return false;
#endif
}

View File

@ -16,5 +16,10 @@ namespace file {
extern void rollback();
extern void commit();
/**
* @param path The target path to test
* @returns true if the target path is writeable or if it does not exists is createable.
*/
extern bool directory_writeable(const std::string &path /* file */);
extern bool file_locked(const std::string& file);
}

View File

@ -2,6 +2,7 @@
#include <string>
#include <chrono>
#include <thread>
#include <filesystem>
#include "./logger.h"
#include "./config.h"
@ -82,6 +83,15 @@ static bool daemonize() {
}
#endif
bool requires_permission_elevation() {
if(!config::permission_test_directory.has_value()) {
/* Old clients don't provide that. We assume yes. */
return true;
}
return file::directory_writeable(*config::permission_test_directory);
}
std::string log_file_path;
int main(int argc, char** argv) {
srand((unsigned int) chrono::floor<chrono::nanoseconds>(chrono::system_clock::now().time_since_epoch()).count());
@ -115,7 +125,7 @@ int main(int argc, char** argv) {
#endif
#ifdef WIN32
{
if(requires_permission_elevation()) {
auto admin = is_administrator();
logger::info("App executed as admin: %s", admin ? "yes" : "no");
if(!admin) {

326
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "TeaClient",
"version": "1.4.10",
"version": "1.4.13",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -160,6 +160,14 @@
"defer-to-connect": "^1.0.1"
}
},
"@types/ajv": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/ajv/-/ajv-1.0.0.tgz",
"integrity": "sha1-T7JEB0Ly9sMOf7B5e4OfxvaWaCo=",
"requires": {
"ajv": "*"
}
},
"@types/bluebird": {
"version": "3.5.30",
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.30.tgz",
@ -177,6 +185,14 @@
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
"@types/cross-spawn": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz",
"integrity": "sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==",
"requires": {
"@types/node": "*"
}
},
"@types/ejs": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-2.7.0.tgz",
@ -225,6 +241,11 @@
"@types/sizzle": "*"
}
},
"@types/json-stable-stringify": {
"version": "1.0.32",
"resolved": "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.32.tgz",
"integrity": "sha512-q9Q6+eUEGwQkv4Sbst3J4PNgDOvpuVuKj79Hl/qnmBMEIPzB5QoFRUtjcgcg2xNUZyYUGXBk5wYIBKHt0A+Mxw=="
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -1602,6 +1623,11 @@
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true
},
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
},
"defaults": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
@ -4126,11 +4152,34 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"json-stable-stringify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
"integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
"requires": {
"jsonify": "~0.0.0"
}
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"json5": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
"requires": {
"minimist": "^1.2.5"
},
"dependencies": {
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
}
}
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
@ -4139,6 +4188,11 @@
"graceful-fs": "^4.1.6"
}
},
"jsonify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
},
"jsprim": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.0.tgz",
@ -5250,8 +5304,7 @@
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
},
"path-type": {
"version": "1.1.0",
@ -5716,7 +5769,6 @@
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
"integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
@ -6709,6 +6761,24 @@
"utf8-byte-length": "^1.0.1"
}
},
"tsconfig-loader": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tsconfig-loader/-/tsconfig-loader-1.1.0.tgz",
"integrity": "sha512-KrFF45RYo/JHpoAp1Lf68NupYNyRmh7BwSh1AmAQ3fdCMl8laOyZSLO5iByQR2VTkVdt454HS3c5kfVeYWq7iQ==",
"requires": {
"deepmerge": "^4.2.2",
"json5": "^2.1.1",
"resolve": "^1.15.1",
"strip-bom": "^4.0.0"
},
"dependencies": {
"strip-bom": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
"integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="
}
}
},
"tslib": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
@ -6747,8 +6817,252 @@
"typescript": {
"version": "3.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz",
"integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==",
"dev": true
"integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ=="
},
"typescript-json-schema": {
"version": "0.38.3",
"resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.38.3.tgz",
"integrity": "sha512-+13qUoBUQwOXqxUoYQWtLA9PEM7ojfv8r+hYc2ebeqqVwVM4+yI5JSlsYRBlJKKewc9q1FHqrMR6L6d9TNX9Dw==",
"requires": {
"glob": "~7.1.4",
"json-stable-stringify": "^1.0.1",
"typescript": "^3.5.1",
"yargs": "^13.2.4"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"requires": {
"locate-path": "^3.0.0"
}
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"requires": {
"ansi-regex": "^4.1.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
"typescript-json-validator": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/typescript-json-validator/-/typescript-json-validator-2.4.2.tgz",
"integrity": "sha512-4oliZJGo8jwRAWxssz1n7KiNo21AwN/XqXm8l66k1sH3emqrulR2EGjsNfLV95/JD07C1YIkFlvClOlNANghag==",
"requires": {
"@types/ajv": "^1.0.0",
"@types/cross-spawn": "^6.0.0",
"@types/glob": "^7.1.1",
"@types/json-stable-stringify": "^1.0.32",
"@types/minimatch": "^3.0.3",
"cross-spawn": "^6.0.5",
"glob": "^7.1.3",
"json-stable-stringify": "^1.0.1",
"minimatch": "^3.0.4",
"tsconfig-loader": "^1.1.0",
"typescript-json-schema": "^0.38.3",
"yargs": "^13.2.4"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"requires": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
}
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"requires": {
"locate-path": "^3.0.0"
}
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"requires": {
"ansi-regex": "^4.1.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
"undefsafe": {
"version": "2.0.2",

View File

@ -1,6 +1,6 @@
{
"name": "TeaClient",
"version": "1.4.13",
"version": "1.5.0",
"description": "",
"main": "main.js",
"scripts": {
@ -13,8 +13,10 @@
"start-devel-download": "electron . --disable-hardware-acceleration --gdb --debug --updater-ui-loader_type=2 --updater-ui-ignore-version -t -u http://localhost:8081/",
"start-s": "electron . --disable-hardware-acceleration --gdb --debug --updater-ui-loader_type=3 --updater-ui-ignore-version -t -u http://localhost:8081/",
"dtest": "electron . dtest",
"sass": "sass",
"compile-sass": "sass --update .:.",
"compile-tsc": "tsc",
"compile-json-validator": "sh generate-json-validators.sh",
"build-linux-64": "node installer/build.js linux",
"package-linux-64": "node installer/package_linux.js",
"build-windows-64": "node installer/build.js win32",
@ -84,6 +86,7 @@
"sshpk": "^1.16.1",
"tar-stream": "^2.1.2",
"tough-cookie": "^3.0.1",
"typescript-json-validator": "^2.4.2",
"url-regex": "^5.0.0",
"v8-callsites": "latest"
},