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" | ||||
|   }, | ||||
|  | ||||