Updates for 1.5.0
6
generate-json-validators.sh
Normal 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
|
@ -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");
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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)');
|
||||
|
||||
|
@ -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
|
||||
|
@ -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!"
|
||||
|
3
main.ts
@ -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 */
|
||||
{
|
||||
|
@ -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.
|
||||
|
21
modules/core/app-updater/AppInfoFile.ts
Normal 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;
|
74
modules/core/app-updater/AppInfoFile.validator.ts
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
6
modules/core/app-updater/UpdateConfigFile.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface UpdateConfigFile {
|
||||
version: number,
|
||||
selectedChannel: string
|
||||
}
|
||||
|
||||
export default UpdateConfigFile;
|
41
modules/core/app-updater/UpdateConfigFile.validator.ts
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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") {
|
||||
|
97
modules/core/ui-loader/Cache.ts
Normal 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");
|
||||
}
|
35
modules/core/ui-loader/CacheFile.ts
Normal 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;
|
145
modules/core/ui-loader/CacheFile.validator.ts
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
322
modules/core/ui-loader/Loader.ts
Normal 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;
|
||||
}
|
125
modules/core/ui-loader/Remote.ts
Normal 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";
|
||||
}
|
||||
}
|
0
modules/core/ui-loader/RemoteData.ts
Normal file
52
modules/core/ui-loader/Shipped.ts
Normal 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;
|
||||
}));
|
||||
}
|
10
modules/core/ui-loader/ShippedFileInfo.ts
Normal 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;
|
57
modules/core/ui-loader/ShippedFileInfo.validator.ts
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from "./loader.js";
|
||||
export * from "./graphical.js";
|
@ -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;
|
||||
}
|
@ -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");
|
||||
}
|
Before Width: | Height: | Size: 13 KiB |
@ -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 {}
|
@ -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"> </a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -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>
|
@ -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
|
||||
|
114
modules/core/windows/app-loader/controller/AppLoader.ts
Normal 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);
|
||||
}
|
36
modules/core/windows/app-loader/renderer/img/logo.svg
Normal 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 |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
23
modules/core/windows/app-loader/renderer/index.html
Normal 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"> </a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
88
modules/core/windows/app-loader/renderer/index.scss
Normal 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;
|
||||
}
|
32
modules/core/windows/app-loader/renderer/index.ts
Normal 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 = {};
|
223
modules/core/windows/client-updater/controller/ClientUpdate.ts
Normal 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;
|
||||
}
|
||||
})();
|
||||
}
|
103
modules/core/windows/client-updater/renderer/index.html
Normal 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 <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>
|
407
modules/core/windows/client-updater/renderer/index.scss
Normal 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;
|
||||
}
|
271
modules/core/windows/client-updater/renderer/index.ts
Normal 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 = " ";
|
||||
} 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 = {};
|
BIN
modules/core/windows/client-updater/renderer/logo.png
Normal file
After Width: | Height: | Size: 50 KiB |
@ -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 |
6
modules/core/windows/client-updater/renderer/up2date.svg
Normal 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 |
8
modules/core/windows/client-updater/renderer/update.svg
Normal 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 |
111
modules/core/windows/main-window/controller/MainWindow.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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 = {};
|
@ -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, "..", "..")) : "";
|
||||
|
@ -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">
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { shell, ipcRenderer } from "electron";
|
||||
|
||||
function open_issue_tracker() {
|
||||
function openIssueTracker() {
|
||||
shell.openExternal("https://github.com/TeaSpeak/TeaClient/issues");
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
@ -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") {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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(" ");
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
}
|
@ -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");
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
}
|
||||
|
23
native/serverconnection/test/js/RequireHandler.ts
Normal 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 = {};
|
@ -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);
|
@ -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 {};
|
11
native/serverconnection/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": [
|
||||
"exports/exports.d.ts",
|
||||
"test/js/"
|
||||
]
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|