A lot of updates
This commit is contained in:
parent
53d3814f92
commit
b956bad3f7
21
.gitignore
vendored
21
.gitignore
vendored
@ -1,21 +0,0 @@
|
||||
.idea/
|
||||
.sass-cache/
|
||||
tmp/
|
||||
node_modules/
|
||||
/build/
|
||||
updater/postzip/test
|
||||
updater/postzip/cmake-build-*/
|
||||
updater/postzip/.idea
|
||||
updater/postzip/TeaClient-linux.tar.gz
|
||||
|
||||
#For the main JS file
|
||||
*.js.map
|
||||
*.js
|
||||
|
||||
.deploy_secret
|
||||
|
||||
**/*.d.ts
|
||||
!
|
||||
!modules/renderer/imports/.copy_*.d.ts
|
||||
|
||||
package-lock.json
|
14
.gitmodules
vendored
14
.gitmodules
vendored
@ -1,14 +0,0 @@
|
||||
[submodule "native/codec/libraries/opus"]
|
||||
path = native/codec/libraries/opus
|
||||
url = https://github.com/xiph/opus.git
|
||||
[submodule "native/codec/libraries/speex"]
|
||||
path = native/codec/libraries/speex
|
||||
url = https://github.com/xiph/speex.git
|
||||
[submodule "native/codec/libraries/celt"]
|
||||
path = native/codec/libraries/celt
|
||||
url = https://github.com/WolverinDEV/celt-0.11.0.git
|
||||
[submodule "DbConnector"]
|
||||
branch = stable
|
||||
[submodule "github"]
|
||||
path = github
|
||||
url = https://github.com/TeaSpeak/TeaClient
|
27
bugs
27
bugs
@ -1,27 +0,0 @@
|
||||
Linux:
|
||||
Updater:
|
||||
After updater has extracted the file set the executable flag again for the TeaClient binary
|
||||
|
||||
|
||||
Windows:
|
||||
Updater:
|
||||
Updater popups console which says that there are invalid arguments! Fix this!
|
||||
Improve access to the update-install.exe (May request admin permissions)
|
||||
|
||||
|
||||
General:
|
||||
Audio replay with TS3 is a bit buggy!
|
||||
|
||||
|
||||
Tasks designer:
|
||||
TeaCup steam animated
|
||||
Client redesign dark [+ Chat system]
|
||||
Redesign loading animation (Web)
|
||||
|
||||
|
||||
|
||||
Notice:
|
||||
electron-package-manager must be at 8.7.2 (Node 6 support)!
|
||||
|
||||
|
||||
FIXME: Test the new voice resampler!
|
@ -1,117 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
cd "${BASEDIR}"
|
||||
|
||||
file_paths=(
|
||||
"$(pwd ~)/../../Web-Client/shared/declarations"
|
||||
"$(pwd ~)/TeaSpeak/Web-Client/shared/declarations"
|
||||
"$(pwd ~)/../../TeaSpeak/Web-Client/shared/declarations"
|
||||
"app/dummy-declarations"
|
||||
#TODO Windows path
|
||||
)
|
||||
files=(
|
||||
"exports_app.d.ts;imports_shared.d.ts"
|
||||
"exports_loader_app.d.ts;imports_shared_loader.d.ts"
|
||||
# "exports_loader.d.ts;imports_shared_loader.d.ts"
|
||||
)
|
||||
|
||||
support_rel_linking=$(ln --help 2>&1 | grep -e "--relative" >/dev/null && echo "1" || echo "0")
|
||||
support_rel_linking=0
|
||||
|
||||
path_target="./modules/renderer/imports"
|
||||
{
|
||||
mkdir -p "${path_target}"
|
||||
|
||||
for path in "${file_paths[@]}"
|
||||
do
|
||||
path_found=1
|
||||
for file_mapping in "${files[@]}"
|
||||
do
|
||||
file_mapping=($(echo ${file_mapping} | tr ";" " "))
|
||||
file=${file_mapping[0]}
|
||||
|
||||
if [[ ! -f "${path}/${file}" ]]; then
|
||||
path_found=0
|
||||
echo "path test ${path} failed to file ${file}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
[[ path_found -eq 1 ]] || continue
|
||||
|
||||
for file in "${files[@]}"
|
||||
do
|
||||
file_mapping=($(echo ${file} | tr ";" "\n"))
|
||||
src_file=${file_mapping[0]}
|
||||
dst_file=${file_mapping[1]}
|
||||
|
||||
if [[ -e "${path_target}/${dst_file}" ]] || [[ -L "${path_target}/${dst_file}" ]]; then
|
||||
rm "${path_target}/${dst_file}"
|
||||
fi
|
||||
|
||||
|
||||
if [[ ${support_rel_linking} -ne 0 ]]; then
|
||||
ln -rs "${path}/${src_file}" "${path_target}/${dst_file}"
|
||||
else
|
||||
_source=$(realpath "${path}/${src_file}")
|
||||
_current_dir=$(pwd)
|
||||
cd ${path_target}
|
||||
[[ $? -ne 0 ]] && {
|
||||
echo "Failed to enter target directory"
|
||||
exit 1;
|
||||
}
|
||||
ln -s "${_source}" "${dst_file}"
|
||||
cd ${_current_dir}
|
||||
fi
|
||||
echo "Linking \"${path_target}/${dst_file}\" to \"${path}/${src_file}\""
|
||||
|
||||
cp "${path}/${src_file}" "${path_target}/.copy_${dst_file}"
|
||||
echo "Create copy \"${path}/${src_file}\" to \"${path_target}/.copy_${dst_file}\""
|
||||
done
|
||||
break
|
||||
done
|
||||
}
|
||||
|
||||
if [[ ${path_found} -eq 0 ]]; then
|
||||
echo "Could not import a link to shared imports. Trying copied import."
|
||||
|
||||
for file in "${files[@]}"
|
||||
do
|
||||
file_mapping=($(echo ${file} | tr ";" "\n"))
|
||||
dst_file=${file_mapping[1]}
|
||||
|
||||
if [[ -e "${path_target}/${dst_file}" ]] || [[ -L "${path_target}/${dst_file}" ]]; then
|
||||
echo "Hmm target file already exists even thou it hasn't been found yet... Deleting it!"
|
||||
rm "${path_target}/${dst_file}"
|
||||
fi
|
||||
|
||||
if [[ ! -e "${path_target}/.copy_${dst_file}" ]]; then
|
||||
echo "Missing copy of file ${dst_file} because we cant find any valid link!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ${support_rel_linking} -ne 0 ]]; then
|
||||
ln -rs "${path_target}/.copy_${dst_file}" "${path_target}/${dst_file}"
|
||||
else
|
||||
_source=$(realpath "${path_target}/.copy_${dst_file}")
|
||||
_current_dir=$(pwd)
|
||||
cd ${path_target}
|
||||
[[ $? -ne 0 ]] && {
|
||||
echo "Failed to enter target directory"
|
||||
exit 1;
|
||||
}
|
||||
ln -s "${_source}" "${dst_file}"
|
||||
cd ${_current_dir}
|
||||
fi
|
||||
echo "Linking \"${path_target}/${dst_file}\" to \"${path_target}/.copy_${dst_file}\""
|
||||
done
|
||||
path_found=1
|
||||
fi
|
||||
|
||||
if [[ path_found -eq 0 ]]; then
|
||||
echo "Failed to find UI imports"
|
||||
echo "Add your path to 'file_paths' and build the declarations first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
1
github
1
github
@ -1 +0,0 @@
|
||||
Subproject commit 14645dca78396c915ad4ad122d532f24fdfd2969
|
@ -1,44 +0,0 @@
|
||||
; Script generated by the Inno Setup Script Wizard.
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application.
|
||||
; Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
AppId={{0F43B730-DF59-4A23-82AD-E895E72BE4AF}
|
||||
AppName=TeaSpeak Client
|
||||
AppVersion=<%= version %>
|
||||
AppPublisher=TeaSpeak
|
||||
AppPublisherURL=https://www.teaspeak.com/
|
||||
AppSupportURL=https://www.forum.teaspeak.com/
|
||||
AppUpdatesURL=https://www.teaspeak.com/
|
||||
DefaultDirName={pf}\TeaSpeak
|
||||
OutputBaseFilename=<%= executable_name %>
|
||||
OutputDir=<%= dest_dir %>
|
||||
SetupIconFile=<%= icon_file %>
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
DisableDirPage=no
|
||||
DisableWelcomePage=no
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1
|
||||
|
||||
[Files]
|
||||
Source: "<%= source_dir %>"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{commonprograms}\TeaSpeak"; Filename: "{app}\TeaClient.exe"
|
||||
Name: "{commondesktop}\TeaSpeak"; Filename: "{app}\TeaClient.exe"; Tasks: desktopicon
|
||||
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\TeaSpeak"; Filename: "{app}\TeaClient.exe"; Tasks: quicklaunchicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\TeaClient.exe"; Description: "{cm:LaunchProgram,TeaSpeak}"; Flags: nowait postinstall skipifsilent
|
||||
|
@ -1,399 +0,0 @@
|
||||
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 * as util from "util";
|
||||
import * as child_process from "child_process";
|
||||
import * as os from "os";
|
||||
import * as asar from "asar";
|
||||
import * as querystring from "querystring";
|
||||
import request = require("request");
|
||||
import * as deployer from "./deploy";
|
||||
|
||||
let options: Options = {} as any;
|
||||
let version = parse_version(pkg.version);
|
||||
version.timestamp = Date.now();
|
||||
|
||||
options.dir = '.';
|
||||
options.name = "TeaClient";
|
||||
options.appVersion = pkg.version;
|
||||
options.appCopyright = "© 2018-2019 Markus Hadenfeldt All Rights Reserved";
|
||||
options.out = "build/";
|
||||
|
||||
if(!pkg.dependencies['electron']) {
|
||||
console.error("Missing electron version");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
options["version-string"] = {
|
||||
'CompanyName': 'TeaSpeak',
|
||||
'LegalCopyright': '© 2018-2019 Markus Hadenfeldt All Rights Reserved',
|
||||
'FileDescription' : 'TeaSpeak-Client',
|
||||
'OriginalFilename' : 'TeaClient.exe',
|
||||
'FileVersion' : pkg.version,
|
||||
'ProductVersion' : pkg.version,
|
||||
'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;
|
||||
}
|
||||
|
||||
interface ProjectFile extends ProjectEntry {
|
||||
path: string;
|
||||
name: string | RegExp;
|
||||
|
||||
//target_name?: string | ((file: string) => string);
|
||||
}
|
||||
|
||||
interface ProjectDirectory extends ProjectEntry {
|
||||
|
||||
path: string | RegExp;
|
||||
children?: boolean;
|
||||
files?: RegExp;
|
||||
}
|
||||
|
||||
enum ProjectEntryType {
|
||||
FILE,
|
||||
DIRECTORY
|
||||
}
|
||||
|
||||
const project_files: ProjectEntry[] = [];
|
||||
{ /* general required files*/
|
||||
project_files.push({
|
||||
type: ProjectEntryType.FILE,
|
||||
path: "/",
|
||||
name: "package.json"
|
||||
} as ProjectFile);
|
||||
project_files.push({
|
||||
type: ProjectEntryType.FILE,
|
||||
path: "/",
|
||||
name: "main.js"
|
||||
} as ProjectFile);
|
||||
|
||||
project_files.push({
|
||||
type: ProjectEntryType.DIRECTORY,
|
||||
path: "/node_modules",
|
||||
children: true,
|
||||
files: /\.(js|css|html|node|json)$/
|
||||
} as ProjectDirectory);
|
||||
|
||||
project_files.push({
|
||||
type: ProjectEntryType.DIRECTORY,
|
||||
path: "/node_modules/electron",
|
||||
children: true,
|
||||
files: /.*/
|
||||
} as ProjectDirectory);
|
||||
}
|
||||
|
||||
/* TeaClient modules */
|
||||
project_files.push({
|
||||
type: ProjectEntryType.DIRECTORY,
|
||||
path: "/modules",
|
||||
children: true,
|
||||
files: /.*\.(js|css|html|png|svg)$/
|
||||
} as ProjectDirectory);
|
||||
|
||||
/* resource files */
|
||||
project_files.push({
|
||||
type: ProjectEntryType.DIRECTORY,
|
||||
path: "/resources"
|
||||
} as ProjectDirectory);
|
||||
|
||||
|
||||
if (process.argv[2] == "linux") {
|
||||
options.arch = "x64";
|
||||
options.platform = "linux";
|
||||
options.icon = "resources/logo.svg";
|
||||
} else if (process.argv[2] == "win32") {
|
||||
options.arch = "x64";
|
||||
options.platform = "win32";
|
||||
options.icon = "resources/logo.ico";
|
||||
} else {
|
||||
console.error("Invalid system");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const path_validator = (path: string) => {
|
||||
path = path.replace(/\\/g,"/");
|
||||
|
||||
const IGNORE = true;
|
||||
const APPEND = false;
|
||||
|
||||
const ppath = path_helper.parse(path);
|
||||
const is_directory = ppath.ext == '' && fs.statSync(path_helper.join('.', ppath.dir, ppath.name)).isDirectory();
|
||||
const directory = (is_directory ? path_helper.join(ppath.dir, ppath.name) : ppath.dir).replace(/\\/g,"/");
|
||||
|
||||
//console.log("Is directory %o => %s", is_directory, directory);
|
||||
//console.dir(ppath);
|
||||
|
||||
for(const entry of project_files) {
|
||||
if(entry.type == ProjectEntryType.DIRECTORY) {
|
||||
const dir_entry = <ProjectDirectory>entry;
|
||||
|
||||
if(typeof(dir_entry.path) === 'string') {
|
||||
//ppath.dir == dir_entry.path
|
||||
//console.log("'" + dir_entry.path + "' | '" + directory + "'");
|
||||
if(dir_entry.path.startsWith(directory)) {
|
||||
if(is_directory)
|
||||
return APPEND;
|
||||
}
|
||||
if(directory == dir_entry.path) {
|
||||
//console.log("Math: " + ppath.base + " to " + dir_entry.files);
|
||||
if(dir_entry.files)
|
||||
return ppath.base.match(dir_entry.files) ? APPEND : IGNORE;
|
||||
return APPEND;
|
||||
}
|
||||
if(directory.startsWith(dir_entry.path)) {
|
||||
if(dir_entry.children) {
|
||||
if(is_directory)
|
||||
return APPEND;
|
||||
|
||||
const sub_path = directory.substr(dir_entry.path.length);
|
||||
//console.log("Sub path: " + sub_path + ". Test: " + (path_helper.join(sub_path, ppath.base) + " against " + dir_entry.files);
|
||||
|
||||
if(dir_entry.files)
|
||||
return path_helper.join(sub_path, ppath.base).match(dir_entry.files) ? APPEND : IGNORE;
|
||||
return APPEND;
|
||||
}
|
||||
//TODO test for regex
|
||||
}
|
||||
} else {
|
||||
//TODO
|
||||
}
|
||||
} else if(entry.type == ProjectEntryType.FILE) {
|
||||
const file_entry = <ProjectFile>entry;
|
||||
if(file_entry.path.startsWith(directory) && is_directory)
|
||||
return APPEND;
|
||||
|
||||
if(directory == file_entry.path) {
|
||||
if(typeof(file_entry.name) === 'string' && ppath.base == file_entry.name)
|
||||
return APPEND;
|
||||
else if (ppath.base.match(file_entry.name))
|
||||
return APPEND;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
options.ignore = path => {
|
||||
if(path.length == 0) return false; //Dont ignore root paths
|
||||
|
||||
const ignore_path = path_validator(path);
|
||||
if(!ignore_path) {
|
||||
console.log(" + " + path);
|
||||
}
|
||||
return ignore_path;
|
||||
};
|
||||
|
||||
async function copy_striped(source: string, target: string, symbol_directory: string) {
|
||||
const exec = (command, options) => new Promise<{ stdout: Buffer | string, stderr: Buffer | string}>((resolve, reject) => child_process.exec(command, options, (error, out, err) => error ? reject(error) : resolve({stdout: out, stderr: err})));
|
||||
|
||||
if(process.argv[2] == "win32") {
|
||||
await fs.copy(source, target);
|
||||
return;
|
||||
}
|
||||
if(process.argv[2] != "linux") throw "invalid target type";
|
||||
|
||||
await fs.copy(source, target);
|
||||
|
||||
{
|
||||
const symbols_command = await exec("dump_syms " + target, {
|
||||
maxBuffer: 1024 * 1024 * 512
|
||||
});
|
||||
if(symbols_command.stderr.length != 0) {
|
||||
console.error("Failed to create sys dump: %o", symbols_command.stderr.toString());
|
||||
throw "symbol create failed";
|
||||
}
|
||||
|
||||
const symbols = symbols_command.stdout.toString();
|
||||
const header = symbols.substr(0, symbols.indexOf('\n'));
|
||||
const binary_name = path_helper.basename(target);
|
||||
const dump_id = header.split(" ")[3];
|
||||
if(binary_name != header.split(" ")[4])
|
||||
throw "binary name missmatch";
|
||||
|
||||
const dump_dir = path_helper.join(symbol_directory, binary_name, dump_id);
|
||||
const dump_file = path_helper.join(dump_dir, binary_name + ".sym");
|
||||
if(!(await fs.pathExists(dump_dir)))
|
||||
await fs.mkdirp(dump_dir);
|
||||
await fs.ensureDir(dump_dir);
|
||||
|
||||
console.log("Writing file to %s", dump_file);
|
||||
await fs.writeFile(dump_file, symbols);
|
||||
console.log("Created dump file for binary %s (%s).", binary_name, dump_id);
|
||||
}
|
||||
|
||||
{
|
||||
console.log("Striping file");
|
||||
|
||||
//TODO: Keep node module names!
|
||||
const strip_command = await exec("strip -s " + target, {
|
||||
maxBuffer: 1024 * 1024 * 512
|
||||
});
|
||||
if(strip_command.stderr.length != 0) {
|
||||
console.error("Failed to strip binary: %o", strip_command.stderr.toString());
|
||||
throw "strip failed";
|
||||
}
|
||||
|
||||
const nm_command = await exec("nm " + target, {
|
||||
maxBuffer: 1024 * 1024 * 512
|
||||
});
|
||||
console.log("File stripped. Symbols left: \n%s", nm_command.stderr.toString().trim() || nm_command.stdout.toString().trim());
|
||||
}
|
||||
}
|
||||
async function create_native_addons(target_directory: string, symbol_directory: string) {
|
||||
let node_source = "native/build/" + os.platform() + "_" + os.arch() + "/";
|
||||
console.log("Native addon path: %s", node_source);
|
||||
await fs.ensureDir(target_directory);
|
||||
for(const file of await fs.readdir(node_source)) {
|
||||
if(!file.endsWith(".node")) {
|
||||
console.warn("Discovered non node file within node file out dir");
|
||||
continue;
|
||||
}
|
||||
|
||||
await copy_striped(path_helper.join(node_source, file), path_helper.join(target_directory, file), symbol_directory);
|
||||
}
|
||||
}
|
||||
|
||||
interface UIVersion {
|
||||
channel: string;
|
||||
version: string;
|
||||
git_hash: string;
|
||||
timestamp: number;
|
||||
|
||||
required_client?: string;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
async function create_default_ui_pack(target_directory: string) {
|
||||
const remote_url = "https://clientapi.teaspeak.de/";
|
||||
const channel = "release";
|
||||
|
||||
const file = path_helper.join(target_directory, "default_ui.tar.gz");
|
||||
console.log("Creating default UI pack. Downloading from %s (channel: %s)", remote_url, channel);
|
||||
await fs.ensureDir(target_directory);
|
||||
|
||||
let ui_info: UIVersion;
|
||||
await new Promise((resolve, reject) => {
|
||||
request.get(remote_url + "api.php?" + querystring.stringify({
|
||||
type: "ui-download",
|
||||
channel: channel,
|
||||
version: "latest"
|
||||
}), {
|
||||
timeout: 5000
|
||||
}).on('response', function(response) {
|
||||
if(response.statusCode != 200)
|
||||
reject("Failed to download UI files (Status code " + response.statusCode + ")");
|
||||
|
||||
ui_info = {
|
||||
channel: channel,
|
||||
version: response.headers["x-ui-version"] as string,
|
||||
git_hash: response.headers["x-ui-git-ref"] as string,
|
||||
required_client: response.headers["x-ui-required_client"] as string,
|
||||
timestamp: parseInt(response.headers["x-ui-timestamp"] as string),
|
||||
filename: path_helper.basename(file)
|
||||
}
|
||||
}).on('error', error => {
|
||||
reject("Failed to download UI files: " + error);
|
||||
}).pipe(fs.createWriteStream(file)).on('finish', resolve);
|
||||
});
|
||||
|
||||
if(!ui_info)
|
||||
throw "failed to generate ui info!";
|
||||
|
||||
await fs.writeJson(path_helper.join(target_directory, "default_ui_info.json"), ui_info);
|
||||
console.log("UI-Pack downloaded!");
|
||||
}
|
||||
|
||||
let path;
|
||||
new Promise((resolve, reject) => packager(options, (err, appPaths) => err ? reject(err) : resolve(appPaths))).then(async app_paths => {
|
||||
console.log("Copying changelog file!");
|
||||
/* We dont have promisify in our build system */
|
||||
await fs.copy(path_helper.join(options.dir, "github", "ChangeLog.txt"), path_helper.join(app_paths[0], "ChangeLog.txt"));
|
||||
return app_paths;
|
||||
}).then(async app_paths => {
|
||||
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"));
|
||||
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
|
||||
});
|
||||
return appPaths;
|
||||
}).then(async app_path => {
|
||||
console.log("Fixing versions file");
|
||||
let version = await fs.readFile(path_helper.join(app_path[0], "version"), 'UTF-8');
|
||||
if(!version.startsWith("v"))
|
||||
version = "v" + version;
|
||||
await fs.writeFile(path_helper.join(app_path[0], "version"), version);
|
||||
return app_path;
|
||||
}).then(async () => {
|
||||
if(process.argv[2] == "win32") {
|
||||
console.log("Installing local PDB files");
|
||||
|
||||
const symbol_binary_path = "native/build/" + os.platform() + "_" + os.arch() + "/";
|
||||
const symbol_pdb_path = "native/build/symbols/";
|
||||
const symbol_server_path = path_helper.join(__dirname, "..", "native", "build", "symbol-server");
|
||||
|
||||
const files = [];
|
||||
for(const file of await fs.readdir(symbol_binary_path)) {
|
||||
console.error(file);
|
||||
if(!file.endsWith(".node"))
|
||||
continue;
|
||||
let file_name = path_helper.basename(file);
|
||||
if(file_name.endsWith(".node"))
|
||||
file_name = file_name.substr(0, file_name.length - 5);
|
||||
const binary_path = path_helper.join(symbol_binary_path, file);
|
||||
const pdb_path = path_helper.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
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Gathered %d files", files.length);
|
||||
await deployer.deploy_win_dbg_files(files, version, symbol_server_path);
|
||||
console.log("PDB files deployed");
|
||||
}
|
||||
}).then(() => {
|
||||
console.log("Package created");
|
||||
process.exit(0);
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
console.error("Failed to create package!");
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
export {}
|
@ -1,143 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as _node_ssh from "node-ssh";
|
||||
import * as ssh2 from "ssh2";
|
||||
import * as util from "util";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
declare namespace node_ssh {
|
||||
export type PutFilesOptions = {
|
||||
sftp?: Object,
|
||||
sftpOptions?: Object,
|
||||
concurrency?: number,
|
||||
}
|
||||
export type PutDirectoryOptions = {
|
||||
sftp?: Object,
|
||||
sftpOptions?: Object,
|
||||
concurrency?: number,
|
||||
recursive?: boolean,
|
||||
tick?: ((localPath: string, remotePath: string, error?: Error) => void),
|
||||
validate?: ((localPath: string) => boolean),
|
||||
}
|
||||
export type ExecOptions = {
|
||||
cwd?: string,
|
||||
options?: Object // passed to ssh2.exec
|
||||
stdin?: string,
|
||||
stream?: 'stdout' | 'stderr' | 'both',
|
||||
onStdout?: ((chunk: Buffer) => void),
|
||||
onStderr?: ((chunk: Buffer) => void),
|
||||
}
|
||||
|
||||
export class Instance {
|
||||
connect(config: ssh2.ConnectConfig): Promise<this>
|
||||
requestSFTP(): Promise<ssh2.SFTPWrapper>
|
||||
requestShell(): Promise<ssh2.ClientChannel>
|
||||
mkdir(path: string, method: 'sftp' | 'exec', givenSftp?: Object): Promise<string>
|
||||
exec(command: string, parameters: Array<string>, options: ExecOptions): Promise<Object | string>
|
||||
execCommand(command: string, options?: { cwd: string, stdin: string }): Promise<{ stdout: string, options?: Object, stderr: string, signal?: string, code: number }>
|
||||
putFile(localFile: string, remoteFile: string, sftp?: Object, opts?: Object): Promise<void>
|
||||
getFile(localFile: string, remoteFile: string, sftp?: Object, opts?: Object): Promise<void>
|
||||
putFiles(files: Array<{ local: string, remote: string }>, options: PutFilesOptions): Promise<void>
|
||||
putDirectory(localDirectory: string, remoteDirectory: string, options: PutDirectoryOptions): Promise<boolean>
|
||||
dispose(): void
|
||||
}
|
||||
}
|
||||
|
||||
let instance: node_ssh.Instance;
|
||||
export async function setup() {
|
||||
if(instance)
|
||||
throw "already initiaized";
|
||||
instance = new _node_ssh();
|
||||
try {
|
||||
await instance.connect({
|
||||
host: 'deploy.teaspeak.de',
|
||||
username: 'TeaSpeak-Jenkins-Client',
|
||||
privateKey: path.join(__dirname, "ssh_key")
|
||||
})
|
||||
} catch(error) {
|
||||
try { instance.dispose(); } finally { instance = undefined; }
|
||||
console.error("Failed to connect: %o", error);
|
||||
throw "failed to connect";
|
||||
}
|
||||
}
|
||||
async function update_remote_file(local_path: string, remote_path: string) {
|
||||
if(!instance)
|
||||
throw "Invalid instance";
|
||||
|
||||
let sftp: ssh2.SFTPWrapper;
|
||||
try {
|
||||
let sha512_remote, sha512_local;
|
||||
try {
|
||||
const sha512_remote_result = await instance.execCommand('sha512sum ' + remote_path);
|
||||
if(sha512_remote_result.code != 0)
|
||||
sha512_remote = undefined; /* file does not exists! */
|
||||
else {
|
||||
const result = sha512_remote_result.stdout.toString();
|
||||
sha512_remote = result.split(" ")[0];
|
||||
//console.log("File %s has a remote sha512: %o", remote_path, sha512_remote);
|
||||
}
|
||||
} catch(error) {
|
||||
console.log("Failed to calculate remote sha521 for file %s: %o", remote_path, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if(sha512_remote) { /* if the remote hasn't the file then we've def a "new" version */
|
||||
const hash_processor = crypto.createHash('sha512');
|
||||
const local_stream = fs.createReadStream(local_path);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
local_stream.on('error', reject);
|
||||
local_stream.on('data', chunk => hash_processor.update(chunk));
|
||||
local_stream.on('end', resolve);
|
||||
});
|
||||
sha512_local = hash_processor.digest('hex');
|
||||
local_stream.close();
|
||||
}
|
||||
|
||||
if(sha512_remote) {
|
||||
if(sha512_remote == sha512_local) {
|
||||
console.log("File %s (%s) is already up to date.", path.basename(local_path), local_path);
|
||||
return;
|
||||
} else {
|
||||
console.log("Updating file %s (%s) at %s. Local sum: %s Remote sum: %s", path.basename(local_path), local_path, remote_path, sha512_local, sha512_remote);
|
||||
}
|
||||
} else {
|
||||
console.log("Uploading file %s (%s) to %s.", path.basename(local_path), local_path, remote_path);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await instance.putFile(local_path, remote_path);
|
||||
} catch(error) {
|
||||
console.error("Failed to upload file %s (%s): %s", path.basename(local_path), local_path, error);
|
||||
throw "Upload failed";
|
||||
}
|
||||
} finally {
|
||||
if(sftp)
|
||||
sftp.end();
|
||||
}
|
||||
}
|
||||
|
||||
export async function deploy_crash_dumps(local_path: string, remote_path: string) {
|
||||
console.log("Uploading crash dumps from %s to %s", local_path, remote_path);
|
||||
|
||||
const do_dir = async (local_path, remote_path) => {
|
||||
for(const file of await util.promisify(fs.readdir)(local_path)) {
|
||||
const local_file = path.join(local_path, file);
|
||||
const remote_file = remote_path + "/" + file;
|
||||
|
||||
if((await util.promisify(fs.stat)(local_file)).isDirectory())
|
||||
await do_dir(local_file, remote_file);
|
||||
else
|
||||
await update_remote_file(local_file, remote_file);
|
||||
}
|
||||
};
|
||||
|
||||
await do_dir(local_path, remote_path);
|
||||
}
|
||||
|
||||
const test = async () => {
|
||||
await setup();
|
||||
await deploy_crash_dumps(path.join(__dirname, "../../build/symbols/"), "symbols");
|
||||
};
|
||||
test();
|
@ -1,3 +0,0 @@
|
||||
```$bash
|
||||
ssh-keygen -t rsa -b 4096 -f ssh_key -N ''
|
||||
```
|
@ -1,239 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as _node_ssh from "node-ssh";
|
||||
import * as ssh2 from "ssh2";
|
||||
import * as stream from "stream";
|
||||
import * as child_process from "child_process";
|
||||
import * as util from "util";
|
||||
import {FileEntry} from "ssh2-streams";
|
||||
|
||||
declare namespace node_ssh {
|
||||
export type PutFilesOptions = {
|
||||
sftp?: Object,
|
||||
sftpOptions?: Object,
|
||||
concurrency?: number,
|
||||
}
|
||||
export type PutDirectoryOptions = {
|
||||
sftp?: Object,
|
||||
sftpOptions?: Object,
|
||||
concurrency?: number,
|
||||
recursive?: boolean,
|
||||
tick?: ((localPath: string, remotePath: string, error?: Error) => void),
|
||||
validate?: ((localPath: string) => boolean),
|
||||
}
|
||||
export type ExecOptions = {
|
||||
cwd?: string,
|
||||
options?: Object // passed to ssh2.exec
|
||||
stdin?: string,
|
||||
stream?: 'stdout' | 'stderr' | 'both',
|
||||
onStdout?: ((chunk: Buffer) => void),
|
||||
onStderr?: ((chunk: Buffer) => void),
|
||||
}
|
||||
|
||||
export class Instance {
|
||||
connect(config: ssh2.ConnectConfig): Promise<this>
|
||||
requestSFTP(): Promise<ssh2.SFTPWrapper>
|
||||
requestShell(): Promise<ssh2.ClientChannel>
|
||||
mkdir(path: string, method: 'sftp' | 'exec', givenSftp?: Object): Promise<string>
|
||||
exec(command: string, parameters: Array<string>, options: ExecOptions): Promise<Object | string>
|
||||
execCommand(command: string, options?: { cwd: string, stdin: string }): Promise<{ stdout: string, options?: Object, stderr: string, signal?: string, code: number }>
|
||||
putFile(localFile: string, remoteFile: string, sftp?: Object, opts?: Object): Promise<void>
|
||||
getFile(localFile: string, remoteFile: string, sftp?: Object, opts?: Object): Promise<void>
|
||||
putFiles(files: Array<{ local: string, remote: string }>, options: PutFilesOptions): Promise<void>
|
||||
putDirectory(localDirectory: string, remoteDirectory: string, options: PutDirectoryOptions): Promise<boolean>
|
||||
dispose(): void
|
||||
}
|
||||
}
|
||||
|
||||
let instance: node_ssh.Instance;
|
||||
export async function setup() {
|
||||
if(instance)
|
||||
throw "already initiaized";
|
||||
instance = new _node_ssh();
|
||||
try {
|
||||
await instance.connect({
|
||||
host: 'deploy.teaspeak.de',
|
||||
username: 'TeaSpeak-Jenkins-Client',
|
||||
privateKey: path.join(__dirname, "ssh_key")
|
||||
})
|
||||
} catch(error) {
|
||||
try { instance.dispose(); } finally { instance = undefined; }
|
||||
console.error("Failed to connect: %o", error);
|
||||
throw "failed to connect";
|
||||
}
|
||||
}
|
||||
|
||||
function read_stream_full(stream: stream.Readable) : Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const buffers = [];
|
||||
stream.on('data', buffer => buffers.push(buffer));
|
||||
stream.on('end', () => resolve(Buffer.concat(buffers)));
|
||||
stream.on('error', error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
export type PlatformSpecs = {
|
||||
system: 'linux' | 'windows' | 'osx';
|
||||
arch: 'amd64' | 'x86';
|
||||
type: 'indev' | 'debug' | 'optimized' | 'stable';
|
||||
}
|
||||
|
||||
export type Version = {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
type?: 'indev' | 'beta';
|
||||
}
|
||||
|
||||
//<system>/<arch>_<type>/
|
||||
function platform_path(platform: PlatformSpecs) {
|
||||
return platform.system + "/" + platform.arch + "_" + platform.type + "/";
|
||||
}
|
||||
|
||||
function version_string(version: Version) {
|
||||
return version.major + "." + version.minor + "." + version.patch + (version.type ? "-" + version.type : "");
|
||||
}
|
||||
|
||||
export async function latest_version(platform: PlatformSpecs) {
|
||||
const path = "versions/" + platform_path(platform);
|
||||
if(!instance)
|
||||
throw "Invalid instance";
|
||||
const sftp = await instance.requestSFTP();
|
||||
try {
|
||||
if(!sftp)
|
||||
throw "failed to request sftp";
|
||||
|
||||
try {
|
||||
const data_stream = sftp.createReadStream(path + "latest");
|
||||
const data = await read_stream_full(data_stream);
|
||||
return data.toString();
|
||||
} catch(error) {
|
||||
if(error instanceof Error && error.message == "No such file")
|
||||
return undefined;
|
||||
|
||||
console.log("Failed to receive last version: %o", error);
|
||||
return undefined;
|
||||
}
|
||||
} finally {
|
||||
if(sftp)
|
||||
sftp.end();
|
||||
}
|
||||
}
|
||||
|
||||
export async function generate_build_index(platform: PlatformSpecs, version: Version) : Promise<number> {
|
||||
const path = "versions/" + platform_path(platform);
|
||||
const version_str = version_string(version);
|
||||
if(!instance)
|
||||
throw "Invalid instance";
|
||||
const sftp = await instance.requestSFTP();
|
||||
try {
|
||||
if(!sftp)
|
||||
throw "failed to request sftp";
|
||||
|
||||
try {
|
||||
const files = await new Promise<FileEntry[]>((resolve, reject) => sftp.readdir(path, (error, result) => error ? reject(error) : resolve(result)));
|
||||
const version_files = files.filter(e => e.filename.startsWith(version_str));
|
||||
if(version_files.length == 0)
|
||||
return 0;
|
||||
let index = 1;
|
||||
while(version_files.find(e => e.filename.toLowerCase() === version_str + "-" + index)) index++;
|
||||
return index;
|
||||
} catch(error) {
|
||||
if(error instanceof Error && error.message == "No such file")
|
||||
return 0;
|
||||
|
||||
console.log("Failed to receive versions list: %o", error);
|
||||
return undefined;
|
||||
}
|
||||
} finally {
|
||||
if(sftp)
|
||||
sftp.end();
|
||||
}
|
||||
}
|
||||
|
||||
export type WinDbgFile = {
|
||||
binary: string,
|
||||
pdb: string;
|
||||
};
|
||||
export async function deploy_win_dbg_files(files: WinDbgFile[], version: Version, path?: string) : Promise<void> {
|
||||
//symstore add /r /f .\*.node /s \\deploy.teaspeak.de\symbols /t "TeaClient-Windows-amd64" /v "x.y.z"
|
||||
//symstore add /r /f .\*.* /s \\deploy.teaspeak.de\symbols /t "TeaClient-Windows-amd64" /v "1.0.0"
|
||||
const server_path = typeof(path) === "string" && path ? path : "\\\\deploy.teaspeak.de\\symbols\\symbols";
|
||||
const vstring = version_string(version);
|
||||
const exec = util.promisify(child_process.exec);
|
||||
for(const file of files) {
|
||||
console.log("Deploying %s to %s", file, server_path);
|
||||
let current_file;
|
||||
try {
|
||||
{
|
||||
const result = await exec("symstore add /r /f " + file.binary + " /s " + server_path + " /t \"TeaClient-Windows-amd64\" /v \"" + vstring + "\"");
|
||||
if(result.stdout)
|
||||
console.log("Stdout: %s", result.stdout);
|
||||
if(result.stderr)
|
||||
console.log("Stderr: %s", result.stderr);
|
||||
}
|
||||
{
|
||||
const result = await exec("symstore add /r /f " + file.pdb + " /s " + server_path + " /t \"TeaClient-Windows-amd64\" /v \"" + vstring + "\"");
|
||||
if(result.stdout)
|
||||
console.log("Stdout: %s", result.stdout);
|
||||
if(result.stderr)
|
||||
console.log("Stderr: %s", result.stderr);
|
||||
}
|
||||
} catch(error) {
|
||||
if('killed' in error && 'code' in error) {
|
||||
const perror: {
|
||||
killed: boolean,
|
||||
code: number,
|
||||
signal: any,
|
||||
cmd: string,
|
||||
stdout: string,
|
||||
stderr: string
|
||||
} = error;
|
||||
console.error("Failed to deploy %s file %s:", current_file, file);
|
||||
console.log(" Code: %d", perror.code);
|
||||
{
|
||||
console.error(" Stdout: ");
|
||||
for(const element of perror.stdout.split("\n"))
|
||||
console.error(" %s", element);
|
||||
}
|
||||
{
|
||||
console.error(" Stderr: ");
|
||||
for(const element of perror.stderr.split("\n"))
|
||||
console.error(" %s", element);
|
||||
}
|
||||
} else
|
||||
console.error("Failed to deploy %s file %s: %o", current_file, file, error);
|
||||
throw "deploy failed";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const test = async () => {
|
||||
await setup();
|
||||
console.log(await latest_version({
|
||||
arch: 'amd64',
|
||||
system: 'linux',
|
||||
type: 'optimized'
|
||||
}));
|
||||
console.log(await generate_build_index({
|
||||
arch: 'amd64',
|
||||
system: 'linux',
|
||||
type: 'optimized'
|
||||
}, {
|
||||
type: 'beta',
|
||||
patch: 19,
|
||||
minor: 3,
|
||||
major: 1
|
||||
}));
|
||||
/*
|
||||
console.log(await deploy_pdb_files(
|
||||
[path.join(__dirname, "..", "..", "native", "build", "symbols", "teaclient_crash_handler.pdb")], {
|
||||
type: 'beta',
|
||||
patch: 19,
|
||||
minor: 3,
|
||||
major: 1
|
||||
}
|
||||
))
|
||||
*/
|
||||
};
|
||||
test();
|
@ -1,24 +0,0 @@
|
||||
@echo off
|
||||
|
||||
set drive=T:
|
||||
|
||||
|
||||
net use z: /dele > nul
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
if %ERRORLEVEL% neq 2 (
|
||||
echo Failed to unmount old drive [%drive%\]: %errorlevel%.
|
||||
exit /b %errorlevel%
|
||||
) else (
|
||||
echo Drive [%drive%\] hasn't been mapped. Mapping device!
|
||||
)
|
||||
) else (
|
||||
echo Unmounted old device. Mapping new device!
|
||||
)
|
||||
rem /persistent:yes
|
||||
net use z: \\deploy.teaspeak.de\symbols T4j7CTADvCMev4Kr /user:deploy.teaspeak.de\TeaSpeak-Jenkins-Client
|
||||
if %errorlevel% neq 0 (
|
||||
echo Failed to mount drive [%drive%\]: %errorlevel%
|
||||
exit /b %errorlevel%
|
||||
) else (
|
||||
echo New device has been mapped successfully!
|
||||
)
|
@ -1,51 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIJKAIBAAKCAgEAnr2tvPgMv9uLdFFHCV6sX9uUZyK57cQNHLSVK/OGjdilf95q
|
||||
RMjI8AkYxM0nc9ljnVNPuhSYWSASTCeCPaBS/VkvUn3krEJQw1Z9GPOWbZo0vLbE
|
||||
LzhZ8uyCgdOZSuWJpHui4Tchz10pxcKACi5wL4MFaouoJUcGX1MgFJ1Dcd/BxuVj
|
||||
1hKrOYLBJy1cDuocmP4aTu8mgtzluRMieTjH1fRq7SAbJDx5RmzsEy5S6+6XDuLq
|
||||
uidXbsoM3GEYhcIHjtBTcbKVsOLmelqR/byGUSFP0tmOLcq85nGuOf0Uccb1DuKA
|
||||
RDZ53OfsGus2/Gbb0z9nipOtEkP6abgvhas3VNWI+JQ+YhLnwfBPy7hNytQFVN5q
|
||||
Q0+HaQ1SXdp+Ze3moKPqCHXrlbCvk8FpQoGhzEIteP/xq6DPeHfHZ/T7mH6T8JzF
|
||||
H3HfGuAfaDMk6f1dg5jHs1L+vaWtWI3SZohj/LeN31TQsPJJbQ9ltK2Eu1hsy9mY
|
||||
BAXv39yR+os1gpsx5imIQfVMFQ5Fg8W2lNyXjn2S/zWfY76G0vEDOomE4BhMjd2b
|
||||
Tn2ZzpAba9bPfgmyv6Kjo4mDXv+qVddHzsivKaV3u7bTAYZMpekcEDs7wLPHKC6+
|
||||
Ca4Z0jXXEtwAeqW/wqop3Hi5gNruj4+FEWsRvC/431AxXnmyo1WEenx27o0CAwEA
|
||||
AQKCAgBvKjfsGO2cwhuj5nNPzXv7WSNRIpGAP4ZLLu80K0N0PF6R8GkNKGsHJeex
|
||||
klXpnDhVaY1wq5GRAJOvvw9HJupXP0iThVRJidtTIFNU0OjsckwyR++VfeoH47b1
|
||||
QgCc4agFhwummlxUAlMJic7u0lx/+UomtgyXpmiBAw55QTSFH4RtTCEhPkuoZ7fq
|
||||
Pqq0SyChx3kXBAU9KYK6m/rNV4UigRsMWiqHss3fEtI0EIHDdX9VznVAzeI91MMy
|
||||
5dAtg5aVXbDB86U+jXeIAbsxLQAG+sQSzYXy7YJiAwRW2bOihgkBVn8qxdeLauL+
|
||||
avBDy6hwBHv3ILnYC8DmnjSxcBi7do/MNIEm5DEs90Lt8Oyh033e+riw7NpLyzY0
|
||||
ZFJ/ScfLAzq/UIooCSGIIbpdPs7c8oER6kPj4X5Gv1unh3u9A0ZGGcrDtxS/f/eo
|
||||
/05ohAFwb31v4KM4eCvHqoaSqKVypFplrslObqxgPQsR/Ecw+gezBAqYmJnbMVRU
|
||||
8RjNnPGbLulG4HnXxA2dLpqOEU7/FJN7bN8U7rdCqcm/WDbBlVUhNm7jjzDr2OFU
|
||||
exJMRZvXfRgdqgk59+RPizanRQIMs7GULCtVcJGWlSUfdo2KIuhfFJ/LVIcgm7oo
|
||||
dFTZVGMOc2KqoRfBnZz6yr74HcexHSlmQZSfIoQhbiiDNexOIQKCAQEA0UMDYrum
|
||||
FVzkisI0QSdq3h/9KFexYnp4ZYYP83FaZl2js7PAYIIcRcXxVb/i9AJx38FmPH9s
|
||||
1uMb/PyLGdHGF/qvYrjYqZLZ64eKVlnQGWcU5eOQ9560l8Je5FUfR9IwC3LTG+3R
|
||||
3QByPANOn19D0GunCJ7cTmwdoedTGPeUxyK7ZTodXn3YHBikLX9atroiRkMe+h4U
|
||||
fg8R2WV8Hul1PPcVqQ3Lz2aNm1mjfVe+nOghcNrmcI0t03BdXCCTcz3NLTJxwAeC
|
||||
4TcXlENUo78OcxsZj4hq84xhauN3npT7sa/5JGcC7c7N+EULh/CTH/CK+zsFUYfD
|
||||
IHC88Vu9fnXcVQKCAQEAwjIHcVkru0apkF65f1AVIyEQJ2sWpndHslAAfSMxEUbL
|
||||
sMOii5QH5MKA8LRAGpDEoZY2XZrrOpuvnvxKIJt7Ijj4wfYF8ffYTyI0oK7gsR6v
|
||||
nvY7i+n18pzHZFJLKq5Z9n4zgEi2YBj802v0GfQyCjk3+8we7Man/Uz4jYZr8nxZ
|
||||
YPy+viBLP5nGp4R7y6nef93prfTL6iApXyr3MdFDMnJBGmiKNGW9UrpKFyFkwvE4
|
||||
7sdHUXj2JW/AM8P7CeNA3x2GQ1l0qS8QJ3R0X9GzTeq5tuIKUH2mDrPiBXisRG6k
|
||||
IYwKb0l8iv7sTLyfMv60T5uUCrS8hompHDtWsh0BWQKCAQBMSOSsEoIaGZIK738D
|
||||
HW586SZtlYJJxyGqyPN5qKHu3UX3FZkU1XmfCejPfLMshtOiYSt29HDl6Ubjs+C1
|
||||
md5gEXfsQjxhnPIqRW/tyLHvAMACijHnwwhMpoPXMxzDHuF62vIQpWKy8R2zuPTp
|
||||
bl4XVZc/skHXqNwokF6fpGmtKoEsBsJ8Ft44Z9c56spUAIjMGl3pihuoVLAKE0/r
|
||||
KOofPme8CBZ7VgRbVJMf92O6aXj/Xh1RfHXvNXAjTJDUGvx39IK5IUPZ/C5xUxZA
|
||||
1z5aQc/Qnkd2338H60JJIkCa5u6pEZBkxtYZInpwpQfNRfA0Y7CtpxM/+Tk3t1ze
|
||||
A/M9AoIBABmp7Ovg4fOk+gG3UwJtPe3fj7f14g9r0hDRm87t2K000vRwVknl7Ukh
|
||||
H1MwLwyTtzi3lkW2lIGxU3tKUi2O/q3eI5nWfqCkpXSHy7a0hcNCj+kNF399EuDW
|
||||
MU+jxIVGd2Mo+HtqoJeAleEG8kJ/0CEjwK9JIYkfE9JY2rwxWJC6OEGmBTsxH2Cv
|
||||
XN6ElquqrlntpNU1dcFiMLWAAx0VT7EaAlqQGDumeme1cNcvtZZBtMlxko5E0xrN
|
||||
cvQkYUfEPa1+xGCgMNeu/Y6JSFvlZbHVZGez5bMPd+OXiDY65WFB0fURAcwFRS1F
|
||||
VUsq3ksp+ABRSjZD/mo1RSETAnkVdjkCggEBAJwC6yt1x9yIAh9RL6AKN2VUAopJ
|
||||
Ot2vnAp6dHMEwkKQY3hseAJx7YkAb5KaTpEZ50VhXOxe5lOz+4eagXL2CelZG1xN
|
||||
jDTjIsReQflGn+jwYT1w35u+CPnDCizlA82cLaR5dlaOc/Np3hKWzvc6sAXzjmxN
|
||||
05+YfB70gbz/u4GE+KFiylArQfJ+2WRxoyUpy3YmbOq7LhadV7GOzYb2ufYgv1cs
|
||||
US58bqWNx9NXSnJQ1uPDUaaSSfUvGJVJHd602h5m3PLxGIzBFrSsgwx9r6qLXPZ3
|
||||
P6XiwAuMOqqEbFfBtJnCxUta9hNoAg4Vo8NV89c92uSFSEJ4xjlKHwOu0U8=
|
||||
-----END RSA PRIVATE KEY-----
|
@ -1 +0,0 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCeva28+Ay/24t0UUcJXqxf25RnIrntxA0ctJUr84aN2KV/3mpEyMjwCRjEzSdz2WOdU0+6FJhZIBJMJ4I9oFL9WS9SfeSsQlDDVn0Y85ZtmjS8tsQvOFny7IKB05lK5Ymke6LhNyHPXSnFwoAKLnAvgwVqi6glRwZfUyAUnUNx38HG5WPWEqs5gsEnLVwO6hyY/hpO7yaC3OW5EyJ5OMfV9GrtIBskPHlGbOwTLlLr7pcO4uq6J1duygzcYRiFwgeO0FNxspWw4uZ6WpH9vIZRIU/S2Y4tyrzmca45/RRxxvUO4oBENnnc5+wa6zb8ZtvTP2eKk60SQ/ppuC+FqzdU1Yj4lD5iEufB8E/LuE3K1AVU3mpDT4dpDVJd2n5l7eago+oIdeuVsK+TwWlCgaHMQi14//GroM94d8dn9PuYfpPwnMUfcd8a4B9oMyTp/V2DmMezUv69pa1YjdJmiGP8t43fVNCw8kltD2W0rYS7WGzL2ZgEBe/f3JH6izWCmzHmKYhB9UwVDkWDxbaU3JeOfZL/NZ9jvobS8QM6iYTgGEyN3ZtOfZnOkBtr1s9+CbK/oqOjiYNe/6pV10fOyK8ppXe7ttMBhkyl6RwQOzvAs8coLr4JrhnSNdcS3AB6pb/CqinceLmA2u6Pj4URaxG8L/jfUDFeebKjVYR6fHbujQ== wolverindev@WolverinDEV
|
@ -1,179 +0,0 @@
|
||||
import * as fs from "fs-extra";
|
||||
import * as tar from "tar-stream";
|
||||
import * as zlib from "zlib";
|
||||
import * as path from "path";
|
||||
import * as asar from "asar";
|
||||
|
||||
import {Pack} from "tar-stream";
|
||||
import * as request from "request";
|
||||
import {Version} from "../modules/shared/version";
|
||||
|
||||
async function append_dir(parent: string, path: string, pack: Pack, excludes: (string | RegExp)[]) {
|
||||
const entries = await fs.readdir(parent + "/" + path);
|
||||
for(let entry of entries) {
|
||||
console.log(entry);
|
||||
const stat = await fs.stat(parent + "/" + path + "/" + entry);
|
||||
if(stat.isDirectory()) {
|
||||
console.log("Add sub: %s", entry);
|
||||
await append_dir(parent, path + "/" + entry, pack, excludes);
|
||||
} else {
|
||||
let exclude = false;
|
||||
for(const pattern of excludes) {
|
||||
if((path + "/" + entry).match(pattern)) {
|
||||
console.log("Excluding file %s", path + "/" + entry);
|
||||
exclude = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(exclude) continue;
|
||||
|
||||
let pentry = pack.entry({
|
||||
name: path + "/" + entry,
|
||||
size: stat.size,
|
||||
mode: stat.mode
|
||||
}, error => {
|
||||
if(error) throw error;
|
||||
});
|
||||
|
||||
if(!pentry) throw "Failed to create new file";
|
||||
|
||||
const pipe = fs.createReadStream(parent + "/" + path + "/" + entry).pipe(pentry);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
pipe.on('finish', resolve);
|
||||
pipe.on('error', reject);
|
||||
});
|
||||
pentry.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function pack_update(source: string, dest: string) : Promise<string> {
|
||||
await fs.mkdirs(path.dirname(dest));
|
||||
const target = fs.createWriteStream(dest);
|
||||
const pack = tar.pack();
|
||||
const compress = zlib.createGzip();
|
||||
|
||||
pack.pipe(compress).pipe(target);
|
||||
|
||||
await append_dir(source, ".", pack, [/.\/app_versions\/.*/]); ///.\/postzip($|.exe)/,
|
||||
pack.finalize();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
target.on('close', resolve);
|
||||
target.on('error', reject);
|
||||
});
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
export async function pack_info(src: string) : Promise<any> {
|
||||
const appAsarPath = path.join(src, 'resources/app.asar');
|
||||
const appPackageJSONPath = path.join(src, 'resources/app/package.json');
|
||||
|
||||
if(await fs.pathExists(appAsarPath))
|
||||
return JSON.parse(asar.extractFile(appAsarPath, "package.json"));
|
||||
else
|
||||
return await fs.readJson(appPackageJSONPath);
|
||||
}
|
||||
|
||||
interface InfoEntry {
|
||||
platform: string;
|
||||
arch: string;
|
||||
update: string;
|
||||
install: string;
|
||||
}
|
||||
|
||||
export async function write_info(file: string, platform: string, arch: string, update_file: string, install_file: string) {
|
||||
let infos: InfoEntry[] = fs.existsSync(file) ? await fs.readJson(file) as InfoEntry[] : [];
|
||||
for(const entry of infos.slice()) {
|
||||
if(entry.platform == platform && entry.arch == arch)
|
||||
infos.splice(infos.indexOf(entry),1);
|
||||
}
|
||||
infos.push({
|
||||
"platform": platform,
|
||||
"arch": arch,
|
||||
"update": update_file,
|
||||
"install": install_file
|
||||
});
|
||||
await fs.writeJson(file, infos);
|
||||
}
|
||||
|
||||
interface VersionFile {
|
||||
release: VersionEntry[];
|
||||
beta: VersionEntry[];
|
||||
}
|
||||
|
||||
interface VersionEntry {
|
||||
platform: string;
|
||||
arch: string;
|
||||
version: Version;
|
||||
}
|
||||
export async function write_version(file: string, platform: string, arch: string, channel: string, version: Version) {
|
||||
let versions: VersionFile = fs.existsSync(file) ? await fs.readJson(file) as VersionFile : {} as any;
|
||||
|
||||
versions[channel] = versions[channel] || [];
|
||||
let channel_data = versions[channel];
|
||||
|
||||
for(const entry of channel_data.slice()) {
|
||||
if(entry.platform == platform && entry.arch == arch)
|
||||
channel_data.splice(channel_data.indexOf(entry), 1);
|
||||
}
|
||||
|
||||
channel_data.push({
|
||||
platform: platform,
|
||||
arch: arch,
|
||||
version: version
|
||||
});
|
||||
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) => {
|
||||
const url = (process.env["teaclient_deploy_url"] || "http://clientapi.teaspeak.de/") + "api.php";
|
||||
console.log("Requesting " + url);
|
||||
console.log("Uploading update file " + update_file);
|
||||
console.log("Uploading install file " + install_file);
|
||||
console.log("Secret (env key: teaclient_deploy_secret): " + process.env["teaclient_deploy_secret"]);
|
||||
if(!process.env["teaclient_deploy_secret"]) throw "Missing secret!";
|
||||
|
||||
|
||||
request.post(url, {
|
||||
formData: {
|
||||
type: "deploy-build",
|
||||
secret: process.env["teaclient_deploy_secret"],
|
||||
|
||||
platform: platform,
|
||||
arch: arch,
|
||||
version: JSON.stringify(version),
|
||||
channel: channel,
|
||||
|
||||
update: fs.createReadStream(update_file),
|
||||
update_suffix: "tar.gz",
|
||||
|
||||
installer: fs.createReadStream(install_file),
|
||||
installer_suffix: install_suffix
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
if(error) {
|
||||
console.error("Failed to upload:");
|
||||
console.error(error);
|
||||
throw "Failed to upload: " + error;
|
||||
}
|
||||
console.log("Response code: " + (response ? response.statusCode : 0));
|
||||
let info;
|
||||
if(response && response.statusCode == 413) {
|
||||
info = {msg: "Files too large! Increase limits!"};
|
||||
} else {
|
||||
try {
|
||||
info = JSON.parse(body);
|
||||
} catch (error) {
|
||||
info = {};
|
||||
console.dir(body);
|
||||
}
|
||||
}
|
||||
console.dir(info);
|
||||
if(!info["success"]) throw info["msg"] || "Could not deploy files!";
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
const installer = require("electron-installer-debian");
|
||||
import * as packager from "./package";
|
||||
import {parse_version, Version} from "../modules/shared/version";
|
||||
|
||||
const package_path = "build/TeaClient-linux-x64/";
|
||||
const filename_update = "TeaClient-linux-x64.tar.gz";
|
||||
|
||||
let options = {
|
||||
src: package_path,
|
||||
dest: undefined,
|
||||
dest_file: undefined,
|
||||
arch: 'amd64',
|
||||
|
||||
rename: (directory, name) => {
|
||||
console.log("Destination directory: " + directory);
|
||||
console.log("Destination name : " + name);
|
||||
options.dest_file = directory + "/" + name;
|
||||
return directory + "/" + name;
|
||||
},
|
||||
options: {
|
||||
name: "TeaClient",
|
||||
productName: "TeaClient",
|
||||
genericName: "TeaSpeak - Client",
|
||||
description: "TeaClient by TeaSpeak",
|
||||
version: undefined,
|
||||
homepage: "https://teaspeak.de",
|
||||
maintainer: "WolverinDEV <client@teaspeak.de>",
|
||||
|
||||
icon: 'resources/logo.svg',
|
||||
categories: [
|
||||
"Utility"
|
||||
],
|
||||
bin: 'TeaClient'
|
||||
}
|
||||
};
|
||||
|
||||
if(process.argv.length < 3) {
|
||||
console.error("Missing build channel!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
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.dest = "build/output/" + process.argv[2] + "/" + options.options.version + "/";
|
||||
console.log('Creating package for version ' + options.options.version + ' (this may take a while)');
|
||||
|
||||
//return Promise.resolve();
|
||||
return installer(options);
|
||||
}).then(() => {
|
||||
if(!options.dest_file)
|
||||
options.dest_file = options.dest + "TeaClient_" + options.options.version + "_amd64.deb";
|
||||
|
||||
console.log(`Successfully created package at ${options.dest} (${options.dest_file})`);
|
||||
return packager.pack_update(options.src, options.dest + "/" + filename_update);
|
||||
}).then(() => {
|
||||
return packager.write_info(options.dest + "info.json", "linux", "x64", filename_update, options.dest_file)
|
||||
}).then(() => {
|
||||
return packager.write_version("build/output/version.json", "linux", "x64", process.argv[2], version);
|
||||
}).then(() => {
|
||||
console.log("Deploying symbol files");
|
||||
//FIXME!
|
||||
}).then(() => {
|
||||
//Fixup in case of skip of the packaging
|
||||
|
||||
console.log("Deploying build");
|
||||
return packager.deploy("linux", "x64", process.argv[2], version, options.dest + filename_update, options.dest_file, "deb");
|
||||
}).then(() => {
|
||||
console.log("Build version (" + options.options.version + ") created!");
|
||||
clearInterval(alive);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to pack package!");
|
||||
console.error(err, err.stack);
|
||||
process.exit(1)
|
||||
});
|
||||
|
@ -1,88 +0,0 @@
|
||||
import * as packager from "./package";
|
||||
import * as deployer from "./deploy";
|
||||
import {parse_version, Version} from "../modules/shared/version";
|
||||
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const util = require('util');
|
||||
const cproc = require('child_process');
|
||||
const proc = require('process');
|
||||
const ejs = require('ejs');
|
||||
const exec = util.promisify(cproc.exec);
|
||||
const ejs_render = util.promisify(ejs.renderFile);
|
||||
|
||||
const filename_update = "TeaClient-windows-x64.tar.gz";
|
||||
const filename_installer = "TeaClient-windows-x64.exe";
|
||||
const package_path = "build/TeaClient-win32-x64/";
|
||||
const symbol_pdb_path = "native/build/symbols/";
|
||||
const symbol_binary_path = package_path + "/resources/natives/";
|
||||
let dest_path = undefined;
|
||||
let info;
|
||||
let version: Version;
|
||||
async function make_template() : Promise<string> {
|
||||
const content = await ejs_render("installer/WinInstall.ejs", {
|
||||
source_dir: path.resolve(package_path) + "/*",
|
||||
dest_dir: path.resolve(dest_path),
|
||||
icon_file: path.resolve("resources/logo.ico"),
|
||||
version: info["version"],
|
||||
executable_name: filename_installer.substr(0, filename_installer.length - 4) //Remove the .exe
|
||||
}, {});
|
||||
|
||||
await fs.mkdirs(dest_path);
|
||||
fs.writeFileSync(dest_path + "/" + "installer.iss", content);
|
||||
return dest_path + "/" + "installer.iss";
|
||||
}
|
||||
|
||||
async function make_installer(path: string) {
|
||||
console.log("Compiling path %s", path);
|
||||
const { stdout, stderr } = await exec("\"C:\\Program Files (x86)\\Inno Setup 5\\iscc.exe\" " + path, {maxBuffer: 1024 * 1024 * 1024}); //FIXME relative path?
|
||||
}
|
||||
if(process.argv.length < 3) {
|
||||
console.error("Missing build channel!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
packager.pack_info(package_path).then(async _info => {
|
||||
info = _info;
|
||||
version = parse_version(_info["version"]);
|
||||
dest_path = "build/output/" + process.argv[2] + "/" + version.toString() + "/";
|
||||
await packager.pack_update(package_path, dest_path + "TeaClient-windows-x64.tar.gz");
|
||||
}).then(async () => {
|
||||
await packager.write_info(dest_path + "info.json", "win32", "x64", filename_update, filename_installer)
|
||||
}).then(async () => {
|
||||
await packager.write_version("build/output/version.json", "win32", "x64", process.argv[2], version);
|
||||
}).then(async () => await make_template())
|
||||
.then(async path => await make_installer(path))
|
||||
.then(async () => {
|
||||
console.log("Deploying PDB files");
|
||||
const files = [];
|
||||
for(const file of await fs.readdir(symbol_binary_path)) {
|
||||
if(!file.endsWith(".node"))
|
||||
continue;
|
||||
let file_name = path.basename(file);
|
||||
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
|
||||
});
|
||||
}
|
||||
await deployer.deploy_win_dbg_files(files, version);
|
||||
console.log("PDB files deployed");
|
||||
})
|
||||
.then(async () => {
|
||||
console.log("Deploying build");
|
||||
await packager.deploy("win32", "x64", process.argv[2], version, dest_path + filename_update, dest_path + filename_installer, "exe");
|
||||
}).then(() => {
|
||||
console.log("Succeed");
|
||||
proc.exit(0);
|
||||
}).catch(error => {
|
||||
console.log(error);
|
||||
proc.exit(1);
|
||||
});
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"./deploy/",
|
||||
"build.ts",
|
||||
"package.ts",
|
||||
"package_linux.ts"
|
||||
]
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"./deploy/",
|
||||
"build.ts",
|
||||
"package.ts",
|
||||
"package_windows.ts"
|
||||
]
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd "$(dirname $0)/../"
|
||||
|
||||
project_name="__build_teaclient"
|
||||
source ../scripts/build_helper.sh
|
||||
|
||||
function install_npm() {
|
||||
begin_task "${project_name}_update" "Installing NPM"
|
||||
npm install --save-dev
|
||||
check_err_exit ${project_name} "Failed to install nodejs files!"
|
||||
npm run install-platform
|
||||
check_err_exit ${project_name} "Failed to install platform depend nodejs files!"
|
||||
|
||||
npm update
|
||||
check_err_exit ${project_name} "Failed to update nodejs files!"
|
||||
end_task "${project_name}_update" "NPM installed"
|
||||
}
|
||||
|
||||
function compile_scripts() {
|
||||
begin_task "${project_name}_tsc_sass" "Compiling TypeScript & SASS"
|
||||
./build_declarations.sh
|
||||
check_err_exit ${project_name} "Failed to build shared ui import declarations!"
|
||||
|
||||
npm run compile-tsc -- -p modules/tsconfig_main.json
|
||||
check_err_exit ${project_name} "Failed to compile typescript main files!"
|
||||
npm run compile-tsc -- -p modules/tsconfig_renderer.json
|
||||
check_err_exit ${project_name} "Failed to compile typescript renderer files!"
|
||||
|
||||
if [[ ${build_os_type} == "win32" ]]; then
|
||||
npm run compile-tsc -- -p installer/tsconfig_windows.json
|
||||
check_err_exit ${project_name} "Failed to compile typescript installer files!"
|
||||
else
|
||||
npm run compile-tsc -- -p installer/tsconfig_linux.json
|
||||
check_err_exit ${project_name} "Failed to compile typescript installer files!"
|
||||
fi
|
||||
|
||||
npm run compile-sass
|
||||
check_err_exit ${project_name} "Failed to compile sass files!"
|
||||
end_task "${project_name}_tsc_sass" "TypeScript & SASS compiled"
|
||||
echo ""
|
||||
}
|
||||
|
||||
function compile_native() {
|
||||
begin_task "${project_name}_native" "Compiling native extensions"
|
||||
|
||||
local build_path="native/out/${build_os_type}_${build_os_arch}/"
|
||||
[[ -d ${build_path} ]] && rm -r ${build_path}
|
||||
mkdir -p ${build_path}
|
||||
check_err_exit ${project_name} "Failed to create build directory!"
|
||||
|
||||
cd ${build_path}
|
||||
check_err_exit ${project_name} "Failed to enter build directory!"
|
||||
|
||||
local _arguments=""
|
||||
[[ ! -z "$tearoot_cmake_module" ]] && _arguments="${_arguments} -DCMAKE_MODULE_PATH=\"$tearoot_cmake_module\""
|
||||
[[ ! -z "$tearoot_cmake_config" ]] && _arguments="${_arguments} -DCMAKE_PLATFORM_INCLUDE=\"$tearoot_cmake_config\""
|
||||
[[ ! -z "$traroot_library" ]] && _arguments="${_arguments} -DLIBRARY_PATH=\"$traroot_library\""
|
||||
|
||||
local _generator=""
|
||||
[[ ${build_os_type} == "win32" ]] && _generator='-G"Visual Studio 15 2017 Win64"'
|
||||
|
||||
_command="cmake ../../ ${_generator} -DCMAKE_BUILD_TYPE=RelWithDebInfo ${_arguments}"
|
||||
echo "Executing cmake command $_command"
|
||||
|
||||
eval ${_command}
|
||||
check_err_exit ${project_name} "Failed create build targets!"
|
||||
|
||||
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}
|
||||
check_err_exit ${project_name} "Failed build teaclient connection!"
|
||||
|
||||
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}
|
||||
check_err_exit ${project_name} "Failed build teaclient ppt!"
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
function package_client() {
|
||||
begin_task "${project_name}_package" "Packaging client"
|
||||
if [[ ${build_os_type} == "win32" ]]; then
|
||||
npm run build-windows-64
|
||||
check_err_exit ${project_name} "Failed to package client!"
|
||||
else
|
||||
npm run build-linux-64
|
||||
check_err_exit ${project_name} "Failed to package client!"
|
||||
fi
|
||||
end_task "${project_name}_package" "Client package created"
|
||||
}
|
||||
|
||||
function deploy_client() {
|
||||
begin_task "${project_name}_package" "Deploying client"
|
||||
[[ -z ${teaclient_deploy_secret} ]] && {
|
||||
echo "Missing deploy secret. Dont deploy client!"
|
||||
return 0
|
||||
}
|
||||
[[ -z ${teaclient_deploy_channel} ]] && {
|
||||
echo "Missing deploy channel. Dont deploy client!"
|
||||
return 0
|
||||
}
|
||||
|
||||
if [[ ${build_os_type} == "win32" ]]; then
|
||||
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}
|
||||
check_err_exit ${project_name} "Failed to deploying client!"
|
||||
fi
|
||||
end_task "${project_name}_package" "Client successfully deployed!"
|
||||
}
|
||||
|
||||
#install_npm
|
||||
#compile_scripts
|
||||
#compile_native
|
||||
#package_client
|
||||
deploy_client
|
@ -1,11 +0,0 @@
|
||||
https://git.assembla.com/portaudio.git
|
||||
https://github.com/WolverinDEV/libfvad
|
||||
|
||||
|
||||
git clone https://git.code.sf.net/p/soxr/code soxr
|
||||
mkdir -p out/linux_x64
|
||||
cd out/linux_x674
|
||||
|
||||
cmake ../../ -DWITH_OPENMP=OFF -DBUILD_TESTS=OFF -DBUILD_SHARED_LIBS=OFF -DCMAKE_C_FLAGS="-fPIC"
|
||||
make -j 12
|
||||
sudo make install
|
31
main.ts
31
main.ts
@ -1,31 +0,0 @@
|
||||
import * as electron from "electron";
|
||||
import * as os from "os";
|
||||
|
||||
{
|
||||
const app_path = electron.app.getAppPath();
|
||||
console.log("Native module path: %s", app_path + "/native/build/" + os.platform() + "_" + os.arch() + "/");
|
||||
}
|
||||
|
||||
import * as crash_handler from "./modules/crash_handler";
|
||||
|
||||
const is_electron_run = process.argv[0].endsWith("electron") || process.argv[0].endsWith("electron.exe");
|
||||
const process_arguments = is_electron_run ? process.argv.slice(2) : process.argv.slice(1);
|
||||
if(process_arguments.length > 0 && process_arguments[0] === "crash-handler") {
|
||||
/* crash handler callback */
|
||||
crash_handler.handle_crash_callback(process_arguments.slice(1));
|
||||
} else {
|
||||
if(process_arguments.length > 0 && process_arguments[0] == "--main-crash-handler")
|
||||
crash_handler.initialize_handler("main", is_electron_run);
|
||||
/* app execute */
|
||||
{
|
||||
const versions = process.versions;
|
||||
console.log("Versions:");
|
||||
console.log(" TeaSpeak Client: " + electron.app.getVersion());
|
||||
|
||||
for (const key of Object.keys(versions))
|
||||
console.log(" %s: %s", key, versions[key]);
|
||||
}
|
||||
|
||||
const tea_client = require("./modules/core/main.js");
|
||||
tea_client.execute();
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
import {BrowserWindow} from "electron";
|
||||
import * as electron from "electron";
|
||||
import * as path from "path";
|
||||
|
||||
let changelog_window: BrowserWindow;
|
||||
export function open() {
|
||||
if(changelog_window) {
|
||||
changelog_window.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
changelog_window = new BrowserWindow({
|
||||
show: false
|
||||
});
|
||||
|
||||
changelog_window.setMenu(null);
|
||||
|
||||
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 */
|
||||
}
|
||||
|
||||
changelog_window.loadFile(file);
|
||||
changelog_window.setTitle("TeaClient ChangeLog");
|
||||
changelog_window.on('ready-to-show', () => {
|
||||
changelog_window.show();
|
||||
});
|
||||
changelog_window.on('close', () => {
|
||||
changelog_window = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
export function close() {
|
||||
if(changelog_window) {
|
||||
changelog_window.close();
|
||||
changelog_window = undefined;
|
||||
}
|
||||
}
|
@ -1,863 +0,0 @@
|
||||
import * as querystring from "querystring";
|
||||
import * as request from "request";
|
||||
import {app, dialog, ipcMain} from "electron";
|
||||
import * as fs from "fs-extra";
|
||||
import * as ofs from "original-fs";
|
||||
import * as os from "os";
|
||||
import * as tar from "tar-stream";
|
||||
import * as path from "path";
|
||||
import * as zlib from "zlib";
|
||||
import * as child_process from "child_process";
|
||||
import * as progress from "request-progress";
|
||||
import * as util from "util";
|
||||
|
||||
import {parse_version, Version} from "../../shared/version";
|
||||
|
||||
import Timer = NodeJS.Timer;
|
||||
import MessageBoxOptions = Electron.MessageBoxOptions;
|
||||
import {Headers} from "tar-stream";
|
||||
import {Arguments, process_args} from "../../shared/process-arguments";
|
||||
import * as electron from "electron";
|
||||
import {PassThrough} from "stream";
|
||||
import * as _main_windows from "../main_window";
|
||||
import ErrnoException = NodeJS.ErrnoException;
|
||||
import {EPERM} from "constants";
|
||||
import * as winmgr from "../window";
|
||||
|
||||
const is_debug = false;
|
||||
export function server_url() : string {
|
||||
const default_path = is_debug ? "http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/client-api/environment/" : "http://clientapi.teaspeak.de/";
|
||||
return process_args.has_value(...Arguments.SERVER_URL) ? process_args.value(...Arguments.SERVER_URL) : default_path;
|
||||
}
|
||||
|
||||
export interface UpdateVersion {
|
||||
channel: string;
|
||||
platform: string,
|
||||
arch: string;
|
||||
version: Version;
|
||||
}
|
||||
|
||||
export interface UpdateData {
|
||||
versions: UpdateVersion[];
|
||||
updater_version: UpdateVersion;
|
||||
}
|
||||
|
||||
let version_cache: UpdateData = undefined;
|
||||
export async function load_data(allow_cached: boolean = true) : Promise<UpdateData> {
|
||||
if(version_cache && allow_cached) return Promise.resolve(version_cache);
|
||||
|
||||
return new Promise<UpdateData>((resolve, reject) => {
|
||||
const request_url = server_url() + "/api.php?" + querystring.stringify({
|
||||
type: "update-info"
|
||||
});
|
||||
console.log("request: %s", request_url);
|
||||
request.get(request_url, {
|
||||
timeout: 2000
|
||||
}, (error, response, body) => {
|
||||
if(!response || response.statusCode != 200) {
|
||||
let info;
|
||||
try {
|
||||
info = JSON.parse(body) || {msg: error};
|
||||
} catch(e) {
|
||||
info = {msg: "!-- failed to parse json --!"};
|
||||
}
|
||||
setImmediate(reject, "Invalid status code (" + (response || {statusCode: -1}).statusCode + " | " + (info || {msg: "undefined"}).msg + ")");
|
||||
return;
|
||||
}
|
||||
const data = JSON.parse(body);
|
||||
if(!data) {
|
||||
setImmediate(reject, "Invalid response");
|
||||
return;
|
||||
}
|
||||
if(!data["success"]) {
|
||||
setImmediate(reject, "Action failed (" + data["msg"] + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
let resp: UpdateData = {} as any;
|
||||
resp.versions = [];
|
||||
for(const channel of Object.keys(data)) {
|
||||
if(channel == "success") continue;
|
||||
|
||||
for(const entry of data[channel]) {
|
||||
let version: UpdateVersion = {} as any;
|
||||
version.channel = channel;
|
||||
version.arch = entry["arch"];
|
||||
version.platform = entry["platform"];
|
||||
version.version = new Version(entry["version"]["major"], entry["version"]["minor"], entry["version"]["patch"], entry["version"]["build"], entry["version"]["timestamp"]);
|
||||
if(version.channel == 'updater')
|
||||
resp.updater_version = version;
|
||||
else
|
||||
resp.versions.push(version);
|
||||
}
|
||||
}
|
||||
resolve(resp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function newest_version(current_version: Version, channel?: string) : Promise<UpdateVersion | undefined> {
|
||||
if(!app.getAppPath().endsWith(".asar")) {
|
||||
throw "You cant run an update when you're executing the source code!";
|
||||
}
|
||||
const data = await load_data();
|
||||
let had_data = false;
|
||||
for(const version of data.versions) {
|
||||
if(version.arch == os.arch() && version.platform == os.platform()) {
|
||||
if(!channel || version.channel == channel) {
|
||||
if(!current_version || version.version.newer_than(current_version))
|
||||
return version;
|
||||
else
|
||||
had_data = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!had_data)
|
||||
throw "Missing data";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function extract_updater(update_file: string) {
|
||||
if(!fs.existsSync(update_file)) throw "Missing update file!";
|
||||
|
||||
|
||||
let parent_path = app.getAppPath();
|
||||
if(parent_path.endsWith(".asar")) {
|
||||
parent_path = path.join(parent_path, "..", "..");
|
||||
parent_path = fs.realpathSync(parent_path);
|
||||
}
|
||||
|
||||
let post_path;
|
||||
if(os.platform() == "linux")
|
||||
post_path = parent_path + "/update-installer";
|
||||
else
|
||||
post_path = parent_path + "/update-installer.exe";
|
||||
|
||||
const source = fs.createReadStream(update_file);
|
||||
const extract = tar.extract();
|
||||
await new Promise(resolve => {
|
||||
let updater_found = false;
|
||||
source.on('end', () => {
|
||||
if(!updater_found) {
|
||||
console.error("Failed to extract the updater (Updater hasn't been found!)");
|
||||
resolve(); //FIXME use reject!
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
extract.on('entry', (header: Headers, stream, callback) => {
|
||||
stream.on('end', callback);
|
||||
console.log("Got entry " + header.name);
|
||||
|
||||
if(header.name == "./update-installer" || header.name == "./update-installer.exe") {
|
||||
console.log("Found updater! (" + header.size + ")");
|
||||
console.log("Extracting to %s", post_path);
|
||||
const s = fs.createWriteStream(post_path);
|
||||
stream.pipe(s).on('finish', event => {
|
||||
console.log("Updater extracted and written!");
|
||||
updater_found = true;
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
stream.resume(); //Drain the stream
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
source.pipe(extract);
|
||||
});
|
||||
}
|
||||
|
||||
export async function update_updater() : Promise<void> {
|
||||
//TODO here
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function data_directory() : string {
|
||||
return electron.app.getPath('userData');
|
||||
}
|
||||
|
||||
function get_update_file(channel: string, version: Version) : string {
|
||||
let _path = fs.realpathSync(data_directory());
|
||||
|
||||
const name = channel + "_" + version.major + "_" + version.minor + "_" + version.patch + "_" + version.build + ".tar";
|
||||
return path.join(_path, "app_versions", name);
|
||||
}
|
||||
|
||||
export interface ProgressState {
|
||||
percent: number, // Overall percent (between 0 to 1)
|
||||
speed: number, // The download speed in bytes/sec
|
||||
size: {
|
||||
total: number, // The total payload size in bytes
|
||||
transferred: number// The transferred payload size in bytes
|
||||
},
|
||||
time: {
|
||||
elapsed: number,// The total elapsed seconds since the start (3 decimals)
|
||||
remaining: number // The remaining seconds to finish (3 decimals)
|
||||
}
|
||||
}
|
||||
|
||||
export async function download_version(channel: string, version: Version, status?: (state: ProgressState) => any) : Promise<string> {
|
||||
const target_path = get_update_file(channel, version);
|
||||
console.log("Downloading version %s to %s", version.toString(false), target_path);
|
||||
if(fs.existsSync(target_path)) {
|
||||
/* TODO test if this file is valid and can be used */
|
||||
try {
|
||||
await fs.remove(target_path);
|
||||
} catch(error) {
|
||||
throw "Failed to remove old file: " + error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.mkdirp(path.dirname(target_path));
|
||||
} catch(error) {
|
||||
throw "Failed to make target directory: " + path.dirname(target_path);
|
||||
}
|
||||
|
||||
const url = server_url() + "/api.php?" + querystring.stringify({
|
||||
type: "update-download",
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
version: version.toString(),
|
||||
channel: channel
|
||||
});
|
||||
console.log("Downloading update from %s. (%s)", server_url(), url);
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let fired = false;
|
||||
let stream = progress(request.get(url, {
|
||||
timeout: 2000
|
||||
}, (error, response, body) => {
|
||||
if(!response || response.statusCode != 200) {
|
||||
let info;
|
||||
try {
|
||||
info = JSON.parse(body)
|
||||
} catch(e) {
|
||||
info = {"msg": "!-- failed to parse json --!"};
|
||||
}
|
||||
if(!fired && (fired = true))
|
||||
setImmediate(reject, "Invalid status code (" + (response || {statusCode: -1}).statusCode + "|" + (info || {"msg": "undefined"}).msg + ")");
|
||||
return;
|
||||
}
|
||||
})).on('progress', _state => status ? status(_state) : {}).on('error', error => {
|
||||
console.warn("Encountered error within download pipe. Ignoring error: %o", error);
|
||||
}).on('end', function () {
|
||||
console.log("Update downloaded successfully. Waiting for write stream to finish.");
|
||||
if(status)
|
||||
status({
|
||||
percent: 1,
|
||||
speed: 0,
|
||||
size: { total: 0, transferred: 0},
|
||||
time: { elapsed: 0, remaining: 0}
|
||||
})
|
||||
});
|
||||
|
||||
console.log("Decompressing update package while streaming!");
|
||||
stream = stream.pipe(zlib.createGunzip());
|
||||
stream.pipe(fs.createWriteStream(target_path, {
|
||||
autoClose: true
|
||||
})).on('finish', () => {
|
||||
console.log("Write stream has finished. Download successfully.");
|
||||
if(!fired && (fired = true))
|
||||
setImmediate(resolve, target_path);
|
||||
}).on('error', error => {
|
||||
console.log("Write stream encountered an error while downloading update. Error: %o", error);
|
||||
if(!fired && (fired = true))
|
||||
setImmediate(reject,"failed to write");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if(typeof(String.prototype.trim) === "undefined")
|
||||
{
|
||||
String.prototype.trim = function()
|
||||
{
|
||||
return String(this).replace(/^\s+|\s+$/g, '');
|
||||
};
|
||||
}
|
||||
|
||||
export async function test_file_accessibility(update_file: string) : Promise<string[]> {
|
||||
if(os.platform() === "win32")
|
||||
return []; /* within windows the update installer request admin privileges if required */
|
||||
|
||||
const original_fs = require('original-fs');
|
||||
if(!fs.existsSync(update_file))
|
||||
throw "Missing update file (" + update_file + ")";
|
||||
|
||||
let parent_path = app.getAppPath();
|
||||
if(parent_path.endsWith(".asar")) {
|
||||
parent_path = path.join(parent_path, "..", "..");
|
||||
parent_path = fs.realpathSync(parent_path);
|
||||
}
|
||||
|
||||
const test_access = async (file: string, mode: number) => {
|
||||
return await new Promise<NodeJS.ErrnoException>(resolve => original_fs.access(file, mode, resolve));
|
||||
};
|
||||
|
||||
let code = await test_access(update_file, original_fs.constants.R_OK);
|
||||
if(code)
|
||||
throw "Failed test read for update file. (" + update_file + " results in " + code.code + ")";
|
||||
|
||||
const fstream = original_fs.createReadStream(update_file);
|
||||
const tar_stream = tar.extract();
|
||||
|
||||
const errors: string[] = [];
|
||||
const tester = async (header: Headers) => {
|
||||
const entry_path = path.normalize(path.join(parent_path, header.name));
|
||||
if(header.type == "file") {
|
||||
if(original_fs.existsSync(entry_path)) {
|
||||
code = await test_access(entry_path, original_fs.constants.W_OK);
|
||||
if(code)
|
||||
errors.push("Failed to acquire write permissions for file " + entry_path + " (Code " + code.code + ")");
|
||||
} else {
|
||||
let directory = path.dirname(entry_path);
|
||||
while(directory.length != 0 && !original_fs.existsSync(directory))
|
||||
directory = path.normalize(path.join(directory, ".."));
|
||||
|
||||
code = await test_access(directory, original_fs.constants.W_OK);
|
||||
if(code) errors.push("Failed to acquire write permissions for directory " + entry_path + " (Code " + code.code + ". Target directory " + directory + ")");
|
||||
}
|
||||
} else if(header.type == "directory") {
|
||||
let directory = path.dirname(entry_path);
|
||||
while(directory.length != 0 && !original_fs.existsSync(directory))
|
||||
directory = path.normalize(path.join(directory, ".."));
|
||||
|
||||
code = await test_access(directory, original_fs.constants.W_OK);
|
||||
if(code) errors.push("Failed to acquire write permissions for directory " + entry_path + " (Code " + code.code + ". Target directory " + directory + ")");
|
||||
}
|
||||
};
|
||||
|
||||
tar_stream.on('entry', (header: Headers, stream, next) => {
|
||||
tester(header).catch(error => {
|
||||
console.log("Emit out of tar_stream.on('entry' ...)");
|
||||
tar_stream.emit('error', error);
|
||||
}).then(() => {
|
||||
stream.on('end', next);
|
||||
stream.resume();
|
||||
});
|
||||
});
|
||||
|
||||
fstream.pipe(tar_stream);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
tar_stream.on('finish', resolve);
|
||||
tar_stream.on('error', error => { reject(error); });
|
||||
});
|
||||
} catch(error) {
|
||||
throw "Failed to list files within tar: " + error;
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
namespace install_config {
|
||||
export interface LockFile {
|
||||
filename: string;
|
||||
timeout: number;
|
||||
"error-id": string;
|
||||
}
|
||||
export interface MoveFile {
|
||||
source: string;
|
||||
target: string;
|
||||
"error-id": string;
|
||||
}
|
||||
export interface ConfigFile {
|
||||
version: number;
|
||||
|
||||
backup: boolean;
|
||||
"backup-directory": string;
|
||||
|
||||
"callback_file": string;
|
||||
"callback_argument_fail": string;
|
||||
"callback_argument_success": string;
|
||||
|
||||
moves: MoveFile[];
|
||||
locks: LockFile[];
|
||||
}
|
||||
}
|
||||
|
||||
async function build_install_config(source_root: string, target_root: string) : Promise<install_config.ConfigFile> {
|
||||
console.log("Building update install config for target directory: %s. Update source: %o", target_root, source_root);
|
||||
const result: install_config.ConfigFile = { } as any;
|
||||
|
||||
result.version = 1;
|
||||
|
||||
result.backup = true;
|
||||
|
||||
{
|
||||
const data = path.parse(source_root);
|
||||
result["backup-directory"] = path.join(data.dir, data.name + "_backup");
|
||||
}
|
||||
|
||||
result.callback_file = app.getPath("exe");
|
||||
result.callback_argument_fail = "--no-single-instance --update-failed-new=";
|
||||
result.callback_argument_success = "--no-single-instance --update-succeed-new=";
|
||||
|
||||
result.moves = [];
|
||||
result.locks = [
|
||||
{
|
||||
"error-id": "main-exe-lock",
|
||||
filename: app.getPath("exe"),
|
||||
timeout: 5000
|
||||
}
|
||||
];
|
||||
|
||||
const ignore_list = [
|
||||
"update-installer.exe", "update-installer"
|
||||
];
|
||||
|
||||
const dir_walker = async (relative_path: string) => {
|
||||
const source_directory = path.join(source_root, relative_path);
|
||||
const target_directory = path.join(target_root, relative_path);
|
||||
|
||||
let files: string[];
|
||||
try {
|
||||
files = await util.promisify(ofs.readdir)(source_directory);
|
||||
} catch(error) {
|
||||
console.warn("Failed to iterate over source directory \"%s\": %o", source_directory, error);
|
||||
return;
|
||||
}
|
||||
|
||||
for(const file of files) {
|
||||
let _exclude = false;
|
||||
for(const exclude of ignore_list) {
|
||||
if(exclude == file) {
|
||||
console.debug("Ignoring file to update (%s/%s)", relative_path, file);
|
||||
_exclude = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(_exclude)
|
||||
continue;
|
||||
|
||||
const source_file = path.join(source_directory, file);
|
||||
const target_file = path.join(target_directory, file);
|
||||
|
||||
//TODO check if file content has changed else ignore?
|
||||
|
||||
const info = await util.promisify(ofs.stat)(source_file);
|
||||
if(info.isDirectory()) {
|
||||
await dir_walker(path.join(relative_path, file));
|
||||
} else {
|
||||
/* TODO: ensure its a file! */
|
||||
result.moves.push({
|
||||
"error-id": "move-file-" + result.moves.length,
|
||||
source: source_file,
|
||||
target: target_file
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await dir_walker(".");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function execute_update(update_file: string, restart_callback: (callback: () => void) => any) : Promise<void> {
|
||||
let application_path = app.getAppPath();
|
||||
if(application_path.endsWith(".asar")) {
|
||||
console.log("App path points to ASAR file (Going up to root directory)");
|
||||
application_path = await fs.realpath(path.join(application_path, "..", ".."));
|
||||
} else if(await fs.pathExists(application_path) && (await fs.stat(application_path)).isFile())
|
||||
application_path = path.dirname(application_path);
|
||||
|
||||
console.log("Located target app path: %s", application_path);
|
||||
console.log("Using update file: %s", update_file);
|
||||
|
||||
const temp_directory = path.join(app.getPath("temp"), "teaclient_update_" + Math.random().toString(36).substring(7));
|
||||
{
|
||||
console.log("Preparing update source directory at %s", temp_directory);
|
||||
try {
|
||||
await fs.mkdirp(temp_directory)
|
||||
} catch(error) {
|
||||
console.error("failed to create update source directory: %o", error);
|
||||
throw "failed to create update source directory";
|
||||
}
|
||||
|
||||
const source = fs.createReadStream(update_file);
|
||||
const extract = tar.extract();
|
||||
|
||||
extract.on('entry', (header: Headers, stream: PassThrough, callback) => {
|
||||
const extract = async (header: Headers, stream: PassThrough) => {
|
||||
const target_file = path.join(temp_directory, header.name);
|
||||
console.debug("Extracting entry %s of type %s to %s", header.name, header.type, target_file);
|
||||
|
||||
if(header.type == "directory") {
|
||||
await fs.mkdirp(target_file);
|
||||
} else if(header.type == "file") {
|
||||
{
|
||||
const directory = path.parse(target_file).dir;
|
||||
console.debug("Testing for directory: %s", directory);
|
||||
if(!(await util.promisify(ofs.exists)(directory)) || !(await util.promisify(ofs.stat)(directory)).isDirectory()) {
|
||||
console.log("Creating directory %s", directory);
|
||||
try {
|
||||
await fs.mkdirp(directory);
|
||||
} catch(error) {
|
||||
console.warn("failed to create directory for file %s", header.type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
const write_stream = ofs.createWriteStream(target_file);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
stream.pipe(write_stream)
|
||||
.on('error', reject)
|
||||
.on('finish', resolve);
|
||||
});
|
||||
return; /* success */
|
||||
} catch(error) {
|
||||
console.error("Failed to extract update file %s: %o", header.name, error);
|
||||
}
|
||||
} else {
|
||||
console.debug("Skipping this unknown file type");
|
||||
}
|
||||
stream.resume(); /* drain the stream */
|
||||
};
|
||||
extract(header, stream).catch(error => {
|
||||
console.log("Ignoring file %s due to an error: %o", header.name, error);
|
||||
}).then(() => {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
source.pipe(extract);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
extract.on('finish', resolve);
|
||||
extract.on('error', reject);
|
||||
});
|
||||
} catch(error) {
|
||||
console.error("Failed to unpack update: %o", error);
|
||||
throw "update unpacking failed";
|
||||
}
|
||||
}
|
||||
/* the "new" environment should now be available at 'temp_directory' */
|
||||
console.log("Update unpacked successfully. Building update extractor file.");
|
||||
|
||||
let install_config;
|
||||
try {
|
||||
install_config = await build_install_config(temp_directory, application_path);
|
||||
} catch(error) {
|
||||
console.error("Failed to build update installer config: %o", error);
|
||||
throw "failed to build update installer config";
|
||||
}
|
||||
|
||||
const log_file = path.join(temp_directory, "update-log.txt");
|
||||
const config_file = path.join(temp_directory, "update_install.json");
|
||||
console.log("Writing config to %s", config_file);
|
||||
try {
|
||||
await fs.writeJSON(config_file, install_config);
|
||||
} catch(error) {
|
||||
console.error("Failed to write update install config file: %s", error);
|
||||
throw "failed to write update install config file";
|
||||
}
|
||||
|
||||
const update_installer = path.join(application_path, "update-installer" + (os.platform() === "win32" ? ".exe" : ""));
|
||||
if(!(await fs.pathExists(update_installer))) {
|
||||
console.error("Missing update installer! Supposed to be at %s", update_installer);
|
||||
throw "Missing update installer!";
|
||||
} else {
|
||||
console.log("Using update installer located at %s", update_installer);
|
||||
}
|
||||
|
||||
if(os.platform() == "linux") {
|
||||
console.log("Executing update install on linux");
|
||||
|
||||
//We have to unpack it later
|
||||
const rest_callback = () => {
|
||||
console.log("Executing command %s with args %o", update_installer, [log_file, config_file]);
|
||||
try {
|
||||
let result = child_process.spawnSync(update_installer, [log_file, config_file]);
|
||||
if(result.status != 0) {
|
||||
console.error("Failed to execute update installer! Return code: %d", result.status);
|
||||
dialog.showMessageBox({
|
||||
buttons: ["update now", "remind me later"],
|
||||
title: "Update available",
|
||||
message:
|
||||
"Failed to execute update installer\n" +
|
||||
"Installer exited with code " + result.status
|
||||
} as MessageBoxOptions);
|
||||
}
|
||||
} catch(error) {
|
||||
console.error("Failed to execute update installer (%o)", error);
|
||||
if("errno" in error) {
|
||||
const errno = error as ErrnoException;
|
||||
if(errno.errno == EPERM) {
|
||||
dialog.showMessageBox({
|
||||
buttons: ["quit"],
|
||||
title: "Update execute failed",
|
||||
message: "Failed to execute update installer. (No permissions)\nPlease execute the client with admin privileges!"
|
||||
} as MessageBoxOptions);
|
||||
return;
|
||||
}
|
||||
dialog.showMessageBox({
|
||||
buttons: ["quit"],
|
||||
title: "Update execute failed",
|
||||
message: "Failed to execute update installer.\nError: " + errno.message
|
||||
} as MessageBoxOptions);
|
||||
return;
|
||||
}
|
||||
dialog.showMessageBox({
|
||||
buttons: ["quit"],
|
||||
title: "Update execute failed",
|
||||
message: "Failed to execute update installer.\nLookup console for more detail"
|
||||
} as MessageBoxOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
if(electron.app.hasSingleInstanceLock())
|
||||
electron.app.releaseSingleInstanceLock();
|
||||
|
||||
const ids = child_process.execSync("pgrep TeaClient").toString().split(os.EOL).map(e => e.trim()).reverse().join(" ");
|
||||
console.log("Executing %s", "kill -9 " + ids);
|
||||
child_process.execSync("kill -9 " + ids);
|
||||
};
|
||||
restart_callback(rest_callback);
|
||||
} else {
|
||||
console.log("Executing update install on windows");
|
||||
|
||||
//We have to unpack it later
|
||||
const rest_callback = () => {
|
||||
let pipe = child_process.spawn(update_installer, [log_file, config_file], {
|
||||
detached: true,
|
||||
cwd: application_path,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
pipe.unref();
|
||||
app.quit();
|
||||
};
|
||||
restart_callback(rest_callback);
|
||||
}
|
||||
}
|
||||
|
||||
export async function current_version() : Promise<Version> {
|
||||
if(process_args.has_value(Arguments.UPDATER_LOCAL_VERSION))
|
||||
return parse_version(process_args.value(Arguments.UPDATER_LOCAL_VERSION));
|
||||
|
||||
let parent_path = app.getAppPath();
|
||||
if(parent_path.endsWith(".asar")) {
|
||||
parent_path = path.join(parent_path, "..", "..");
|
||||
parent_path = fs.realpathSync(parent_path);
|
||||
}
|
||||
try {
|
||||
const info = await fs.readJson(path.join(parent_path, "app_version.json"));
|
||||
let result = parse_version(info["version"]);
|
||||
result.timestamp = info["timestamp"];
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.log("Got no version!");
|
||||
return new Version(0, 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
async function minawait<T>(object: Promise<T>, time: number) : Promise<T> {
|
||||
const begin = Date.now();
|
||||
const r = await object;
|
||||
const end = Date.now();
|
||||
if(end - begin < time)
|
||||
await new Promise(resolve => setTimeout(resolve, time + begin - end));
|
||||
return r;
|
||||
}
|
||||
|
||||
export let update_restart_pending = false;
|
||||
export async function execute_graphical(channel: string, ask_install: boolean) : Promise<Boolean> {
|
||||
const electron = require('electron');
|
||||
|
||||
const ui_debug = process_args.has_flag(Arguments.UPDATER_UI_DEBUG);
|
||||
const window = new electron.BrowserWindow({
|
||||
show: false,
|
||||
width: ui_debug ? 1200 : 600,
|
||||
height: ui_debug ? 800 : 400,
|
||||
|
||||
webPreferences: {
|
||||
devTools: true,
|
||||
nodeIntegration: true,
|
||||
javascript: true
|
||||
}
|
||||
});
|
||||
|
||||
window.loadFile(path.join(path.dirname(module.filename), "ui", "index.html"));
|
||||
if(ui_debug) {
|
||||
window.webContents.openDevTools();
|
||||
}
|
||||
await new Promise(resolve => window.on('ready-to-show', resolve));
|
||||
window.show();
|
||||
await winmgr.apply_bounds('update-installer', window);
|
||||
winmgr.track_bounds('update-installer', window);
|
||||
|
||||
const current_vers = await current_version();
|
||||
console.log("Current version: " + current_vers.toString(true));
|
||||
|
||||
console.log("Showed");
|
||||
const set_text = text => window.webContents.send('status-update-text', text);
|
||||
const set_error = text => window.webContents.send('status-error', text);
|
||||
const set_progress = progress => window.webContents.send('status-update', progress);
|
||||
const await_exit = () => { return new Promise(resolve => window.on('closed', resolve))};
|
||||
const await_version_confirm = version => {
|
||||
const id = "version-accept-" + Date.now();
|
||||
window.webContents.send('status-confirm-update', id, current_vers, version);
|
||||
return new Promise((resolve, reject) => {
|
||||
window.on('closed', () => resolve(false));
|
||||
ipcMain.once(id, (event, result) => {
|
||||
console.log("Got response %o", result);
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
};
|
||||
const await_confirm_execute = () => {
|
||||
const id = "status-confirm-execute-" + Date.now();
|
||||
window.webContents.send('status-confirm-execute', id);
|
||||
return new Promise((resolve, reject) => {
|
||||
window.on('closed', () => resolve(false));
|
||||
ipcMain.once(id, (event, result) => {
|
||||
console.log("Got response %o", result);
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
set_text("Loading data");
|
||||
let version: UpdateVersion;
|
||||
try {
|
||||
version = await minawait(newest_version(process_args.has_flag(Arguments.UPDATER_ENFORCE) ? undefined : current_vers, channel), 3000);
|
||||
} catch (error) {
|
||||
set_error("Failed to get newest information:<br>" + error);
|
||||
await await_exit();
|
||||
return false;
|
||||
}
|
||||
console.log("Got version %o", version);
|
||||
|
||||
if(!version) {
|
||||
set_error("You're already on the newest version!");
|
||||
await await_exit();
|
||||
return false;
|
||||
}
|
||||
|
||||
if(ask_install) {
|
||||
try {
|
||||
const test = await await_version_confirm(version.version);
|
||||
if(!test) {
|
||||
window.close();
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.dir(error);
|
||||
window.close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
set_text("Updating to version " + version.version.toString() + "<br>Downloading....");
|
||||
let update_path: string;
|
||||
try {
|
||||
update_path = await download_version(version.channel, version.version, status => { setImmediate(set_progress, status.percent); });
|
||||
} catch (error) {
|
||||
set_error("Failed to download version: <br>" + error);
|
||||
console.error(error);
|
||||
await await_exit();
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const inaccessible = await test_file_accessibility(update_path);
|
||||
if(inaccessible.length > 0) {
|
||||
console.log("Failed to access the following files:");
|
||||
for(const fail of inaccessible)
|
||||
console.log(" - " + fail);
|
||||
|
||||
if(os.platform() == "linux") {
|
||||
set_error("Failed to access target files.<br>Please execute this app with administrator (sudo) privileges.<br>Use the following command:<br><p>" +
|
||||
"sudo " + path.normalize(app.getAppPath()) + " --update-execute=\"" + path.normalize(update_path) + "\"</p>");
|
||||
await await_exit();
|
||||
return false;
|
||||
} else if(os.platform() == "win32") {
|
||||
/* the updater asks for admin rights anyway :/ */
|
||||
}
|
||||
}
|
||||
} catch(error) {
|
||||
set_error("Failed to access target files.<br>You may need to execute the TeaClient as Administrator!<br>Error: " + error);
|
||||
await await_exit();
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!await await_confirm_execute()) {
|
||||
window.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
set_text("Extracting update installer...<br>Please wait");
|
||||
try {
|
||||
await extract_updater(update_path);
|
||||
} catch(error) {
|
||||
console.error("Failed to update the updater! (%o)", error);
|
||||
set_error("Failed to update the update installer.\nUpdate failed!");
|
||||
await await_exit();
|
||||
return false;
|
||||
}
|
||||
set_text("Executing update...<br>Please wait");
|
||||
|
||||
try {
|
||||
await execute_update(update_path, callback => {
|
||||
_main_windows.set_prevent_instant_close(true);
|
||||
update_restart_pending = true;
|
||||
window.close();
|
||||
callback();
|
||||
});
|
||||
} catch (error) {
|
||||
dialog.showErrorBox("Update error", "Failed to execute update!\n" + error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export let update_question_open = false;
|
||||
async function check_update(channel: string) {
|
||||
let version: UpdateVersion;
|
||||
try {
|
||||
version = await newest_version(await current_version(), channel);
|
||||
} catch(error) {
|
||||
console.warn("failed check for newer versions!");
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
if(version) {
|
||||
update_question_open = true;
|
||||
dialog.showMessageBox({
|
||||
buttons: ["update now", "remind me later"],
|
||||
title: "TeaClient: Update available",
|
||||
message:
|
||||
"There is an update available!\n" +
|
||||
"Should we update now?\n" +
|
||||
"\n" +
|
||||
"Current version: " + (await current_version()).toString() + "\n" +
|
||||
"Target version: " + version.version.toString()
|
||||
} as MessageBoxOptions).then(result => {
|
||||
if(result.response == 0) {
|
||||
execute_graphical(channel, false).then(() => {
|
||||
update_question_open = false;
|
||||
});
|
||||
} else {
|
||||
update_question_open = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let update_task: Timer;
|
||||
export function start_auto_update_check() {
|
||||
if(update_task) return;
|
||||
update_task = setInterval(check_update, 2 * 60 * 60 * 1000);
|
||||
setImmediate(check_update);
|
||||
}
|
||||
|
||||
export function stop_auto_update_check() {
|
||||
clearInterval(update_task);
|
||||
update_task = undefined;
|
||||
}
|
||||
|
||||
export async function selected_channel() : Promise<string> {
|
||||
return process_args.has_value(Arguments.UPDATER_CHANNEL) ? process_args.value(Arguments.UPDATER_CHANNEL) : "release";
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
.page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=index.css.map */
|
@ -1 +0,0 @@
|
||||
{"version":3,"sourceRoot":"","sources":["index.scss"],"names":[],"mappings":"AAAA;EACC;;;AAGD;EACC","file":"index.css"}
|
@ -1,36 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Updating app</title>
|
||||
|
||||
<script type="application/javascript">
|
||||
const exports = {};
|
||||
</script>
|
||||
<script src="index.js" type="application/javascript"></script>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="page info">
|
||||
<div class="state">Downloading update. Please wait!</div>
|
||||
<progress class="progress" value="0" max="100"></progress>
|
||||
</div>
|
||||
<div class="page error">
|
||||
<div class="message"></div>
|
||||
</div>
|
||||
<div class="page confirm-restart">
|
||||
Download succeeded.<br>
|
||||
Update installation requires client restart!<br>
|
||||
Please ensure that <b>all instances</b> are closed!<br>
|
||||
If not this app would kill them!<br>
|
||||
<button class="button-execute">Execute update</button>
|
||||
</div>
|
||||
<div class="page config-update">
|
||||
Update available.<br>
|
||||
Current version: <a class="current-version"></a><br>
|
||||
Target version: <a class="target-version"></a><br>
|
||||
<button class="button-update">update</button>
|
||||
<button class="button-cancel">cancel</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,7 +0,0 @@
|
||||
.page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: block;
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import * as electron from "electron";
|
||||
import * as $ from "jquery";
|
||||
import {Version} from "../../../shared/version";
|
||||
|
||||
electron.ipcRenderer.on('status-update', (event, progress) => {
|
||||
if(!$(".info").is(":visible")) {
|
||||
$(".page").hide();
|
||||
$(".info").show()
|
||||
}
|
||||
$(".progress").attr("value", progress * 100);
|
||||
});
|
||||
|
||||
electron.ipcRenderer.on('status-update-text', (event, text) => {
|
||||
if(!$(".info").is(":visible")) {
|
||||
$(".page").hide();
|
||||
$(".info").show()
|
||||
}
|
||||
$(".state").html(text);
|
||||
});
|
||||
|
||||
electron.ipcRenderer.on('status-error', (event, text) => {
|
||||
console.log("Got error %s", text);
|
||||
$(".page").hide();
|
||||
$(".error").show().html(text);
|
||||
});
|
||||
electron.ipcRenderer.on('status-confirm-execute', (event, callback_name) => {
|
||||
$(".page").hide();
|
||||
$(".confirm-restart").show();
|
||||
$(".button-execute").on('click', event => electron.ipcRenderer.send(callback_name, true))
|
||||
});
|
||||
electron.ipcRenderer.on('status-confirm-update', (event, callback_name, current: Version, version: Version) => {
|
||||
console.dir(callback_name);
|
||||
console.dir(version);
|
||||
|
||||
$(".page").hide();
|
||||
$(".config-update").show();
|
||||
|
||||
$(".target-version").text(version.major + "." + version.minor + "." + version.patch + (version.build > 0 ? " (" + version.build + ")" : ""));
|
||||
$(".current-version").text(current.major + "." + current.minor + "." + current.patch + (current.build > 0 ? " (" + current.build + ")" : ""));
|
||||
|
||||
$(".button-update").on('click', event => electron.ipcRenderer.send(callback_name, true));
|
||||
$(".button-cancel").on('click', event => electron.ipcRenderer.send(callback_name, false));
|
||||
});
|
||||
|
||||
/*
|
||||
const set_text = text => window.webContents.send('status-update-text', text);
|
||||
const set_error = text => window.webContents.send('status-error', text);
|
||||
const set_confirm_restart = () => window.webContents.send('status-confirm-restart');
|
||||
const set_progress = progress => window.webContents.send('status-update', progress);
|
||||
const await_exit = () => { return new Promise(resolve => window.on('closed', resolve))};
|
||||
*/
|
@ -1,209 +0,0 @@
|
||||
// Quit when all windows are closed.
|
||||
import * as electron from "electron";
|
||||
import * as app_updater from "./app-updater";
|
||||
|
||||
import { app } from "electron";
|
||||
import MessageBoxOptions = electron.MessageBoxOptions;
|
||||
|
||||
import {process_args, parse_arguments, Arguments} from "../shared/process-arguments";
|
||||
import {open as open_changelog} from "./app-updater/changelog";
|
||||
import * as crash_handler from "../crash_handler";
|
||||
import {open_preview} from "./url-preview";
|
||||
|
||||
async function execute_app() {
|
||||
/* legacy, will be removed soon */
|
||||
if(process_args.has_value("update-failed")) {
|
||||
const result = await electron.dialog.showMessageBox({
|
||||
type: "error",
|
||||
message: "Failed to execute update:\n" + process_args.value("update-failed"),
|
||||
title: "Update failed!",
|
||||
buttons: ["retry", "ignore"]
|
||||
} as MessageBoxOptions);
|
||||
if(result.response == 0)
|
||||
if(await app_updater.execute_graphical(await app_updater.selected_channel(), false))
|
||||
return;
|
||||
} else if(process_args.has_value("update-succeed")) {
|
||||
const result = await electron.dialog.showMessageBox({
|
||||
type: "info",
|
||||
message: "Update successfully installed!\nShould we launch TeaClient?",
|
||||
title: "Update succeeded!",
|
||||
buttons: ["yes", "no"]
|
||||
} as MessageBoxOptions);
|
||||
if(result.response != 0) {
|
||||
electron.app.exit(0);
|
||||
return; //Not really required here!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if(process_args.has_value("update-execute")) {
|
||||
console.log("Executing update " + process_args.value("update-execute"));
|
||||
await app_updater.execute_update(process_args.value("update-execute"), callback => {
|
||||
console.log("Update preconfig successful. Extracting update. (The client should start automatically)");
|
||||
app.quit();
|
||||
setImmediate(callback);
|
||||
});
|
||||
return;
|
||||
} else if(process_args.has_value("update-failed-new") || process_args.has_value("update-succeed-new")) {
|
||||
const success = process_args.has_value("update-succeed-new");
|
||||
let data: {
|
||||
parse_success: boolean;
|
||||
log_file?: string;
|
||||
error_id?: string;
|
||||
error_message?: string;
|
||||
} = {
|
||||
parse_success: false
|
||||
};
|
||||
try {
|
||||
let encoded_data = Buffer.from(process_args.value("update-failed-new") || process_args.value("update-succeed-new"), "base64").toString();
|
||||
for(const part of encoded_data.split(";")) {
|
||||
const index = part.indexOf(':');
|
||||
if(index == -1)
|
||||
data[part] = true;
|
||||
else {
|
||||
data[part.substr(0, index)] = Buffer.from(part.substr(index + 1), "base64").toString();
|
||||
}
|
||||
}
|
||||
data.parse_success = true;
|
||||
} catch($) {
|
||||
console.error($);
|
||||
}
|
||||
console.log("Update success: %o. Update data: %o", success, data);
|
||||
|
||||
let title = "";
|
||||
let type = "";
|
||||
let message = "";
|
||||
|
||||
const buttons: ({
|
||||
key: string,
|
||||
callback: () => Promise<boolean>
|
||||
})[] = [];
|
||||
|
||||
if(success) {
|
||||
open_changelog();
|
||||
|
||||
type = "info";
|
||||
title = "Update succeeded!";
|
||||
|
||||
message = "Update has been successfully installed!\nWhat do you want to do next?";
|
||||
|
||||
buttons.push({
|
||||
key: "Launch client",
|
||||
callback: async () => false
|
||||
});
|
||||
if(data.parse_success && data.log_file) {
|
||||
buttons.push({
|
||||
key: "Open update log",
|
||||
callback: async () => {
|
||||
electron.shell.openItem(data.log_file);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
type = "error";
|
||||
title = "Update failed!";
|
||||
|
||||
message = "Failed to install update.";
|
||||
if(data.parse_success) {
|
||||
message += "\n\n";
|
||||
message += "Error ID: " + (data.error_id || "undefined") + "\n";
|
||||
message += "Error Message: " + (data.error_message || "undefined") + "\n";
|
||||
message += "Installer log: " + (data.log_file || "undefined");
|
||||
} else {
|
||||
message += "\nUnknown error! Lookup the console for more details.";
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
key: "Ignore",
|
||||
callback: async () => false
|
||||
});
|
||||
buttons.push({
|
||||
key: "Retry update",
|
||||
callback: async () => {
|
||||
await app_updater.execute_graphical(await app_updater.selected_channel(), false);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if(data.parse_success && data.log_file) {
|
||||
buttons.push({
|
||||
key: "Open update log",
|
||||
callback: async () => {
|
||||
electron.shell.openItem(data.log_file);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
buttons.push({
|
||||
key: "Close",
|
||||
callback: async () => true
|
||||
});
|
||||
|
||||
const result = await electron.dialog.showMessageBox({
|
||||
type: type,
|
||||
message: message,
|
||||
title: title,
|
||||
buttons: buttons.map(e => e.key)
|
||||
} as MessageBoxOptions);
|
||||
if(buttons[result.response].callback) {
|
||||
if(await buttons[result.response].callback())
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
} catch (error) {
|
||||
console.dir(error);
|
||||
const result = electron.dialog.showMessageBox({
|
||||
type: "error",
|
||||
message: "Failed to execute app main!\n" + error,
|
||||
title: "Main execution failed!",
|
||||
buttons: ["close"]
|
||||
} as MessageBoxOptions);
|
||||
electron.app.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
process.on('uncaughtException', err => {
|
||||
console.error(err, 'Uncaught Exception thrown');
|
||||
console.dir(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
/*
|
||||
if(false) {
|
||||
SegfaultHandler = require('segfault-handler');
|
||||
|
||||
SegfaultHandler.registerHandler("crash.log"); // With no argument, SegfaultHandler will generate a generic log file name
|
||||
}
|
||||
|
||||
const SegfaultHandler = require('segfault-handler');
|
||||
SegfaultHandler.registerHandler("crash.log"); // With no argument, SegfaultHandler will generate a generic log file name
|
||||
*/
|
||||
if(app) { //We're executed!
|
||||
parse_arguments();
|
||||
if(process_args.has_value(Arguments.DISABLE_HARDWARE_ACCELERATION))
|
||||
app.disableHardwareAcceleration();
|
||||
if(process_args.has_value(Arguments.DUMMY_CRASH_MAIN))
|
||||
crash_handler.handler.crash();
|
||||
if(!process_args.has_value(Arguments.DEBUG) && !process_args.has_value(Arguments.NO_SINGLE_INSTANCE)) {
|
||||
if(!app.requestSingleInstanceLock()) {
|
||||
console.log("Another instance is already running. Closing this instance");
|
||||
app.exit(0);
|
||||
}
|
||||
}
|
||||
app.on('ready', execute_app);
|
||||
}
|
||||
}
|
||||
export const execute = main;
|
@ -1,206 +0,0 @@
|
||||
import {BrowserWindow, Menu, MenuItem, MessageBoxOptions, app, dialog} from "electron";
|
||||
import * as electron from "electron";
|
||||
import * as winmgr from "./window";
|
||||
import * as path from "path";
|
||||
|
||||
export let prevent_instant_close: boolean = true;
|
||||
export function set_prevent_instant_close(flag: boolean) {
|
||||
prevent_instant_close = flag;
|
||||
}
|
||||
|
||||
export let is_debug: boolean;
|
||||
export let allow_dev_tools: boolean;
|
||||
|
||||
import {Arguments, parse_arguments, process_args} from "../shared/process-arguments";
|
||||
import * as updater from "./app-updater";
|
||||
import * as loader from "./ui-loader";
|
||||
import * as crash_handler from "../crash_handler";
|
||||
|
||||
// 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 main_window: BrowserWindow = null;
|
||||
|
||||
function spawn_main_window(entry_point: string) {
|
||||
// Create the browser window.
|
||||
console.log("Spawning main window");
|
||||
main_window = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
|
||||
minHeight: 600,
|
||||
minWidth: 600,
|
||||
|
||||
show: false,
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
nodeIntegrationInWorker: true,
|
||||
nodeIntegration: true
|
||||
},
|
||||
icon: path.join(__dirname, "..", "..", "resources", "logo.ico")
|
||||
});
|
||||
|
||||
main_window.webContents.on('devtools-closed', event => {
|
||||
console.log("Dev tools destroyed!");
|
||||
});
|
||||
|
||||
main_window.on('closed', () => {
|
||||
require("./url-preview").close();
|
||||
main_window = null;
|
||||
prevent_instant_close = false;
|
||||
});
|
||||
|
||||
|
||||
main_window.loadFile(loader.ui.preloading_page(entry_point));
|
||||
|
||||
main_window.once('ready-to-show', () => {
|
||||
main_window.show();
|
||||
winmgr.apply_bounds('main-window', main_window).then(() => {
|
||||
winmgr.track_bounds('main-window', main_window);
|
||||
|
||||
main_window.focus();
|
||||
loader.ui.cleanup();
|
||||
if(allow_dev_tools && !main_window.webContents.isDevToolsOpened())
|
||||
main_window.webContents.openDevTools();
|
||||
prevent_instant_close = false; /* just to ensure that the client could be unloaded */
|
||||
});
|
||||
});
|
||||
|
||||
main_window.webContents.on('new-window', (event, url_str, frameName, disposition, options, additionalFeatures) => {
|
||||
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);
|
||||
const url_preview = require("./url-preview");
|
||||
url_preview.open_preview(url_str);
|
||||
} 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);
|
||||
}
|
||||
});
|
||||
|
||||
main_window.webContents.on('crashed', event => {
|
||||
console.error("UI thread crashed! Closing app!");
|
||||
if(!process_args.has_flag(Arguments.DEBUG)) {
|
||||
main_window.close();
|
||||
prevent_instant_close = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handle_error(message: string) {
|
||||
console.log("Caught loading error: %s", message);
|
||||
//"A critical error happened while loading TeaClient!", "A critical error happened while loading TeaClient!<br>" + message
|
||||
dialog.showMessageBox({
|
||||
type: "error",
|
||||
buttons: ["exit"],
|
||||
title: "A critical error happened while loading TeaClient!",
|
||||
message: message
|
||||
});
|
||||
loader.ui.cancel();
|
||||
}
|
||||
|
||||
function init_listener() {
|
||||
app.on('quit', () => {
|
||||
console.debug("Finalizing crash handler");
|
||||
crash_handler.finalize_handler();
|
||||
console.log("RUNNING quit!");
|
||||
loader.cleanup();
|
||||
console.log("RUNNING quit 2!");
|
||||
loader.ui.cleanup();
|
||||
console.log("RUNNING quit done!");
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
console.log("RUNNING all win closed!");
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
if(!prevent_instant_close) {
|
||||
console.log("All windows have been closed, closing app.");
|
||||
app.quit();
|
||||
} else {
|
||||
console.log("All windows have been closed, but we dont want to quit instantly. Waiting 10 seconds if something happens");
|
||||
setTimeout(() => {
|
||||
if(BrowserWindow.getAllWindows().length == 0) {
|
||||
console.log("All windows have been closed for over an minute. Exiting app!");
|
||||
app.quit();
|
||||
}
|
||||
}, 10 * 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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.
|
||||
if (main_window === null) {
|
||||
//spawn_loading_screen();
|
||||
//createWindow()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||||
console.log("Allowing untrusted certificate for %o", url);
|
||||
event.preventDefault();
|
||||
callback(true);
|
||||
});
|
||||
}
|
||||
|
||||
export function execute() {
|
||||
console.log("Main app executed!");
|
||||
|
||||
parse_arguments();
|
||||
is_debug = process_args.has_flag(...Arguments.DEBUG);
|
||||
allow_dev_tools = process_args.has_flag(...Arguments.DEV_TOOLS);
|
||||
if(is_debug) {
|
||||
console.log("Enabled debug!");
|
||||
console.log("Arguments: %o", process_args);
|
||||
}
|
||||
|
||||
Menu.setApplicationMenu(null);
|
||||
init_listener();
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
if(updater.update_restart_pending)
|
||||
return undefined;
|
||||
|
||||
return entry_point;
|
||||
}).then((entry_point: string) => {
|
||||
loader.ui.cleanup(); /* close the window */
|
||||
|
||||
if(entry_point) //has not been canceled
|
||||
spawn_main_window(entry_point);
|
||||
else {
|
||||
console.warn("Missing entry point!");
|
||||
}
|
||||
}).catch(handle_error);
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import "./menu";
|
||||
|
||||
import * as electron from "electron";
|
||||
import ipcMain = electron.ipcMain;
|
||||
import BrowserWindow = electron.BrowserWindow;
|
||||
|
||||
import {open as open_changelog} from "../app-updater/changelog";
|
||||
import * as updater from "../app-updater";
|
||||
|
||||
ipcMain.on('basic-action', (event, action, ...args: any[]) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
|
||||
if(action === "open-changelog") {
|
||||
open_changelog();
|
||||
} else if(action === "check-native-update") {
|
||||
updater.selected_channel().then(channel => updater.execute_graphical(channel, true));
|
||||
} else if(action === "open-dev-tools") {
|
||||
window.webContents.openDevTools();
|
||||
} else if(action === "reload-window") {
|
||||
window.reload();
|
||||
}
|
||||
});
|
@ -1,34 +0,0 @@
|
||||
import * as electron from "electron";
|
||||
import ipcMain = electron.ipcMain;
|
||||
import BrowserWindow = electron.BrowserWindow;
|
||||
|
||||
ipcMain.on('top-menu', (event, menu_template: electron.MenuItemConstructorOptions[]) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
|
||||
const process_template = (item: electron.MenuItemConstructorOptions) => {
|
||||
if(typeof(item.icon) === "string" && item.icon.startsWith("data:"))
|
||||
item.icon = electron.nativeImage.createFromDataURL(item.icon);
|
||||
|
||||
item.click = () => window.webContents.send('top-menu', item.id);
|
||||
for(const i of item.submenu as electron.MenuItemConstructorOptions[] || []) {
|
||||
process_template(i);
|
||||
}
|
||||
};
|
||||
|
||||
for(const m of menu_template)
|
||||
process_template(m);
|
||||
|
||||
try {
|
||||
const menu = new electron.Menu();
|
||||
for(const m of menu_template) {
|
||||
try {
|
||||
menu.append(new electron.MenuItem(m));
|
||||
} catch(error) {
|
||||
console.error("Failed to build menu entry: %o\nSource: %o", error, m);
|
||||
}
|
||||
}
|
||||
window.setMenu(menu_template.length == 0 ? undefined : menu);
|
||||
} catch(error) {
|
||||
console.error("Failed to set window menu: %o", error);
|
||||
}
|
||||
});
|
@ -1,147 +0,0 @@
|
||||
import * as electron from "electron";
|
||||
import * as path from "path";
|
||||
import {screen} from "electron";
|
||||
|
||||
import {Arguments, process_args} from "../../shared/process-arguments";
|
||||
import * as loader from "./loader";
|
||||
import * as updater from "../app-updater";
|
||||
import * as winmgr from "../window";
|
||||
import {main_window} from "../main_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', 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(!process_args.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() {
|
||||
console.log("Spawn window!");
|
||||
let dev_tools = false;
|
||||
|
||||
const WINDOW_WIDTH = 340 + (dev_tools ? 1000 : 0);
|
||||
const WINDOW_HEIGHT = 400 + (process.platform == "win32" ? 40 : 0);
|
||||
|
||||
let bounds = screen.getPrimaryDisplay().bounds;
|
||||
let x = bounds.x + (bounds.width - WINDOW_WIDTH) / 2;
|
||||
let y = bounds.y + (bounds.height - WINDOW_HEIGHT) / 2;
|
||||
console.log("Bounds: %o; Move loader window to %ox%o", bounds, x, y);
|
||||
|
||||
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.loadFile(path.join(path.dirname(module.filename), "ui", "loading_screen.html"));
|
||||
gui.on('closed', () => {
|
||||
if(resolve)
|
||||
resolve();
|
||||
gui = undefined;
|
||||
cleanup();
|
||||
});
|
||||
|
||||
gui.on('ready-to-show', () => {
|
||||
gui.show();
|
||||
gui.setPosition(x, y);
|
||||
winmgr.apply_bounds('ui-load-window', gui, undefined, { apply_size: false }).then(() => {
|
||||
winmgr.track_bounds('ui-load-window', gui);
|
||||
|
||||
const call_loader = () => load_files().catch(reject);
|
||||
if(!process_args.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,515 +0,0 @@
|
||||
import {is_debug} from "../main_window";
|
||||
|
||||
const request = require('request');
|
||||
const querystring = require('querystring');
|
||||
const fs = require('fs-extra');
|
||||
const os = require('os');
|
||||
const UUID = require('pure-uuid');
|
||||
import * as path from "path";
|
||||
import * as zlib from "zlib";
|
||||
import * as tar from "tar-stream";
|
||||
import {Arguments, process_args} 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";
|
||||
|
||||
const TIMEOUT = 10000;
|
||||
let local_path = undefined;
|
||||
|
||||
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 = (process_args.has_value(...Arguments.SERVER_URL) ? process_args.value(...Arguments.SERVER_URL) : default_path);
|
||||
};
|
||||
|
||||
function data_directory() : string {
|
||||
return electron.app.getPath('userData');
|
||||
}
|
||||
|
||||
function cache_directory() : string {
|
||||
return path.join(data_directory(), "cache", "ui");
|
||||
}
|
||||
|
||||
function working_directory() : string {
|
||||
return path.join(data_directory(), "tmp", "ui");
|
||||
}
|
||||
|
||||
export interface VersionedFile {
|
||||
name: string,
|
||||
hash: string,
|
||||
path: string,
|
||||
type: string,
|
||||
|
||||
local_url: () => Promise<String>
|
||||
}
|
||||
|
||||
function generate_tmp() : Promise<String> {
|
||||
if(local_path) return Promise.resolve(local_path);
|
||||
|
||||
const id = new UUID(4).format();
|
||||
const directory = path.join(os.tmpdir(), "TeaClient-" + id) + "/";
|
||||
|
||||
return fs.mkdirs(directory).then(() => {
|
||||
local_path = directory;
|
||||
global["browser-root"] = local_path;
|
||||
console.log("Local browser path: %s", local_path);
|
||||
return Promise.resolve(local_path);
|
||||
});
|
||||
}
|
||||
|
||||
function get_raw_app_files() : Promise<VersionedFile[]> {
|
||||
return generate_tmp().then(path => 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) => {
|
||||
response = response || {statusCode: -1};
|
||||
|
||||
if(error) { reject(error); return; }
|
||||
if(response.statusCode != 200) { setImmediate(reject, "invalid status code " + response.statusCode + " for " + url); return; }
|
||||
if(response.headers["info-version"] != 1) { 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);
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function download_raw_app_files() : Promise<VersionedFile[]> {
|
||||
return get_raw_app_files().then(response => {
|
||||
for(let file of response) {
|
||||
const full_path = path.join(local_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 => {
|
||||
setImmediate(reject, error);
|
||||
}).pipe(write_stream)
|
||||
.on('finish', event => {
|
||||
setImmediate(resolve, file.path + "/" + file.name);
|
||||
});
|
||||
}));
|
||||
}
|
||||
return Promise.resolve(response);
|
||||
}).catch(error => {
|
||||
console.log("Failed to get file list: %o", error);
|
||||
return Promise.reject("Failed to get file list (" + error + ")");
|
||||
})
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function ui_file_path(version: UIVersion) : string {
|
||||
if(version.client_shipped) {
|
||||
const app_path = electron.app.getAppPath();
|
||||
if(!app_path.endsWith(".asar"))
|
||||
return undefined;
|
||||
|
||||
return path.join(path.join(path.dirname(app_path), "ui"), version.filename);
|
||||
}
|
||||
|
||||
const file_name = "ui_" + version.channel + "_" + version.version + "_" + version.git_hash + "_" + version.timestamp + ".tar.gz";
|
||||
return path.join(cache_directory(), file_name);
|
||||
}
|
||||
|
||||
let _ui_load_cache: LocalUICache;
|
||||
async function ui_load_cache() : Promise<LocalUICache> {
|
||||
if(_ui_load_cache) return _ui_load_cache;
|
||||
|
||||
const file = path.join(cache_directory(), "data.json");
|
||||
if(!fs.existsSync(file)) return {} as LocalUICache;
|
||||
|
||||
console.log("Loading UI cache file %s", file);
|
||||
_ui_load_cache = await fs.readJson(file) as LocalUICache;
|
||||
return _ui_load_cache;
|
||||
}
|
||||
|
||||
async function client_shipped_ui() : Promise<UIVersion | 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 {
|
||||
channel: info.channel,
|
||||
client_shipped: true,
|
||||
|
||||
filename: info.filename,
|
||||
git_hash: info.git_hash,
|
||||
required_client: info.required_client,
|
||||
timestamp: info.timestamp,
|
||||
version: info.version,
|
||||
}
|
||||
}
|
||||
|
||||
async function ui_save_cache(cache: LocalUICache) {
|
||||
const file = path.join(cache_directory(), "data.json");
|
||||
if(!fs.existsSync(path.dirname(file)))
|
||||
await fs.mkdirs(path.dirname(file));
|
||||
await fs.writeJson(file, cache);
|
||||
}
|
||||
|
||||
async function get_ui_pack(channel?: string) : Promise<UIVersion[] | UIVersion> {
|
||||
return await new Promise<UIVersion[] | UIVersion>((resolve, reject) => {
|
||||
const url = remote_url() + "api.php?" + querystring.stringify({
|
||||
type: "ui-info"
|
||||
});
|
||||
request.get(url, {
|
||||
timeout: TIMEOUT
|
||||
}, (error, response, body: string) => {
|
||||
try {
|
||||
response = response || {statusCode: -1};
|
||||
|
||||
if(error) { throw error; }
|
||||
if(response.statusCode != 200) { throw "invalid status code " + response.statusCode + " for " + url; }
|
||||
if(!body) throw "invalid response body";
|
||||
|
||||
let result: UIVersion[] = [];
|
||||
|
||||
const json = JSON.parse(body) || {success: false, msg: "invalid body"};
|
||||
if(!json["success"]) throw "Failed to get ui info: " + json["msg"];
|
||||
|
||||
for(const entry of json["versions"]) {
|
||||
if(!channel || entry["channel"] == channel)
|
||||
result.push({
|
||||
channel: entry["channel"],
|
||||
version: entry["version"],
|
||||
git_hash: entry["git-ref"],
|
||||
timestamp: entry["timestamp"],
|
||||
required_client: entry["required_client"]
|
||||
});
|
||||
}
|
||||
|
||||
if(result.length == 0 && channel) result.push(undefined);
|
||||
const res = channel ? result[0] : result;
|
||||
ui_load_cache().then(async cache => {
|
||||
cache.fetch_history = cache.fetch_history || {} as any;
|
||||
cache.fetch_history.timestamp = Date.now();
|
||||
cache.fetch_history.status = 0;
|
||||
cache.remote_index = res as any;
|
||||
cache.remote_index_channel = channel;
|
||||
await ui_save_cache(cache);
|
||||
}).catch(error => {
|
||||
console.warn("Failed to save UI cache info: %o", error);
|
||||
resolve(res);
|
||||
}).then(err => resolve(res));
|
||||
} catch(error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
async function download_ui_pack(version: UIVersion) : Promise<void> {
|
||||
const directory = cache_directory();
|
||||
const file = ui_file_path(version);
|
||||
await fs.mkdirs(directory);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
request.get(remote_url() + "api.php?" + querystring.stringify({
|
||||
type: "ui-download",
|
||||
"git-ref": version.git_hash,
|
||||
version: version.version,
|
||||
timestamp: version.timestamp,
|
||||
channel: version.channel
|
||||
}), {
|
||||
timeout: TIMEOUT
|
||||
}).on('response', function(response) {
|
||||
if(response.statusCode != 200) { reject("Failed to download UI files (Status code " + response.statusCode + ")"); }
|
||||
}).on('error', error => {
|
||||
reject("Failed to download UI files: " + error);
|
||||
}).pipe(fs.createWriteStream(file)).on('finish', () => {
|
||||
ui_load_cache().then(cache => {
|
||||
cache.versions.push({
|
||||
checksum: "undefined",
|
||||
tar_file: file,
|
||||
download_timestamp: Date.now(),
|
||||
version: version
|
||||
});
|
||||
return ui_save_cache(cache);
|
||||
}).catch(error => resolve()).then(() => resolve());
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ui_pack_exists(version: UIVersion) : boolean {
|
||||
return fs.existsSync(ui_file_path(version));
|
||||
}
|
||||
|
||||
async function unpack_cached(version: UIVersion) : Promise<string> {
|
||||
const file = ui_file_path(version);
|
||||
if(!fs.existsSync(file)) throw "missing file";
|
||||
|
||||
const target_dir = path.join(working_directory(), version.channel + "_" + version.timestamp);
|
||||
if(fs.existsSync(target_dir)) fs.removeSync(target_dir);
|
||||
|
||||
await fs.mkdirs(target_dir);
|
||||
|
||||
const gunzip = zlib.createGunzip();
|
||||
const extract = tar.extract();
|
||||
const fpipe = fs.createReadStream(file);
|
||||
|
||||
extract.on('entry', function(header: tar.Headers, stream, next) {
|
||||
if(header.type == 'file') {
|
||||
const target_file = path.join(target_dir, 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_dir, header.name)))
|
||||
setImmediate(next);
|
||||
fs.mkdirs(path.join(target_dir, header.name)).catch(error => {
|
||||
console.warn("Failed to create unpacking fir " + path.join(target_dir, 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 => {
|
||||
extract.on('finish', resolve);
|
||||
extract.on('error', event => {
|
||||
if(!event) return;
|
||||
throw event;
|
||||
});
|
||||
});
|
||||
|
||||
fpipe.pipe(gunzip).pipe(extract);
|
||||
await finish_promise;
|
||||
|
||||
return target_dir;
|
||||
}
|
||||
|
||||
export async function cleanup() {
|
||||
if(await fs.pathExists(local_path))
|
||||
await fs.remove(local_path);
|
||||
}
|
||||
|
||||
export async function load_files(channel: string, static_cb: (message: string, index: number) => any) : Promise<String> {
|
||||
const type = parseInt(process_args.has_value(Arguments.UPDATER_UI_LOAD_TYPE) ? process_args.value(Arguments.UPDATER_UI_LOAD_TYPE) : "-1");
|
||||
if(type == 0 || !is_debug) {
|
||||
console.log("Loading ui package");
|
||||
|
||||
static_cb("Fetching info", 0);
|
||||
const cache = await ui_load_cache();
|
||||
console.log("Local cache: %o", cache);
|
||||
|
||||
let ui_info: UIVersion;
|
||||
try {
|
||||
ui_info = await get_ui_pack(channel) as UIVersion;
|
||||
} catch(error) {
|
||||
if(error instanceof Error)
|
||||
console.error("Failed to fetch ui info: %s. Using cached info!", error.message);
|
||||
else
|
||||
console.error("Failed to fetch ui info: %o. Using cached info!", error);
|
||||
}
|
||||
if(!ui_info) {
|
||||
if(cache && !process_args.has_flag(Arguments.UPDATER_UI_NO_CACHE)) {
|
||||
if(Array.isArray(cache.remote_index)) {
|
||||
for(const index of cache.remote_index) {
|
||||
if(index && index.channel == "release") {
|
||||
ui_info = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//TODO: test channel?
|
||||
ui_info = cache.remote_index;
|
||||
}
|
||||
}
|
||||
if(ui_info) {
|
||||
console.debug("Found local UI pack.");
|
||||
} else {
|
||||
//Test for the client shipped ui pack
|
||||
try {
|
||||
console.info("Looking for client shipped UI pack.");
|
||||
ui_info = await client_shipped_ui();
|
||||
if(!ui_info)
|
||||
throw "failed to load info";
|
||||
console.info("Using client shipped UI pack because we've no active internet connection.")
|
||||
} catch(error) {
|
||||
console.warn("Failed to load client shipped UI pack: %o", error);
|
||||
throw "Failed to load UI pack from cache!\nPlease ensure a valid internet connection.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static_cb("Searching cache for file", .33);
|
||||
console.log("Loading UI from data: %o. Target path: %s", ui_info, ui_file_path(ui_info));
|
||||
if(ui_info.required_client && !process_args.has_flag(Arguments.DEBUG)) {
|
||||
const ui_vers = parse_version(ui_info.required_client);
|
||||
const current_vers = await current_version();
|
||||
console.log("Checking required client version (Required: %s, Version: %s)", ui_vers.toString(true), current_vers.toString(true));
|
||||
if(ui_vers.newer_than(current_vers) && !current_vers.in_dev()) {
|
||||
const local_available = cache && cache.local_index ? ui_pack_exists(cache.local_index) : undefined;
|
||||
|
||||
const result = await electron.dialog.showMessageBox({
|
||||
type: "question",
|
||||
message:
|
||||
"Local client is outdated.\n" +
|
||||
"Newer UI packs (>= " + ui_info.version + ") require client " + ui_info.required_client + "\n" +
|
||||
"Do you want to upgrade?",
|
||||
title: "Client outdated!",
|
||||
buttons: ["yes", local_available ? "ignore and use last possible (" + cache.local_index.version + ")" : "close client"]
|
||||
} as MessageBoxOptions);
|
||||
if(result.response == 0) {
|
||||
await execute_graphical(channel, true);
|
||||
throw "client outdated";
|
||||
} else {
|
||||
if(!local_available) {
|
||||
electron.app.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
ui_info = cache.local_index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!ui_pack_exists(ui_info)) {
|
||||
console.log("Ui version does not locally exists. Downloading new one");
|
||||
static_cb("Downloading files", .34);
|
||||
await download_ui_pack(ui_info);
|
||||
console.log("Download completed!");
|
||||
}
|
||||
|
||||
console.log("Unpacking cached ui info");
|
||||
static_cb("Unpacking files", .66);
|
||||
const target_path = await unpack_cached(ui_info);
|
||||
cache.local_index = ui_info;
|
||||
await ui_save_cache(cache);
|
||||
|
||||
console.log("Unpacked. Target path: %s", target_path);
|
||||
static_cb("UI loaded", 1);
|
||||
|
||||
return path.join(target_path, "index.html");
|
||||
} else {
|
||||
console.log("Loading file by file");
|
||||
|
||||
static_cb("Fetching files", 0);
|
||||
let files;
|
||||
try {
|
||||
files = await download_raw_app_files()
|
||||
} catch (error) {
|
||||
throw "Failed to get file list: " + error;
|
||||
}
|
||||
console.log("Get raw files:");
|
||||
let futures: Promise<void>[] = [];
|
||||
let finish_count = 0;
|
||||
static_cb("Downloading files", 0);
|
||||
|
||||
for(const file of files) {
|
||||
console.log("Start downloading %s (%s)", file.name, file.path);
|
||||
|
||||
const start = Date.now();
|
||||
futures.push(file.local_url().then(data => {
|
||||
finish_count++;
|
||||
console.log("Downloaded %s (%s) (%ims)", file.name, file.path, Date.now() - start);
|
||||
static_cb("Downloading files", finish_count / files.length);
|
||||
}));
|
||||
|
||||
//await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(futures);
|
||||
} catch (error) {
|
||||
throw "Failed to download files: " + error;
|
||||
}
|
||||
return await generate_tmp() + "index.html"; /* entry point */
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.6 KiB |
@ -1,21 +0,0 @@
|
||||
const icp = require("electron").ipcRenderer;
|
||||
|
||||
interface Window {
|
||||
$: JQuery;
|
||||
}
|
||||
(window as any).$ = require("jquery");
|
||||
|
||||
icp.on('progress-update', (event, count) => {
|
||||
console.log("Process update to %f", count);
|
||||
|
||||
$(".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,98 +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;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
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%;
|
||||
}
|
||||
</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">
|
||||
<img class="smoke" src="img/smoke.png">
|
||||
</div>
|
||||
<div class="container-info">
|
||||
<a id="loading-text">Loading... Please wait!</a>
|
||||
<div class="container-bar">
|
||||
<div class="bar"></div>
|
||||
</div>
|
||||
</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))
|
||||
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>
|
@ -1,86 +0,0 @@
|
||||
#nav-body-ctrls {
|
||||
background-color: #2a2a2a;
|
||||
padding: 20px;
|
||||
font-family: arial
|
||||
}
|
||||
|
||||
#nav-body-tabs {
|
||||
background: linear-gradient(#2a2a2a 75%, #404040);
|
||||
height: 36px;
|
||||
font-family: arial
|
||||
}
|
||||
|
||||
#nav-body-views {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.nav-icons {
|
||||
fill: #fcfcfc !important
|
||||
}
|
||||
|
||||
.nav-icons:hover {
|
||||
fill: #c2c2c2 !important
|
||||
}
|
||||
|
||||
#nav-ctrls-back, #nav-ctrls-forward, #nav-ctrls-reload {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
margin-right: 10px
|
||||
}
|
||||
|
||||
#nav-ctrls-url {
|
||||
box-shadow: 0 0;
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
height: 30px !important;
|
||||
margin-left: 8px;
|
||||
font-size: 11pt;
|
||||
outline: none;
|
||||
padding-left: 10px;
|
||||
color: #b7b7b7;
|
||||
background-color: #404040
|
||||
}
|
||||
|
||||
#nav-ctrls-url:focus {
|
||||
color: #fcfcfc;
|
||||
box-shadow: 0 0 5px #3d3d3d;
|
||||
}
|
||||
|
||||
#nav-tabs-add {
|
||||
margin: 5px
|
||||
}
|
||||
|
||||
.nav-tabs-tab {
|
||||
border-radius: 2px;
|
||||
height: 35px
|
||||
}
|
||||
|
||||
.nav-tabs-tab.active {
|
||||
background: #404040
|
||||
}
|
||||
|
||||
.nav-tabs-favicon {
|
||||
margin: 6px
|
||||
}
|
||||
|
||||
.nav-tabs-title {
|
||||
padding-left: 5px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
color: #fcfcfc
|
||||
}
|
||||
|
||||
.nav-tabs-title:hover {
|
||||
color: #c2c2c2
|
||||
}
|
||||
|
||||
.nav-tabs-close {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 6px;
|
||||
margin-left: 2px
|
||||
}
|
||||
|
||||
.nav-tabs-close:hover {
|
||||
fill: #dc143c !important
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>TeaClient - URL preview</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="./index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="nav-body-ctrls">
|
||||
<!-- address -->
|
||||
</div>
|
||||
<div id="nav-body-tabs">
|
||||
<!-- tabs -->
|
||||
</div>
|
||||
<div id="nav-body-views">
|
||||
<!-- view -->
|
||||
</div>
|
||||
<script>
|
||||
let exports = {};
|
||||
</script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,78 +0,0 @@
|
||||
import * as electron from "electron";
|
||||
import * as path from "path";
|
||||
|
||||
interface Options {
|
||||
showBackButton: boolean,
|
||||
showForwardButton: boolean,
|
||||
showReloadButton: boolean,
|
||||
showUrlBar: boolean,
|
||||
showAddTabButton: boolean,
|
||||
closableTabs: boolean,
|
||||
verticalTabs: boolean,
|
||||
defaultFavicons: boolean,
|
||||
newTabCallback: (url: string, options: any) => any,
|
||||
changeTabCallback: () => any,
|
||||
newTabParams: any
|
||||
}
|
||||
|
||||
interface NewTabOptions {
|
||||
id: string,
|
||||
node: boolean,
|
||||
readonlyUrl: boolean,
|
||||
contextMenu: boolean,
|
||||
webviewAttributes: any,
|
||||
icon: "clean" | "default" | string,
|
||||
title: "default",
|
||||
close: boolean
|
||||
}
|
||||
|
||||
const enav = new (require('electron-navigation'))({
|
||||
closableTabs: true,
|
||||
showAddTabButton: false,
|
||||
defaultFavicons: true,
|
||||
|
||||
changeTabCallback: new_tab => {
|
||||
if(new_tab === undefined)
|
||||
window.close();
|
||||
}
|
||||
} as Options);
|
||||
|
||||
/* Required here: https://github.com/simply-coded/electron-navigation/blob/master/index.js#L364 */
|
||||
enav.executeJavaScript = () => {}; /* just to suppress an error cause by the API */
|
||||
|
||||
let _id_counter = 0;
|
||||
const execute_preview = (url: string) => {
|
||||
const id = "preview_" + (++_id_counter);
|
||||
const tab: HTMLElement & { executeJavaScript(js: string) : Promise<any> } = enav.newTab(url, {
|
||||
id: id,
|
||||
contextMenu: false,
|
||||
readonlyUrl: true,
|
||||
icon: "default",
|
||||
webviewAttributes: {
|
||||
'preload': path.join(__dirname, "inject.js")
|
||||
}
|
||||
} as NewTabOptions);
|
||||
|
||||
/* we only want to preload our script once */
|
||||
const show_preview = () => {
|
||||
tab.removeEventListener("dom-ready", show_preview);
|
||||
tab.removeAttribute("preload");
|
||||
|
||||
tab.executeJavaScript('__teaclient_preview_notice()').catch((error) => console.log("Failed to show TeaClient overlay! Error: %o", error));
|
||||
};
|
||||
|
||||
tab.addEventListener("dom-ready", show_preview);
|
||||
|
||||
tab.addEventListener('did-fail-load', (res: any) => {
|
||||
console.error("Side load failed: %o", res);
|
||||
if (res.errorCode != -3) {
|
||||
res.target.executeJavaScript('__teaclient_preview_error("' + res.errorCode + '", "' + encodeURIComponent(res.errorDescription) + '", "' + encodeURIComponent(res.validatedURL) + '")').catch(error => {
|
||||
console.warn("Failed to show error page: %o", error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
tab.addEventListener('close', () => enav.closeTab(id));
|
||||
};
|
||||
|
||||
electron.ipcRenderer.on('preview', (event, url) => execute_preview(url));
|
@ -1,118 +0,0 @@
|
||||
declare let __teaclient_preview_notice: () => any;
|
||||
declare let __teaclient_preview_error;
|
||||
|
||||
const electron = require("electron");
|
||||
const log_prefix = "[TeaSpeak::Preview] ";
|
||||
|
||||
const html_overlay =
|
||||
"<div style='position: fixed; top: 0; bottom: 0; left: 0; right: 0; z-index: 99999999999999999999999999;'>" +
|
||||
"<div style='\n" +
|
||||
"font-family: \"Open Sans\"," +
|
||||
"sans-serif;\n" +
|
||||
"width: 100%;\n" +
|
||||
"margin: 0;\n" +
|
||||
"height: 40px;\n" +
|
||||
"font-size: 17px;\n" +
|
||||
"font-weight: 400;\n" +
|
||||
"padding: .33em .5em;\n" +
|
||||
"color: #5c5e60;\n" +
|
||||
"position: fixed;\n" +
|
||||
"background-color: white;\n" +
|
||||
"box-shadow: 0 1px 3px 2px rgba(0,0,0,0.15);" +
|
||||
"display: flex;\n" +
|
||||
"flex-direction: row;\n" +
|
||||
"justify-content: center;" +
|
||||
"align-items: center;'" +
|
||||
">" +
|
||||
"<div style='margin-right: .67em;display: inline-block;line-height: 1.3;text-align: center'>You're in TeaWeb website preview mode. Click <a href='#' class='button-open'>here</a> to open the website in the browser</div>" +
|
||||
"</div>" +
|
||||
"<div style='display: table-cell;width: 1.6em;'>" +
|
||||
"<a style='font-size: 14px;\n" +
|
||||
"top: 13px;\n" +
|
||||
"right: 25px;\n" +
|
||||
"width: 15px;\n" +
|
||||
"height: 15px;\n" +
|
||||
"opacity: .3;\n" +
|
||||
"color: #000;\n" +
|
||||
"cursor: pointer;\n" +
|
||||
"position: absolute;\n" +
|
||||
"text-align: center;\n" +
|
||||
"line-height: 15px;\n" +
|
||||
"z-index: 1000;\n" +
|
||||
"text-decoration: none;'" +
|
||||
"class='button-close'>" +
|
||||
"✖" +
|
||||
"</a>" +
|
||||
"</div>" +
|
||||
"</div>";
|
||||
|
||||
let _close_overlay: () => void;
|
||||
let _inject_overlay = () => {
|
||||
const element = document.createElement("div");
|
||||
element.id = "TeaClient-Overlay-Container";
|
||||
document.body.append(element);
|
||||
element.innerHTML = html_overlay;
|
||||
|
||||
{
|
||||
_close_overlay = () => {
|
||||
console.trace(log_prefix + "Closing preview notice");
|
||||
element.remove();
|
||||
};
|
||||
|
||||
const buttons = element.getElementsByClassName("button-close");
|
||||
if(buttons.length < 1) {
|
||||
console.warn(log_prefix + "Failed to find close button for preview notice!");
|
||||
} else {
|
||||
for(const button of buttons) {
|
||||
(<HTMLElement>button).onclick = _close_overlay;
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
const buttons = element.getElementsByClassName("button-open");
|
||||
if(buttons.length < 1) {
|
||||
console.warn(log_prefix + "Failed to find open button for preview notice!");
|
||||
} else {
|
||||
for(const element of buttons) {
|
||||
(<HTMLElement>element).onclick = event => {
|
||||
console.info(log_prefix + "Opening URL with default browser");
|
||||
electron.remote.shell.openExternal(location.href, {
|
||||
activate: true
|
||||
}).catch(error => {
|
||||
console.warn(log_prefix + "Failed to open URL in browser window: %o", error);
|
||||
}).then(() => {
|
||||
window.close();
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* Put this into the global scope. But we dont leek some nodejs stuff! */
|
||||
console.log(log_prefix + "Script loaded waiting to be called!");
|
||||
__teaclient_preview_notice = () => {
|
||||
if(_inject_overlay) {
|
||||
console.log(log_prefix + "TeaClient overlay called. Showing overlay.");
|
||||
_inject_overlay();
|
||||
} else {
|
||||
console.warn(log_prefix + "TeaClient overlay called, but overlay method undefined. May an load error occured?");
|
||||
}
|
||||
};
|
||||
|
||||
const html_error = (error_code, error_desc, url) =>
|
||||
"<div style='background-color: whitesmoke; padding: 40px; margin: 20px; font-family: consolas,serif;'>" +
|
||||
"<h2 align=center>Oops, this page failed to load correctly.</h2>" +
|
||||
"<p align=center><i>ERROR [ " + error_code + ", " + error_desc + " ]</i></p>" +
|
||||
'<br/><hr/>' +
|
||||
'<h4>Try this</h4>' +
|
||||
'<li type=circle>Check your spelling - <b>"' + url + '".</b></li><br/>' +
|
||||
'<li type=circle><a href="javascript:location.reload();">Refresh</a> the page.</li><br/>' +
|
||||
'<li type=circle>Perform a <a href=javascript:location.href="https://www.google.com/search?q=' + url + '">search</a> instead.</li><br/>' +
|
||||
"</div>";
|
||||
|
||||
__teaclient_preview_error = (error_code, error_desc, url) => {
|
||||
document.body.innerHTML = html_error(decodeURIComponent(error_code), decodeURIComponent(error_desc), decodeURIComponent(url));
|
||||
_inject_overlay = undefined;
|
||||
if(_close_overlay) _close_overlay();
|
||||
};
|
@ -1,105 +0,0 @@
|
||||
import * as electron from "electron";
|
||||
import * as path from "path";
|
||||
import * as winmgr from "../window";
|
||||
|
||||
let global_window: electron.BrowserWindow;
|
||||
let global_window_promise: Promise<void>;
|
||||
|
||||
export async function close() {
|
||||
while(global_window_promise) {
|
||||
try {
|
||||
await global_window_promise;
|
||||
break;
|
||||
} catch(error) {} /* error will be already logged */
|
||||
}
|
||||
if(global_window) {
|
||||
global_window.close();
|
||||
global_window = undefined;
|
||||
global_window_promise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function open_preview(url: string) {
|
||||
while(global_window_promise) {
|
||||
try {
|
||||
await global_window_promise;
|
||||
break;
|
||||
} catch(error) {} /* error will be already logged */
|
||||
}
|
||||
if(!global_window) {
|
||||
global_window_promise = (async () => {
|
||||
global_window = new electron.BrowserWindow({
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
webviewTag: true
|
||||
},
|
||||
center: true,
|
||||
show: false,
|
||||
});
|
||||
global_window.setMenuBarVisibility(false);
|
||||
global_window.setMenu(null);
|
||||
global_window.loadFile(path.join(__dirname, "html", "index.html")).then(() => {
|
||||
//global_window.webContents.openDevTools();
|
||||
});
|
||||
global_window.on('close', event => {
|
||||
global_window = undefined;
|
||||
});
|
||||
|
||||
try {
|
||||
await winmgr.apply_bounds('url-preview', global_window);
|
||||
winmgr.track_bounds('url-preview', global_window);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject("timeout"), 5000);
|
||||
global_window.on('ready-to-show', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} catch(error) {
|
||||
console.warn("Failed to initialize preview window. Dont show preview! Error: %o", error);
|
||||
throw "failed to initialize";
|
||||
}
|
||||
|
||||
global_window.show();
|
||||
})();
|
||||
try {
|
||||
await global_window_promise;
|
||||
} catch(error) {
|
||||
console.log("Failed to create preview window! Error: %o", error);
|
||||
try {
|
||||
global_window.close();
|
||||
} finally {
|
||||
global_window = undefined;
|
||||
}
|
||||
global_window_promise = undefined;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Opening URL '%s' as preview.", url);
|
||||
global_window.webContents.send('preview', url);
|
||||
if(!global_window.isFocused())
|
||||
global_window.focus();
|
||||
}
|
||||
|
||||
electron.ipcMain.on('preview-action', (event, args) => {
|
||||
const sender: electron.WebContents = event.sender;
|
||||
if(!args || !args.action) {
|
||||
console.warn("Received preview action without a valid action type!");
|
||||
return;
|
||||
}
|
||||
|
||||
if(args.action === "open-url") {
|
||||
console.log("Opening " +args.url);
|
||||
electron.shell.openExternal(args.url, {
|
||||
activate: true
|
||||
});
|
||||
|
||||
const browser = electron.BrowserWindow.fromWebContents(sender);
|
||||
if(!browser)
|
||||
console.warn("Failed to find browser handle");
|
||||
else
|
||||
browser.close();
|
||||
}
|
||||
});
|
@ -1,109 +0,0 @@
|
||||
import * as electron from "electron";
|
||||
import * as fs from "fs-extra";
|
||||
import * as path from "path";
|
||||
|
||||
/* We read/write to this file every time again because this file could be used by multible processes */
|
||||
const data_file: string = path.join(electron.app.getPath('userData'), "window-bounds.json");
|
||||
|
||||
import BrowserWindow = Electron.BrowserWindow;
|
||||
import Rectangle = Electron.Rectangle;
|
||||
|
||||
let _changed_data: {[key: string]:Rectangle} = {};
|
||||
let _changed_saver: NodeJS.Timer;
|
||||
|
||||
export async function save_changes() {
|
||||
clearTimeout(_changed_saver);
|
||||
|
||||
try {
|
||||
const data = (await fs.pathExists(data_file) ? await fs.readJson(data_file) : {}) || {};
|
||||
Object.assign(data, _changed_data);
|
||||
|
||||
await fs.ensureFile(data_file);
|
||||
await fs.writeJson(data_file, data);
|
||||
path_exists = true;
|
||||
|
||||
_changed_data = {};
|
||||
} catch(error) {
|
||||
console.warn("Failed to save window bounds: %o", error);
|
||||
}
|
||||
console.log("Window bounds have been successfully saved!");
|
||||
}
|
||||
|
||||
let path_exists = undefined;
|
||||
export async function get_last_bounds(key: string) : Promise<Rectangle> {
|
||||
try {
|
||||
if(typeof(path_exists) === "undefined" ? !(path_exists = await fs.pathExists(data_file)) : !path_exists)
|
||||
throw "skip!";
|
||||
|
||||
const data = await fs.readJson(data_file) || {};
|
||||
if(data[key])
|
||||
return data[key];
|
||||
} catch(error) {
|
||||
if(error !== "skip!")
|
||||
console.warn("Failed to load window bounds for %s: %o", key, error);
|
||||
}
|
||||
|
||||
return {
|
||||
height: undefined,
|
||||
width: undefined,
|
||||
x: undefined,
|
||||
y: undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function track_bounds(key: string, window: BrowserWindow) {
|
||||
const events = ['move', 'moved', 'resize'];
|
||||
|
||||
const update_bounds = () => {
|
||||
_changed_data[key] = window.getBounds();
|
||||
|
||||
clearTimeout(_changed_saver);
|
||||
_changed_saver = setTimeout(save_changes, 1000);
|
||||
};
|
||||
|
||||
for(const event of events)
|
||||
window.on(event as any, update_bounds);
|
||||
|
||||
window.on('closed', () => {
|
||||
for(const event of events)
|
||||
window.removeListener(event as any, update_bounds);
|
||||
})
|
||||
}
|
||||
|
||||
export async function apply_bounds(key: string, window: BrowserWindow, bounds?: Rectangle, options?: { apply_size?: boolean; apply_position?: boolean }) {
|
||||
const screen = electron.screen;
|
||||
|
||||
if(!bounds)
|
||||
bounds = await get_last_bounds(key);
|
||||
|
||||
if(!options)
|
||||
options = {};
|
||||
|
||||
const original_bounds = window.getBounds();
|
||||
|
||||
if(typeof(options.apply_size) !== "boolean" || options.apply_size) {
|
||||
let height = bounds.height > 0 ? bounds.height : original_bounds.height;
|
||||
let width = bounds.width > 0 ? bounds.width : original_bounds.width;
|
||||
|
||||
if(height != original_bounds.height || width != original_bounds.width)
|
||||
window.setSize(width, height, true);
|
||||
}
|
||||
if(typeof(options.apply_position) !== "boolean" || options.apply_position) {
|
||||
let x = typeof(bounds.x) === "number" ? bounds.x : original_bounds.x;
|
||||
let y = typeof(bounds.y) === "number" ? bounds.y : original_bounds.y;
|
||||
|
||||
if(x != original_bounds.x || y != original_bounds.y) {
|
||||
const display = screen.getDisplayNearestPoint({ x: x, y: y });
|
||||
if(display) {
|
||||
const bounds = display.workArea || display.bounds;
|
||||
let flag_invalid = false;
|
||||
flag_invalid = flag_invalid || bounds.x > x || (bounds.x + bounds.width) < x;
|
||||
flag_invalid = flag_invalid || bounds.y > x || (bounds.y + bounds.height) < y;
|
||||
if(!flag_invalid) {
|
||||
window.setPosition(x, y, true);
|
||||
console.log("Updating position for %s", key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
import {app, BrowserWindow, remote} from "electron";
|
||||
import * as path from "path";
|
||||
import * as electron from "electron";
|
||||
import * as os from "os";
|
||||
|
||||
export function handle_crash_callback(args: string[]) {
|
||||
const parameter = {};
|
||||
for(const argument of args) {
|
||||
const colon_index = argument.indexOf('=');
|
||||
if(colon_index == -1) {
|
||||
console.warn("Crash callback contains invalid argument! (%s)", argument);
|
||||
continue;
|
||||
}
|
||||
|
||||
parameter[argument.substr(0, colon_index)] = argument.substr(colon_index + 1);
|
||||
}
|
||||
console.log("Received crash dump callback. Arguments: %o", parameter);
|
||||
|
||||
let error = undefined;
|
||||
let crash_file = undefined;
|
||||
|
||||
if(parameter["success"] == true) {
|
||||
/* okey we have an crash dump */
|
||||
crash_file = parameter["dump_path"];
|
||||
if(typeof(crash_file) === "string") {
|
||||
try {
|
||||
crash_file = Buffer.from(crash_file, 'base64').toString();
|
||||
} catch(error) {
|
||||
console.warn("Failed to decode dump path: %o", error);
|
||||
crash_file = undefined;
|
||||
error = "failed to decode dump path!";
|
||||
}
|
||||
}
|
||||
} else if(typeof(parameter["error"]) === "string") {
|
||||
try {
|
||||
error = Buffer.from(crash_file, 'base64').toString();
|
||||
} catch(error) {
|
||||
console.warn("Failed to decode error: %o", error);
|
||||
error = "failed to decode error";
|
||||
}
|
||||
} else {
|
||||
error = "missing parameters";
|
||||
}
|
||||
|
||||
app.on('ready', () => {
|
||||
const crash_window = new BrowserWindow({
|
||||
show: false,
|
||||
width: 1000,
|
||||
height: 300 + (os.platform() === "win32" ? 50 : 0),
|
||||
|
||||
webPreferences: {
|
||||
devTools: true,
|
||||
nodeIntegration: true,
|
||||
javascript: true
|
||||
}
|
||||
});
|
||||
crash_window.setMenu(null);
|
||||
crash_window.loadFile(path.join(path.dirname(module.filename), "ui", "index.html"));
|
||||
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();
|
||||
});
|
||||
app.on('window-all-closed', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
crash_window.focus();
|
||||
});
|
||||
}
|
||||
|
||||
module.paths.push(...(() => {
|
||||
const app_path = (remote || electron).app.getAppPath();
|
||||
const result = [];
|
||||
result.push(app_path + "/native/build/" + os.platform() + "_" + os.arch() + "/");
|
||||
if(app_path.endsWith(".asar"))
|
||||
result.push(path.join(path.dirname(app_path), "natives"));
|
||||
return result;
|
||||
})());
|
||||
|
||||
export const handler = require( "teaclient_crash_handler");
|
||||
|
||||
export function initialize_handler(component_name: string, requires_file: boolean) {
|
||||
const start_path = requires_file ? (" " + path.join(__dirname, "..", "..")) : "";
|
||||
const success_arguments = process.argv[0] + start_path + " crash-handler success=1 dump_path=%crash_path%";
|
||||
const error_arguments = process.argv[0] + start_path + " crash-handler success=0 error=%error_message%";
|
||||
|
||||
console.log("Setting up crash handler. Success callback: %s; Error callback: %s", success_arguments, error_arguments);
|
||||
handler.setup_crash_handler(
|
||||
component_name,
|
||||
path.join((remote || electron).app.getPath('userData'), "crash_dumps"),
|
||||
success_arguments,
|
||||
error_arguments
|
||||
);
|
||||
}
|
||||
|
||||
export function finalize_handler() {
|
||||
handler.finalize();
|
||||
}
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 13 KiB |
@ -1,42 +0,0 @@
|
||||
body {
|
||||
background-color: grey;
|
||||
min-width: 750px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container .container-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
}
|
||||
.container .container-header > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.container .container-header img {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
.container .container-header .text {
|
||||
margin-left: 20px;
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
align-self: center;
|
||||
}
|
||||
.container .container-header .text h1 {
|
||||
color: darkred;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.container .container-header .text h2 {
|
||||
font-size: 1.25em;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
.container .error-dump {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=index.css.map */
|
@ -1 +0,0 @@
|
||||
{"version":3,"sourceRoot":"","sources":["index.scss"],"names":[],"mappings":"AAAA;EACC;EACA;EAEA;EACA;EACA;;;AAIA;EACC;EACA;EACA;EACA;;AAEA;EACC;;AAGD;EACC;EACA;EAEA;EACA;;AAGD;EACC;EAEA;EACA;EACA;;AAEA;EACC;EACA;;AAGD;EACC;EACA;;AAKH;EACC","file":"index.css"}
|
@ -1,36 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Application crashed!</title>
|
||||
<link rel="stylesheet" type="text/css" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="container-header">
|
||||
<img src="crash_logo.svg">
|
||||
<div class="text">
|
||||
<h1>Ooops, something went incredible wrong!</h1>
|
||||
<h2>It seems like your TeaSpeak Client has been crashed.</h2>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<b>Attention:</b> Crash reports without a crash dump file will be ignored!
|
||||
</p>
|
||||
<p class="error-hide">
|
||||
Crash dump file: <a href="#" class="crash-dump-directory">undefined</a>
|
||||
</p>
|
||||
<p class="error-dump error-show">
|
||||
Failed to create crash dump file: <a class="crash-dump-error"></a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let exports = {};
|
||||
</script>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,51 +0,0 @@
|
||||
body {
|
||||
background-color: grey;
|
||||
min-width: 750px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
.container-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
|
||||
> * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 20px;
|
||||
|
||||
display: inline-block;;
|
||||
text-align: left;
|
||||
align-self: center;
|
||||
|
||||
h1 {
|
||||
color: darkred;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25em;
|
||||
margin-top: .2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-dump {
|
||||
color: red;
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import { shell, ipcRenderer } from "electron";
|
||||
|
||||
function open_issue_tracker() {
|
||||
shell.openExternal("https://github.com/TeaSpeak/TeaClient/issues");
|
||||
}
|
||||
|
||||
function set_dump_error_flag(flag: boolean) {
|
||||
for(const node of document.getElementsByClassName("error-show") as HTMLCollectionOf<HTMLElement>)
|
||||
node.style.display = flag ? "block" : "none";
|
||||
|
||||
for(const node of document.getElementsByClassName("error-hide") as HTMLCollectionOf<HTMLElement>)
|
||||
node.style.display = flag ? "none" : "block";
|
||||
}
|
||||
|
||||
function set_dump_url(url: string) {
|
||||
for(const crash_path_node of document.getElementsByClassName("crash-dump-directory") as HTMLCollectionOf<HTMLElement>) {
|
||||
crash_path_node.textContent = url;
|
||||
crash_path_node.onclick = () => shell.showItemInFolder(url);
|
||||
}
|
||||
set_dump_error_flag(false);
|
||||
}
|
||||
|
||||
function set_dump_error(error: string) {
|
||||
set_dump_error_flag(true);
|
||||
for(const node of document.getElementsByClassName("crash-dump-error") as HTMLCollectionOf<HTMLElement>)
|
||||
node.textContent = error;
|
||||
}
|
||||
|
||||
ipcRenderer.on('dump-url', (event, url) => set_dump_url(url));
|
||||
ipcRenderer.on('dump-error', (event, error) => set_dump_error(error));
|
@ -1,82 +0,0 @@
|
||||
import * as electron from "electron";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs-extra";
|
||||
|
||||
const APP_DATA = electron.remote.app.getPath("userData");
|
||||
const SETTINGS_DIR = path.join(APP_DATA, "settings");
|
||||
|
||||
let _local_storage: {[key: string]: any} = {};
|
||||
let _local_storage_save: {[key: string]: boolean} = {};
|
||||
export async function initialize() {
|
||||
await fs.mkdirs(SETTINGS_DIR);
|
||||
|
||||
const files = await fs.readdir(SETTINGS_DIR);
|
||||
for(const file of files) {
|
||||
const key = decodeURIComponent(file);
|
||||
console.log("Load settings: %s", key);
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(path.join(SETTINGS_DIR, file));
|
||||
const decoded = JSON.parse(data.toString() || "{}");
|
||||
|
||||
_local_storage[key] = decoded;
|
||||
} catch(error) {
|
||||
console.error("Failed to load settings for %s: %o", key, error);
|
||||
}
|
||||
}
|
||||
|
||||
let _new_storage: Storage = {} as any;
|
||||
|
||||
_new_storage.getItem = key => _local_storage[key] || null;
|
||||
_new_storage.setItem = (key, value) => {
|
||||
_local_storage[key] = value;
|
||||
_local_storage_save[key] = true;
|
||||
save_key(key).catch(error => {
|
||||
console.warn("Failed to save key: %s => %o", key, error);
|
||||
});
|
||||
(_new_storage as any)["length"] = Object.keys(_local_storage).length;
|
||||
};
|
||||
|
||||
_new_storage.clear = () => {
|
||||
_local_storage = {};
|
||||
_local_storage_save = {};
|
||||
|
||||
try {
|
||||
fs.emptyDirSync(SETTINGS_DIR);
|
||||
} catch(error) {
|
||||
console.warn("Failed to empty settings dir");
|
||||
}
|
||||
(_new_storage as any)["length"] = 0;
|
||||
};
|
||||
|
||||
_new_storage.key = index => Object.keys(_local_storage)[index];
|
||||
_new_storage.removeItem = key => {
|
||||
delete _local_storage[key];
|
||||
delete_key(key).catch(error => {
|
||||
console.warn("Failed to delete key on fs: %s => %o", key, error);
|
||||
});
|
||||
(_new_storage as any)["length"] = Object.keys(_local_storage).length;
|
||||
};
|
||||
|
||||
Object.assign(window.localStorage, _new_storage);
|
||||
}
|
||||
|
||||
export async function save_all() {
|
||||
let promises: Promise<void>[] = [];
|
||||
for(const key of Object.keys(_local_storage))
|
||||
promises.push(save_key(key));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
export async function save_key(key: string) {
|
||||
if(!_local_storage_save[key])
|
||||
return;
|
||||
|
||||
_local_storage_save[key] = false;
|
||||
await fs.writeJson(path.join(SETTINGS_DIR, encodeURIComponent(key)), _local_storage[key], {spaces: 0});
|
||||
}
|
||||
|
||||
export async function delete_key(key: string) {
|
||||
delete _local_storage_save[key];
|
||||
await fs.remove(path.join(SETTINGS_DIR, encodeURIComponent(key)));
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
window["require_setup"](module);
|
||||
|
||||
import {audio as naudio} from "teaclient_connection";
|
||||
|
||||
namespace audio.player {
|
||||
export interface Device {
|
||||
device_id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Navigator {
|
||||
mozGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void;
|
||||
webkitGetUserMedia(constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void;
|
||||
}
|
||||
|
||||
let _initialized_callbacks: (() => any)[] = [];
|
||||
export let _initialized = false;
|
||||
export let _audioContext: AudioContext;
|
||||
export let _processor: ScriptProcessorNode;
|
||||
export let _output_stream: naudio.playback.OwnedAudioOutputStream;
|
||||
export let _current_device: naudio.AudioDevice;
|
||||
|
||||
export function initialized() : boolean {
|
||||
return _initialized;
|
||||
}
|
||||
|
||||
|
||||
export function context() : AudioContext {
|
||||
if(!_audioContext) throw "Initialize first!";
|
||||
return _audioContext;
|
||||
}
|
||||
|
||||
export function destination() : AudioNode {
|
||||
if(!_initialized)
|
||||
throw "Audio player hasn't yet be initialized";
|
||||
return _processor || _audioContext.destination;
|
||||
}
|
||||
|
||||
export function on_ready(cb: () => any) {
|
||||
if(_initialized)
|
||||
cb();
|
||||
else
|
||||
_initialized_callbacks.push(cb);
|
||||
}
|
||||
|
||||
export function initialize() {
|
||||
_output_stream = naudio.playback.create_stream();
|
||||
_output_stream.set_buffer_max_latency(0.4);
|
||||
_output_stream.set_buffer_latency(0.02);
|
||||
|
||||
_output_stream.callback_overflow = () => {
|
||||
console.warn("Main audio overflow");
|
||||
_output_stream.clear();
|
||||
};
|
||||
_output_stream.callback_underflow = () => {
|
||||
console.warn("Main audio underflow");
|
||||
};
|
||||
|
||||
_audioContext = new AudioContext();
|
||||
_processor = _audioContext.createScriptProcessor(1024 * 8, _output_stream.channels, _output_stream.channels);
|
||||
|
||||
_processor.onaudioprocess = function(event) {
|
||||
const buffer = event.inputBuffer;
|
||||
//console.log("Received %d channels of %d with a rate of %d", buffer.numberOfChannels, buffer.length, buffer.sampleRate);
|
||||
const target_buffer = new Float32Array(buffer.numberOfChannels * buffer.length);
|
||||
|
||||
for(let channel = 0; channel < buffer.numberOfChannels; channel++) {
|
||||
const channel_data = buffer.getChannelData(channel);
|
||||
target_buffer.set(channel_data, channel * buffer.length);
|
||||
}
|
||||
_output_stream.write_data_rated(target_buffer.buffer, false, buffer.sampleRate);
|
||||
};
|
||||
_processor.connect(_audioContext.destination);
|
||||
|
||||
|
||||
_initialized = true;
|
||||
for(const callback of _initialized_callbacks)
|
||||
callback();
|
||||
_initialized_callbacks = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function available_devices() : Promise<Device[]> {
|
||||
return naudio.available_devices().filter(e => e.output_supported || e.output_default).map(e => {
|
||||
return {
|
||||
device_id: e.device_id,
|
||||
name: e.name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function set_device(device_id?: string) : Promise<void> {
|
||||
const dev = naudio.available_devices().filter(e => e.device_id == device_id);
|
||||
if(dev.length == 0) {
|
||||
console.warn("Missing audio device with is %s", device_id)
|
||||
throw "invalid device id";
|
||||
}
|
||||
|
||||
await naudio.playback.set_device(dev[0].device_index);
|
||||
_current_device = dev[0];
|
||||
}
|
||||
|
||||
export function current_device() : Device {
|
||||
if(_current_device)
|
||||
return _current_device;
|
||||
|
||||
const dev = naudio.available_devices().filter(e => e.output_default);
|
||||
if(dev.length > 0)
|
||||
return dev[0];
|
||||
return {device_id: "default", name: "default"} as Device;
|
||||
}
|
||||
|
||||
export function get_master_volume() : number {
|
||||
return naudio.playback.get_master_volume();
|
||||
}
|
||||
export function set_master_volume(volume: number) {
|
||||
naudio.playback.set_master_volume(volume);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(window["audio"] || (window["audio"] = {} as any), audio);
|
@ -1,510 +0,0 @@
|
||||
window["require_setup"](module);
|
||||
|
||||
import {audio as naudio} from "teaclient_connection";
|
||||
//import {audio, tr} from "../imports/imports_shared";
|
||||
/// <reference types="./imports/import_shared.d.ts" />
|
||||
|
||||
export namespace _audio.recorder {
|
||||
import InputDevice = audio.recorder.InputDevice;
|
||||
import AbstractInput = audio.recorder.AbstractInput;
|
||||
|
||||
interface NativeDevice extends InputDevice {
|
||||
device_index: number;
|
||||
}
|
||||
|
||||
let _device_cache: NativeDevice[] = undefined;
|
||||
export function devices() : InputDevice[] {
|
||||
return _device_cache || (_device_cache = naudio.available_devices().filter(e => e.input_supported || e.input_default).map(e => {
|
||||
return {
|
||||
unique_id: e.device_id,
|
||||
channels: 2, /* TODO */
|
||||
default_input: e.input_default,
|
||||
supported: e.input_supported,
|
||||
name: e.name,
|
||||
driver: e.driver,
|
||||
sample_rate: 44100, /* TODO! */
|
||||
device_index: e.device_index,
|
||||
} as NativeDevice
|
||||
}));
|
||||
}
|
||||
|
||||
export function device_refresh_available() : boolean { return false; }
|
||||
export function refresh_devices() : Promise<void> { throw "not supported yet!"; }
|
||||
|
||||
export function create_input() : AbstractInput {
|
||||
return new NativeInput();
|
||||
}
|
||||
|
||||
namespace filter {
|
||||
export abstract class NativeFilter implements audio.recorder.filter.Filter {
|
||||
type: audio.recorder.filter.Type;
|
||||
handle: NativeInput;
|
||||
enabled: boolean = false;
|
||||
|
||||
protected constructor(handle, type) { this.handle = handle; this.type = type; }
|
||||
|
||||
abstract initialize();
|
||||
abstract finalize();
|
||||
|
||||
|
||||
is_enabled(): boolean { return this.enabled; }
|
||||
}
|
||||
|
||||
export class NThresholdFilter extends NativeFilter implements audio.recorder.filter.ThresholdFilter {
|
||||
private filter: naudio.record.ThresholdConsumeFilter;
|
||||
|
||||
private _margin_frames: number = 6; /* 120ms */
|
||||
private _threshold: number = 50;
|
||||
private _callback_level: any;
|
||||
|
||||
private _attack_smooth = 0;
|
||||
private _release_smooth = 0;
|
||||
|
||||
callback_level: (level: number) => any;
|
||||
|
||||
constructor(handle) {
|
||||
super(handle, audio.recorder.filter.Type.THRESHOLD);
|
||||
|
||||
Object.defineProperty(this, 'callback_level', {
|
||||
get(): any {
|
||||
return this._callback_level;
|
||||
}, set(v: any): void {
|
||||
if(v === this._callback_level)
|
||||
return;
|
||||
|
||||
this._callback_level = v;
|
||||
if(this.filter)
|
||||
this.filter.set_analyze_filter(v);
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
})
|
||||
}
|
||||
|
||||
get_margin_frames(): number {
|
||||
return this.filter ? this.filter.get_margin_frames() : this._margin_frames;
|
||||
}
|
||||
|
||||
get_threshold(): number {
|
||||
return this.filter ? this.filter.get_threshold() : this._threshold;
|
||||
}
|
||||
|
||||
set_margin_frames(value: number) {
|
||||
this._margin_frames = value;
|
||||
if(this.filter)
|
||||
this.filter.set_margin_frames(value);
|
||||
}
|
||||
|
||||
get_attack_smooth(): number {
|
||||
return this.filter ? this.filter.get_attack_smooth() : this._attack_smooth;
|
||||
}
|
||||
|
||||
get_release_smooth(): number {
|
||||
return this.filter ? this.filter.get_release_smooth() : this._release_smooth;
|
||||
}
|
||||
|
||||
set_attack_smooth(value: number) {
|
||||
this._attack_smooth = value;
|
||||
if(this.filter)
|
||||
this.filter.set_attack_smooth(value);
|
||||
}
|
||||
|
||||
set_release_smooth(value: number) {
|
||||
this._release_smooth = value;
|
||||
if(this.filter)
|
||||
this.filter.set_release_smooth(value);
|
||||
}
|
||||
|
||||
set_threshold(value: number): Promise<void> {
|
||||
if(typeof(value) === "string")
|
||||
value = parseInt(value); /* yes... this happens */
|
||||
this._threshold = value;
|
||||
if(this.filter)
|
||||
this.filter.set_threshold(value);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if(this.filter) {
|
||||
if(this.handle.consumer)
|
||||
this.handle.consumer.unregister_filter(this.filter);
|
||||
this.filter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if(!this.handle.consumer)
|
||||
return;
|
||||
|
||||
this.finalize();
|
||||
this.filter = this.handle.consumer.create_filter_threshold(this._threshold);
|
||||
if(this._callback_level)
|
||||
this.filter.set_analyze_filter(this._callback_level);
|
||||
this.filter.set_margin_frames(this._margin_frames);
|
||||
this.filter.set_attack_smooth(this._attack_smooth);
|
||||
this.filter.set_release_smooth(this._release_smooth);
|
||||
}
|
||||
}
|
||||
|
||||
export class NStateFilter extends NativeFilter implements audio.recorder.filter.StateFilter {
|
||||
private filter: naudio.record.StateConsumeFilter;
|
||||
private active = false;
|
||||
|
||||
constructor(handle) {
|
||||
super(handle, audio.recorder.filter.Type.STATE);
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if(this.filter) {
|
||||
if(this.handle.consumer)
|
||||
this.handle.consumer.unregister_filter(this.filter);
|
||||
this.filter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if(!this.handle.consumer)
|
||||
return;
|
||||
|
||||
this.finalize();
|
||||
this.filter = this.handle.consumer.create_filter_state();
|
||||
this.filter.set_consuming(this.active);
|
||||
}
|
||||
|
||||
is_active(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
async set_state(state: boolean): Promise<void> {
|
||||
if(this.active === state)
|
||||
return;
|
||||
this.active = state;
|
||||
if(this.filter)
|
||||
this.filter.set_consuming(state);
|
||||
}
|
||||
}
|
||||
|
||||
export class NVoiceLevelFilter extends NativeFilter implements audio.recorder.filter.VoiceLevelFilter {
|
||||
private filter: naudio.record.VADConsumeFilter;
|
||||
private level = 3;
|
||||
private _margin_frames = 5;
|
||||
|
||||
constructor(handle) {
|
||||
super(handle, audio.recorder.filter.Type.VOICE_LEVEL);
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if(this.filter) {
|
||||
if(this.handle.consumer)
|
||||
this.handle.consumer.unregister_filter(this.filter);
|
||||
this.filter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if(!this.handle.consumer)
|
||||
return;
|
||||
|
||||
this.finalize();
|
||||
this.filter = this.handle.consumer.create_filter_vad(this.level);
|
||||
this.filter.set_margin_frames(this._margin_frames);
|
||||
}
|
||||
|
||||
get_level(): number {
|
||||
return this.level;
|
||||
}
|
||||
|
||||
set_level(value: number) {
|
||||
if(this.level === value)
|
||||
return;
|
||||
|
||||
this.level = value;
|
||||
if(this.filter) {
|
||||
this.finalize();
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
set_margin_frames(value: number) {
|
||||
this._margin_frames = value;
|
||||
if(this.filter)
|
||||
this.filter.set_margin_frames(value);
|
||||
}
|
||||
|
||||
get_margin_frames(): number {
|
||||
return this.filter ? this.filter.get_margin_frames() : this._margin_frames;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NativeInput implements AbstractInput {
|
||||
private handle: naudio.record.AudioRecorder;
|
||||
consumer: naudio.record.AudioConsumer;
|
||||
|
||||
private _current_device: audio.recorder.InputDevice;
|
||||
private _current_state: audio.recorder.InputState = audio.recorder.InputState.PAUSED;
|
||||
|
||||
callback_begin: () => any;
|
||||
callback_end: () => any;
|
||||
|
||||
private filters: filter.NativeFilter[] = [];
|
||||
|
||||
constructor() {
|
||||
this.handle = naudio.record.create_recorder();
|
||||
|
||||
this.consumer = this.handle.create_consumer();
|
||||
this.consumer.callback_ended = () => {
|
||||
if(this._current_state !== audio.recorder.InputState.RECORDING)
|
||||
return;
|
||||
|
||||
this._current_state = audio.recorder.InputState.DRY;
|
||||
if(this.callback_end)
|
||||
this.callback_end();
|
||||
};
|
||||
this.consumer.callback_started = () => {
|
||||
if(this._current_state !== audio.recorder.InputState.DRY)
|
||||
return;
|
||||
|
||||
this._current_state = audio.recorder.InputState.RECORDING;
|
||||
if(this.callback_begin)
|
||||
this.callback_begin();
|
||||
};
|
||||
|
||||
this._current_state = audio.recorder.InputState.PAUSED;
|
||||
}
|
||||
|
||||
/* TODO: some kind of finalize? */
|
||||
current_consumer(): audio.recorder.InputConsumer | undefined {
|
||||
return {
|
||||
type: audio.recorder.InputConsumerType.NATIVE
|
||||
};
|
||||
}
|
||||
|
||||
async set_consumer(consumer: audio.recorder.InputConsumer): Promise<void> {
|
||||
if(typeof(consumer) !== "undefined")
|
||||
throw "we only support native consumers!"; /* TODO: May create a general wrapper? */
|
||||
return;
|
||||
}
|
||||
|
||||
async set_device(_device: audio.recorder.InputDevice | undefined): Promise<void> {
|
||||
if(_device === this._current_device)
|
||||
return;
|
||||
|
||||
const device = _device as NativeDevice; /* TODO: test for? */
|
||||
this._current_device = _device;
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
this.handle.set_device(device ? device.device_index : -1, flag => {
|
||||
if(typeof(flag) === "boolean" && flag)
|
||||
resolve();
|
||||
else
|
||||
reject("failed to set device" + (typeof(flag) === "string" ? (": " + flag) : ""));
|
||||
});
|
||||
});
|
||||
if(!device) return;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
this.handle.start(flag => {
|
||||
if(flag)
|
||||
resolve();
|
||||
else
|
||||
reject("start failed");
|
||||
});
|
||||
});
|
||||
} catch(error) {
|
||||
console.warn(tr("Failed to start playback on new input device (%o)"), error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
current_device(): audio.recorder.InputDevice | undefined {
|
||||
return this._current_device;
|
||||
}
|
||||
|
||||
current_state(): audio.recorder.InputState {
|
||||
return this._current_state;
|
||||
}
|
||||
|
||||
disable_filter(type: audio.recorder.filter.Type) {
|
||||
const filter = this.get_filter(type) as filter.NativeFilter;
|
||||
if(filter.is_enabled())
|
||||
filter.enabled = false;
|
||||
filter.finalize();
|
||||
}
|
||||
|
||||
enable_filter(type: audio.recorder.filter.Type) {
|
||||
const filter = this.get_filter(type) as filter.NativeFilter;
|
||||
if(!filter.is_enabled()) {
|
||||
filter.enabled = true;
|
||||
filter.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
clear_filter() {
|
||||
for(const filter of this.filters) {
|
||||
filter.enabled = false;
|
||||
filter.finalize();
|
||||
}
|
||||
}
|
||||
|
||||
get_filter(type: audio.recorder.filter.Type): audio.recorder.filter.Filter | undefined {
|
||||
for(const filter of this.filters)
|
||||
if(filter.type === type)
|
||||
return filter;
|
||||
|
||||
let _filter: filter.NativeFilter;
|
||||
switch (type) {
|
||||
case audio.recorder.filter.Type.THRESHOLD:
|
||||
_filter = new filter.NThresholdFilter(this);
|
||||
break;
|
||||
case audio.recorder.filter.Type.STATE:
|
||||
_filter = new filter.NStateFilter(this);
|
||||
break;
|
||||
case audio.recorder.filter.Type.VOICE_LEVEL:
|
||||
_filter = new filter.NVoiceLevelFilter(this);
|
||||
break;
|
||||
default:
|
||||
throw "this filter isn't supported!";
|
||||
}
|
||||
this.filters.push(_filter);
|
||||
return _filter;
|
||||
}
|
||||
|
||||
supports_filter(type: audio.recorder.filter.Type) : boolean {
|
||||
switch (type) {
|
||||
case audio.recorder.filter.Type.THRESHOLD:
|
||||
case audio.recorder.filter.Type.STATE:
|
||||
case audio.recorder.filter.Type.VOICE_LEVEL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<audio.recorder.InputStartResult> {
|
||||
try {
|
||||
await this.stop();
|
||||
} catch(error) {
|
||||
console.warn(tr("Failed to stop old record session before start (%o)"), error);
|
||||
}
|
||||
|
||||
this._current_state = audio.recorder.InputState.DRY;
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
this.handle.start(flag => {
|
||||
if(flag)
|
||||
resolve();
|
||||
else
|
||||
reject("start failed");
|
||||
});
|
||||
});
|
||||
for(const filter of this.filters)
|
||||
if(filter.is_enabled())
|
||||
filter.initialize();
|
||||
return audio.recorder.InputStartResult.EOK;
|
||||
} catch(error) {
|
||||
this._current_state = audio.recorder.InputState.PAUSED;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.handle.stop();
|
||||
for(const filter of this.filters)
|
||||
filter.finalize();
|
||||
if(this.callback_end)
|
||||
this.callback_end();
|
||||
this._current_state = audio.recorder.InputState.PAUSED;
|
||||
}
|
||||
|
||||
get_volume(): number {
|
||||
return this.handle.get_volume();
|
||||
}
|
||||
|
||||
set_volume(volume: number) {
|
||||
this.handle.set_volume(volume);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create_levelmeter(device: InputDevice) : Promise<audio.recorder.LevelMeter> {
|
||||
const meter = new NativeLevelmenter(device as any);
|
||||
await meter.initialize();
|
||||
return meter;
|
||||
}
|
||||
|
||||
class NativeLevelmenter implements audio.recorder.LevelMeter {
|
||||
readonly _device: NativeDevice;
|
||||
|
||||
private _callback: (num: number) => any;
|
||||
private _recorder: naudio.record.AudioRecorder;
|
||||
private _consumer: naudio.record.AudioConsumer;
|
||||
private _filter: naudio.record.ThresholdConsumeFilter;
|
||||
|
||||
constructor(device: NativeDevice) {
|
||||
this._device = device;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
this._recorder = naudio.record.create_recorder();
|
||||
this._consumer = this._recorder.create_consumer();
|
||||
|
||||
this._filter = this._consumer.create_filter_threshold(.5);
|
||||
this._filter.set_attack_smooth(.75);
|
||||
this._filter.set_release_smooth(.75);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
this._recorder.set_device(this._device.device_index, flag => {
|
||||
if(typeof(flag) === "boolean" && flag)
|
||||
resolve();
|
||||
else
|
||||
reject("initialize failed" + (typeof(flag) === "string" ? (": " + flag) : ""));
|
||||
});
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
this._recorder.start(flag => {
|
||||
if(flag)
|
||||
resolve();
|
||||
else
|
||||
reject("start failed");
|
||||
});
|
||||
});
|
||||
} catch(error) {
|
||||
if(typeof(error) === "string")
|
||||
throw error;
|
||||
console.warn(tr("Failed to initialize levelmeter for device %o: %o"), this._device, error);
|
||||
throw "initialize failed (lookup console)";
|
||||
}
|
||||
|
||||
/* references this variable, needs a destory() call, else memory leak */
|
||||
this._filter.set_analyze_filter(value => {
|
||||
(this._callback || (() => {}))(value);
|
||||
});
|
||||
}
|
||||
|
||||
destory() {
|
||||
if(this._filter) {
|
||||
this._filter.set_analyze_filter(undefined);
|
||||
this._consumer.unregister_filter(this._filter);
|
||||
}
|
||||
if(this._consumer)
|
||||
this._recorder.delete_consumer(this._consumer);
|
||||
this._recorder.stop();
|
||||
this._recorder.set_device(-1, () => {}); /* -1 := No device */
|
||||
this._recorder = undefined;
|
||||
this._consumer = undefined;
|
||||
this._filter = undefined;
|
||||
}
|
||||
|
||||
device(): audio.recorder.InputDevice {
|
||||
return this._device;
|
||||
}
|
||||
|
||||
set_observer(callback: (value: number) => any) {
|
||||
this._callback = callback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(window["audio"] || (window["audio"] = {} as any), _audio);
|
||||
_audio.recorder.devices(); /* query devices */
|
@ -1,185 +0,0 @@
|
||||
/// <reference path="../imports/imports_shared.d.ts" />
|
||||
|
||||
|
||||
window["require_setup"](module);
|
||||
import * as native from "teaclient_connection";
|
||||
|
||||
namespace _transfer {
|
||||
class NativeFileDownload implements transfer.DownloadTransfer {
|
||||
readonly key: transfer.DownloadKey;
|
||||
private _handle: native.ft.NativeFileTransfer;
|
||||
private _buffer: Uint8Array;
|
||||
|
||||
private _result: Promise<void>;
|
||||
private _response: Response;
|
||||
|
||||
private _result_success: () => any;
|
||||
private _result_error: (error: any) => any;
|
||||
|
||||
constructor(key: transfer.DownloadKey) {
|
||||
this.key = key;
|
||||
this._buffer = new Uint8Array(key.total_size);
|
||||
this._handle = native.ft.spawn_connection({
|
||||
client_transfer_id: key.client_transfer_id,
|
||||
server_transfer_id: key.server_transfer_id,
|
||||
|
||||
remote_address: key.peer.hosts[0],
|
||||
remote_port: key.peer.port,
|
||||
|
||||
transfer_key: key.key,
|
||||
|
||||
object: native.ft.download_transfer_object_from_buffer(this._buffer.buffer)
|
||||
});
|
||||
}
|
||||
|
||||
get_key(): transfer.DownloadKey {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
async request_file(): Promise<Response> {
|
||||
if(this._response)
|
||||
return this._response;
|
||||
|
||||
try {
|
||||
await (this._result || this._start_transfer());
|
||||
} catch(error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if(this._response)
|
||||
return this._response;
|
||||
|
||||
const buffer = this._buffer.buffer.slice(this._buffer.byteOffset, this._buffer.byteOffset + Math.min(64, this._buffer.byteLength));
|
||||
|
||||
/* may another task has been stepped by and already set the response */
|
||||
return this._response || (this._response = new Response(this._buffer, {
|
||||
status: 200,
|
||||
statusText: "success",
|
||||
headers: {
|
||||
"X-media-bytes": base64_encode_ab(buffer)
|
||||
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
_start_transfer() : Promise<void> {
|
||||
return this._result = new Promise((resolve, reject) => {
|
||||
this._result_error = (error) => {
|
||||
this._result_error = undefined;
|
||||
this._result_success = undefined;
|
||||
reject(error);
|
||||
};
|
||||
this._result_success = () => {
|
||||
this._result_error = undefined;
|
||||
this._result_success = undefined;
|
||||
resolve();
|
||||
};
|
||||
|
||||
this._handle.callback_failed = this._result_error;
|
||||
this._handle.callback_finished = aborted => {
|
||||
if(aborted)
|
||||
this._result_error("aborted");
|
||||
else
|
||||
this._result_success();
|
||||
};
|
||||
|
||||
this._handle.start();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class NativeFileUpload implements transfer.UploadTransfer {
|
||||
readonly transfer_key: transfer.UploadKey;
|
||||
private _handle: native.ft.NativeFileTransfer;
|
||||
|
||||
private _result: Promise<void>;
|
||||
|
||||
private _result_success: () => any;
|
||||
private _result_error: (error: any) => any;
|
||||
|
||||
constructor(key: transfer.UploadKey) {
|
||||
this.transfer_key = key;
|
||||
}
|
||||
|
||||
async put_data(data: BlobPart | File) : Promise<void> {
|
||||
if(this._result) {
|
||||
await this._result;
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer: native.ft.FileTransferSource;
|
||||
|
||||
if(data instanceof File) {
|
||||
if(data.size != this.transfer_key.total_size)
|
||||
throw "invalid size";
|
||||
|
||||
buffer = native.ft.upload_transfer_object_from_file(data.path, data.name);
|
||||
} else if(typeof(data) === "string") {
|
||||
if(data.length != this.transfer_key.total_size)
|
||||
throw "invalid size";
|
||||
|
||||
buffer = native.ft.upload_transfer_object_from_buffer(str2ab8(data));
|
||||
} else {
|
||||
let buf = <BufferSource>data;
|
||||
if(buf.byteLength != this.transfer_key.total_size)
|
||||
throw "invalid size";
|
||||
|
||||
if(ArrayBuffer.isView(buf))
|
||||
buf = buf.buffer.slice(buf.byteOffset);
|
||||
|
||||
buffer = native.ft.upload_transfer_object_from_buffer(buf);
|
||||
}
|
||||
|
||||
this._handle = native.ft.spawn_connection({
|
||||
client_transfer_id: this.transfer_key.client_transfer_id,
|
||||
server_transfer_id: this.transfer_key.server_transfer_id,
|
||||
|
||||
remote_address: this.transfer_key.peer.hosts[0],
|
||||
remote_port: this.transfer_key.peer.port,
|
||||
|
||||
transfer_key: this.transfer_key.key,
|
||||
|
||||
object: buffer
|
||||
});
|
||||
|
||||
await (this._result = new Promise((resolve, reject) => {
|
||||
this._result_error = (error) => {
|
||||
this._result_error = undefined;
|
||||
this._result_success = undefined;
|
||||
reject(error);
|
||||
};
|
||||
this._result_success = () => {
|
||||
this._result_error = undefined;
|
||||
this._result_success = undefined;
|
||||
resolve();
|
||||
};
|
||||
|
||||
this._handle.callback_failed = this._result_error;
|
||||
this._handle.callback_finished = aborted => {
|
||||
if(aborted)
|
||||
this._result_error("aborted");
|
||||
else
|
||||
this._result_success();
|
||||
};
|
||||
|
||||
this._handle.start();
|
||||
}));
|
||||
}
|
||||
|
||||
get_key(): transfer.UploadKey {
|
||||
return this.transfer_key;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function spawn_download_transfer(key: transfer.DownloadKey) : transfer.DownloadTransfer {
|
||||
return new NativeFileDownload(key);
|
||||
}
|
||||
|
||||
|
||||
export function spawn_upload_transfer(key: transfer.UploadKey) : transfer.UploadTransfer {
|
||||
return new NativeFileUpload(key);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(window["transfer"] || (window["transfer"] = {} as any), _transfer);
|
@ -1,318 +0,0 @@
|
||||
/// <reference path="../imports/imports_shared.d.ts" />
|
||||
|
||||
window["require_setup"](module);
|
||||
import {
|
||||
destroy_server_connection as _destroy_server_connection,
|
||||
NativeServerConnection,
|
||||
ServerType,
|
||||
spawn_server_connection as _spawn_server_connection
|
||||
} from "teaclient_connection";
|
||||
import {_audio} from "./VoiceConnection";
|
||||
|
||||
export namespace _connection {
|
||||
export namespace native {
|
||||
import VoiceConnection = _audio.native.VoiceConnection;
|
||||
|
||||
class ErrorCommandHandler extends connection.AbstractCommandHandler {
|
||||
private _handle: ServerConnection;
|
||||
|
||||
constructor(handle: ServerConnection) {
|
||||
super(handle);
|
||||
this._handle = handle;
|
||||
}
|
||||
|
||||
handle_command(command: connection.ServerCommand): boolean {
|
||||
if(command.command === "error") {
|
||||
const return_listener: {[key: string]: (result: CommandResult) => any} = this._handle["_return_listener"];
|
||||
const data = command.arguments[0];
|
||||
|
||||
let return_code : string = data["return_code"];
|
||||
if(!return_code) {
|
||||
const listener = return_listener["last_command"] || return_listener["_clientinit"];
|
||||
if(typeof(listener) === "function") {
|
||||
console.warn(tr("Received error without return code. Using last command (%o)"), listener);
|
||||
listener(new CommandResult(data));
|
||||
delete return_listener["last_command"];
|
||||
delete return_listener["_clientinit"];
|
||||
} else {
|
||||
console.warn(tr("Received error without return code."), data);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if(return_listener[return_code]) {
|
||||
return_listener[return_code](new CommandResult(data));
|
||||
} else {
|
||||
console.warn(tr("Error received for no handler! (%o)"), data);
|
||||
}
|
||||
return true;
|
||||
} else if(command.command == "initivexpand") {
|
||||
if(command.arguments[0]["teaspeak"] == true) {
|
||||
console.log("Using TeaSpeak identity type");
|
||||
this._handle.handshake_handler().startHandshake();
|
||||
}
|
||||
return true;
|
||||
} else if(command.command == "initivexpand2") {
|
||||
/* its TeamSpeak or TeaSpeak with experimental 3.1 and not up2date */
|
||||
this._handle["_do_teamspeak"] = true;
|
||||
} else if(command.command == "initserver") {
|
||||
const return_listener: {[key: string]: (result: CommandResult) => any} = this._handle["_return_listener"];
|
||||
|
||||
if(typeof(return_listener["_clientinit"]) === "function") {
|
||||
return_listener["_clientinit"](new CommandResult({id: 0, message: ""}));
|
||||
delete return_listener["_clientinit"];
|
||||
}
|
||||
|
||||
if(this._handle.onconnectionstatechanged)
|
||||
this._handle.onconnectionstatechanged(ConnectionState.INITIALISING, ConnectionState.CONNECTING);
|
||||
} else if(command.command == "notifyconnectioninforequest") {
|
||||
this._handle.send_command("setconnectioninfo",
|
||||
{
|
||||
//TODO calculate
|
||||
connection_ping: 0.0000,
|
||||
connection_ping_deviation: 0.0,
|
||||
|
||||
connection_packets_sent_speech: 0,
|
||||
connection_packets_sent_keepalive: 0,
|
||||
connection_packets_sent_control: 0,
|
||||
connection_bytes_sent_speech: 0,
|
||||
connection_bytes_sent_keepalive: 0,
|
||||
connection_bytes_sent_control: 0,
|
||||
connection_packets_received_speech: 0,
|
||||
connection_packets_received_keepalive: 0,
|
||||
connection_packets_received_control: 0,
|
||||
connection_bytes_received_speech: 0,
|
||||
connection_bytes_received_keepalive: 0,
|
||||
connection_bytes_received_control: 0,
|
||||
connection_server2client_packetloss_speech: 0.0000,
|
||||
connection_server2client_packetloss_keepalive: 0.0000,
|
||||
connection_server2client_packetloss_control: 0.0000,
|
||||
connection_server2client_packetloss_total: 0.0000,
|
||||
connection_bandwidth_sent_last_second_speech: 0,
|
||||
connection_bandwidth_sent_last_second_keepalive: 0,
|
||||
connection_bandwidth_sent_last_second_control: 0,
|
||||
connection_bandwidth_sent_last_minute_speech: 0,
|
||||
connection_bandwidth_sent_last_minute_keepalive: 0,
|
||||
connection_bandwidth_sent_last_minute_control: 0,
|
||||
connection_bandwidth_received_last_second_speech: 0,
|
||||
connection_bandwidth_received_last_second_keepalive: 0,
|
||||
connection_bandwidth_received_last_second_control: 0,
|
||||
connection_bandwidth_received_last_minute_speech: 0,
|
||||
connection_bandwidth_received_last_minute_keepalive: 0,
|
||||
connection_bandwidth_received_last_minute_control: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerConnection extends connection.AbstractServerConnection {
|
||||
private _native_handle: NativeServerConnection;
|
||||
private _voice_connection: VoiceConnection;
|
||||
|
||||
private _do_teamspeak: boolean;
|
||||
private _return_listener: {[key: string]: (result: CommandResult) => any} = {};
|
||||
|
||||
private _command_handler: NativeConnectionCommandBoss;
|
||||
private _command_error_handler: ErrorCommandHandler;
|
||||
private _command_handler_default: connection.ConnectionCommandHandler;
|
||||
|
||||
private _remote_address: ServerAddress;
|
||||
private _handshake_handler: connection.HandshakeHandler;
|
||||
|
||||
private _return_code_index: number = 0;
|
||||
|
||||
onconnectionstatechanged: connection.ConnectionStateListener;
|
||||
|
||||
constructor(props: ConnectionHandler) {
|
||||
super(props);
|
||||
|
||||
this._command_handler = new NativeConnectionCommandBoss(this);
|
||||
this._command_error_handler = new ErrorCommandHandler(this);
|
||||
this._command_handler_default = new connection.ConnectionCommandHandler(this);
|
||||
|
||||
this._command_handler.register_handler(this._command_error_handler);
|
||||
this._command_handler.register_handler(this._command_handler_default);
|
||||
|
||||
this._native_handle = _spawn_server_connection();
|
||||
this._native_handle.callback_disconnect = reason => {
|
||||
this.client.handleDisconnect(DisconnectReason.CONNECTION_CLOSED, {
|
||||
reason: reason,
|
||||
event: event
|
||||
});
|
||||
};
|
||||
this._native_handle.callback_command = (command, args, switches) => {
|
||||
console.log("Received: %o %o %o", command, args, switches);
|
||||
//FIXME catch error
|
||||
|
||||
this._command_handler.invoke_handle({
|
||||
command: command,
|
||||
arguments: args
|
||||
});
|
||||
};
|
||||
this._voice_connection = new VoiceConnection(this, this._native_handle._voice_connection);
|
||||
|
||||
this.command_helper.initialize();
|
||||
this._voice_connection.setup();
|
||||
}
|
||||
|
||||
native_handle() : NativeServerConnection {
|
||||
return this._native_handle;
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if(this._native_handle)
|
||||
_destroy_server_connection(this._native_handle);
|
||||
this._native_handle = undefined;
|
||||
}
|
||||
|
||||
connect(address: ServerAddress, handshake: connection.HandshakeHandler, timeout?: number): Promise<void> {
|
||||
this._remote_address = address;
|
||||
this._handshake_handler = handshake;
|
||||
this._do_teamspeak = false;
|
||||
handshake.setConnection(this);
|
||||
handshake.initialize();
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this._native_handle.connect({
|
||||
remote_host: address.host,
|
||||
remote_port: address.port,
|
||||
|
||||
timeout: typeof(timeout) === "number" ? timeout : -1,
|
||||
|
||||
|
||||
callback: error => {
|
||||
if(error != 0) {
|
||||
/* required to notify the handle, just a promise reject does not work */
|
||||
this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE, error);
|
||||
reject(this._native_handle.error_message(error));
|
||||
return;
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
|
||||
console.log("Remote server type: %o (%s)", this._native_handle.server_type, ServerType[this._native_handle.server_type]);
|
||||
if(this._native_handle.server_type == ServerType.TEAMSPEAK || this._do_teamspeak) {
|
||||
console.log("Trying to use TeamSpeak's identity system");
|
||||
this.handshake_handler().on_teamspeak();
|
||||
}
|
||||
},
|
||||
|
||||
identity_key: (handshake.get_identity_handler() as profiles.identities.TeaSpeakHandshakeHandler).identity.private_key,
|
||||
teamspeak: false
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
remote_address(): ServerAddress {
|
||||
return this._remote_address;
|
||||
}
|
||||
|
||||
handshake_handler(): connection.HandshakeHandler {
|
||||
return this._handshake_handler;
|
||||
}
|
||||
|
||||
connected(): boolean {
|
||||
return typeof(this._native_handle) !== "undefined" && this._native_handle.connected();
|
||||
}
|
||||
|
||||
disconnect(reason?: string): Promise<void> {
|
||||
console.trace("Disconnect: %s",reason);
|
||||
return new Promise<void>((resolve, reject) => this._native_handle.disconnect(reason || "", error => {
|
||||
if(error == 0)
|
||||
resolve();
|
||||
else
|
||||
reject(this._native_handle.error_message(error));
|
||||
}));
|
||||
}
|
||||
|
||||
support_voice(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
voice_connection(): connection.voice.AbstractVoiceConnection {
|
||||
return this._voice_connection;
|
||||
}
|
||||
|
||||
command_handler_boss(): connection.AbstractCommandHandlerBoss {
|
||||
return this._command_handler;
|
||||
}
|
||||
|
||||
private generate_return_code() : string {
|
||||
return (this._return_code_index++).toString();
|
||||
}
|
||||
|
||||
send_command(command: string, data?: any, _options?: connection.CommandOptions): Promise<CommandResult> {
|
||||
if(!this.connected()) {
|
||||
console.warn(tr("Tried to send a command without a valid connection."));
|
||||
return Promise.reject(tr("not connected"));
|
||||
}
|
||||
|
||||
const options: connection.CommandOptions = {};
|
||||
Object.assign(options, connection.CommandOptionDefaults);
|
||||
Object.assign(options, _options);
|
||||
|
||||
data = $.isArray(data) ? data : [data || {}];
|
||||
if(data.length == 0) /* we require min one arg to append return_code */
|
||||
data.push({});
|
||||
|
||||
let return_code = data[0]["return_code"] !== undefined ? data[0].return_code : this.generate_return_code();
|
||||
data[0]["return_code"] = return_code;
|
||||
|
||||
console.log("Sending %s (%o)", command, data);
|
||||
const promise = new Promise<CommandResult>((resolve, reject) => {
|
||||
const timeout_id = setTimeout(() => {
|
||||
delete this._return_listener[return_code];
|
||||
reject("timeout");
|
||||
}, 5000);
|
||||
|
||||
this._return_listener[return_code] = result => {
|
||||
clearTimeout(timeout_id);
|
||||
delete this._return_listener[return_code];
|
||||
|
||||
(result.success ? resolve : reject)(result);
|
||||
};
|
||||
|
||||
if(command == "clientinit")
|
||||
this._return_listener["_clientinit"] = this._return_listener[return_code]; /* fix for TS3 (clientinit does not accept a return code) */
|
||||
|
||||
try {
|
||||
this._native_handle.send_command(command, data, options.flagset || []);
|
||||
} catch(error) {
|
||||
console.warn(tr("Failed to send command: %o"), error);
|
||||
}
|
||||
});
|
||||
return this._command_handler_default.proxy_command_promise(promise, options);
|
||||
}
|
||||
|
||||
ping(): { native: number; javascript?: number } {
|
||||
return {
|
||||
native: this._native_handle ? (this._native_handle.current_ping() / 1000) : -2
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NativeConnectionCommandBoss extends connection.AbstractCommandHandlerBoss {
|
||||
constructor(connection: connection.AbstractServerConnection) {
|
||||
super(connection);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* override the "normal" connection */
|
||||
export function spawn_server_connection(handle: ConnectionHandler) : connection.AbstractServerConnection {
|
||||
console.log("Spawning native connection");
|
||||
return new native.ServerConnection(handle); /* will be overridden by the client */
|
||||
}
|
||||
|
||||
export function destroy_server_connection(handle: connection.AbstractServerConnection) {
|
||||
if(!(handle instanceof native.ServerConnection))
|
||||
throw "invalid handle";
|
||||
//TODO: Here!
|
||||
console.log("Call to destroy a server connection");
|
||||
}
|
||||
}
|
||||
Object.assign(window["connection"] || (window["connection"] = {} as any), _connection);
|
@ -1,161 +0,0 @@
|
||||
import {_connection} from "./ServerConnection";
|
||||
import {_audio as _recorder} from "../audio/AudioRecorder";
|
||||
|
||||
import {
|
||||
NativeVoiceConnection,
|
||||
NativeVoiceClient
|
||||
} from "teaclient_connection";
|
||||
|
||||
export namespace _audio {
|
||||
export namespace native {
|
||||
import ServerConnection = _connection.native.ServerConnection;
|
||||
|
||||
export class VoiceConnection extends connection.voice.AbstractVoiceConnection {
|
||||
readonly connection: ServerConnection;
|
||||
readonly handle: NativeVoiceConnection;
|
||||
|
||||
private _audio_source: RecorderProfile;
|
||||
|
||||
constructor(connection: ServerConnection, voice: NativeVoiceConnection) {
|
||||
super(connection);
|
||||
this.connection = connection;
|
||||
this.handle = voice;
|
||||
}
|
||||
|
||||
setup() { }
|
||||
|
||||
async acquire_voice_recorder(recorder: RecorderProfile | undefined, enforce?: boolean) {
|
||||
if(this._audio_source === recorder && !enforce)
|
||||
return;
|
||||
|
||||
if(this._audio_source)
|
||||
await this._audio_source.unmount();
|
||||
|
||||
if(recorder) {
|
||||
if(!(recorder.input instanceof _recorder.recorder.NativeInput))
|
||||
throw "Recorder input must be an instance of NativeInput!";
|
||||
await recorder.unmount();
|
||||
}
|
||||
|
||||
this.handleVoiceEnded();
|
||||
this._audio_source = recorder;
|
||||
|
||||
if(recorder) {
|
||||
recorder.current_handler = this.connection.client;
|
||||
|
||||
recorder.callback_unmount = () => {
|
||||
this._audio_source = undefined;
|
||||
this.handle.set_audio_source(undefined);
|
||||
this.connection.client.update_voice_status(undefined);
|
||||
};
|
||||
|
||||
recorder.callback_start = this.on_voice_started.bind(this);
|
||||
recorder.callback_stop = this.handleVoiceEnded.bind(this);
|
||||
|
||||
recorder.callback_support_change = () => {
|
||||
this.connection.client.update_voice_status(undefined);
|
||||
};
|
||||
|
||||
this.handle.set_audio_source((recorder.input as _recorder.recorder.NativeInput).consumer);
|
||||
}
|
||||
this.connection.client.update_voice_status(undefined);
|
||||
}
|
||||
|
||||
voice_playback_support() : boolean {
|
||||
return this.connection.connected();
|
||||
}
|
||||
|
||||
voice_send_support() : boolean {
|
||||
return this.connection.connected();
|
||||
}
|
||||
|
||||
private current_channel_codec() : number {
|
||||
const chandler = this.connection.client;
|
||||
return (chandler.getClient().currentChannel() || {properties: { channel_codec: 4}}).properties.channel_codec;
|
||||
}
|
||||
|
||||
private handleVoiceEnded() {
|
||||
const chandler = this.connection.client;
|
||||
chandler.getClient().speaking = false;
|
||||
|
||||
if(!chandler.connected)
|
||||
return false;
|
||||
|
||||
if(chandler.client_status.input_muted)
|
||||
return false;
|
||||
|
||||
console.log(tr("Local voice ended"));
|
||||
//TODO
|
||||
}
|
||||
|
||||
private on_voice_started() {
|
||||
const chandler = this.connection.client;
|
||||
if(chandler.client_status.input_muted) {
|
||||
/* evil hack due to the settings :D */
|
||||
log.warn(LogCategory.VOICE, tr("Received local voice started event, even thou we're muted! Do not send any voice."));
|
||||
if(this.handle) {
|
||||
this.handle.enable_voice_send(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(LogCategory.VOICE, tr("Local voice started"));
|
||||
this.handle.enable_voice_send(true);
|
||||
|
||||
const ch = chandler.getClient();
|
||||
if(ch) ch.speaking = true;
|
||||
}
|
||||
|
||||
connected(): boolean {
|
||||
return true; /* we cant be disconnected at any time! */
|
||||
}
|
||||
|
||||
voice_recorder(): RecorderProfile {
|
||||
return this._audio_source;
|
||||
}
|
||||
|
||||
available_clients(): connection.voice.VoiceClient[] {
|
||||
return this.handle.available_clients();
|
||||
}
|
||||
|
||||
find_client(client_id: number) : connection.voice.VoiceClient | undefined {
|
||||
for(const client of this.available_clients())
|
||||
if(client.client_id === client_id)
|
||||
return client;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
unregister_client(client: connection.voice.VoiceClient): Promise<void> {
|
||||
this.handle.unregister_client(client.client_id);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
register_client(client_id: number): connection.voice.VoiceClient {
|
||||
const client = this.handle.register_client(client_id);
|
||||
if(!client)
|
||||
return client;
|
||||
|
||||
const stream = client.get_stream();
|
||||
stream.set_buffer_latency(0.02);
|
||||
stream.set_buffer_max_latency(0.2);
|
||||
return client;
|
||||
}
|
||||
|
||||
decoding_supported(codec: number): boolean {
|
||||
return this.handle.decoding_supported(codec);
|
||||
}
|
||||
|
||||
encoding_supported(codec: number): boolean {
|
||||
return this.handle.encoding_supported(codec);
|
||||
}
|
||||
|
||||
get_encoder_codec(): number {
|
||||
return this.handle.get_encoder_codec();
|
||||
}
|
||||
|
||||
set_encoder_codec(codec: number) {
|
||||
return this.handle.set_encoder_codec(codec);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
import {class_to_image} from "./icon-helper";
|
||||
|
||||
window["require_setup"](module);
|
||||
|
||||
import * as electron from "electron";
|
||||
const remote = electron.remote;
|
||||
const {Menu, MenuItem} = remote;
|
||||
|
||||
import {isFunction} from "util";
|
||||
|
||||
class ElectronContextMenu implements contextmenu.ContextMenuProvider {
|
||||
private _close_listeners: (() => any)[] = [];
|
||||
private _current_menu: electron.Menu;
|
||||
|
||||
private _div: JQuery;
|
||||
|
||||
despawn_context_menu() {
|
||||
if(!this._current_menu)
|
||||
return;
|
||||
this._current_menu.closePopup();
|
||||
this._current_menu = undefined;
|
||||
|
||||
for(const listener of this._close_listeners) {
|
||||
if(listener) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
this._close_listeners = [];
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if(this._div) this._div.detach();
|
||||
this._div = undefined;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
}
|
||||
|
||||
|
||||
private _entry_id = 0;
|
||||
private build_menu(entry: contextmenu.MenuEntry) : electron.MenuItem {
|
||||
if(entry.type == contextmenu.MenuEntryType.CLOSE) {
|
||||
this._close_listeners.push(entry.callback);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const click_callback = () => {
|
||||
if(entry.callback)
|
||||
entry.callback();
|
||||
this.despawn_context_menu();
|
||||
};
|
||||
const _id = "entry_" + (this._entry_id++);
|
||||
if(entry.type == contextmenu.MenuEntryType.ENTRY) {
|
||||
return new MenuItem({
|
||||
id: _id,
|
||||
label: (isFunction(entry.name) ? (entry.name as (() => string))() : entry.name) as string,
|
||||
type: "normal",
|
||||
click: click_callback,
|
||||
icon: class_to_image(entry.icon_class),
|
||||
visible: entry.visible,
|
||||
enabled: !entry.disabled
|
||||
});
|
||||
} else if(entry.type == contextmenu.MenuEntryType.HR) {
|
||||
if(typeof(entry.visible) === "boolean" && !entry.visible)
|
||||
return undefined;
|
||||
|
||||
return new MenuItem({
|
||||
id: _id,
|
||||
type: "separator",
|
||||
label: '',
|
||||
click: click_callback
|
||||
})
|
||||
} else if(entry.type == contextmenu.MenuEntryType.CHECKBOX) {
|
||||
return new MenuItem({
|
||||
id: _id,
|
||||
label: (isFunction(entry.name) ? (entry.name as (() => string))() : entry.name) as string,
|
||||
type: "checkbox",
|
||||
checked: !!entry.checkbox_checked,
|
||||
click: click_callback,
|
||||
icon: class_to_image(entry.icon_class),
|
||||
visible: entry.visible,
|
||||
enabled: !entry.disabled
|
||||
});
|
||||
} else if (entry.type == contextmenu.MenuEntryType.SUB_MENU) {
|
||||
const sub_menu = new Menu();
|
||||
for(const e of entry.sub_menu) {
|
||||
const build = this.build_menu(e);
|
||||
if(!build)
|
||||
continue;
|
||||
sub_menu.append(build);
|
||||
}
|
||||
return new MenuItem({
|
||||
id: _id,
|
||||
label: (isFunction(entry.name) ? (entry.name as (() => string))() : entry.name) as string,
|
||||
type: "submenu",
|
||||
submenu: sub_menu,
|
||||
click: click_callback,
|
||||
icon: class_to_image(entry.icon_class),
|
||||
visible: entry.visible,
|
||||
enabled: !entry.disabled
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
spawn_context_menu(x: number, y: number, ...entries: contextmenu.MenuEntry[]) {
|
||||
this.despawn_context_menu();
|
||||
|
||||
this._current_menu = new Menu();
|
||||
for(const entry of entries) {
|
||||
const build = this.build_menu(entry);
|
||||
if(!build)
|
||||
continue;
|
||||
this._current_menu.append(build);
|
||||
}
|
||||
|
||||
this._current_menu.popup({
|
||||
window: remote.getCurrentWindow(),
|
||||
x: x,
|
||||
y: y,
|
||||
callback: () => this.despawn_context_menu()
|
||||
});
|
||||
}
|
||||
|
||||
html_format_enabled() { return false; }
|
||||
}
|
||||
|
||||
contextmenu.set_provider(new ElectronContextMenu());
|
||||
|
||||
export {};
|
@ -1,38 +0,0 @@
|
||||
/// <reference path="../imports/imports_shared.d.ts" />
|
||||
|
||||
window["require_setup"](module);
|
||||
import * as dns_handler from "teaclient_dns";
|
||||
|
||||
namespace _dns {
|
||||
export function supported() { return true; }
|
||||
export async function resolve_address(address: ServerAddress, _options?: dns.ResolveOptions) : Promise<dns.AddressTarget> {
|
||||
/* backwards compatibility */
|
||||
if(typeof(address) === "string") {
|
||||
address = {
|
||||
host: address,
|
||||
port: 9987
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<dns.AddressTarget>((resolve, reject) => {
|
||||
dns_handler.resolve_cr(address.host, address.port, result => {
|
||||
if(typeof(result) === "string")
|
||||
reject(result);
|
||||
else
|
||||
resolve({
|
||||
target_ip: result.host,
|
||||
target_port: result.port
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(window["dns"] || (window["dns"] = {} as any), _dns);
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "Native DNS initialized",
|
||||
function: async () => {
|
||||
dns_handler.initialize();
|
||||
},
|
||||
priority: 10
|
||||
});
|
@ -1,63 +0,0 @@
|
||||
import * as electron from "electron";
|
||||
import NativeImage = electron.NativeImage;
|
||||
|
||||
let _div: JQuery;
|
||||
let _icon_mash_url: string;
|
||||
let _icon_mask_img: NativeImage;
|
||||
let _cache_klass_map: {[key: string]: NativeImage};
|
||||
|
||||
export function class_to_image(klass: string) : NativeImage {
|
||||
if(!klass || !_icon_mask_img || !_cache_klass_map)
|
||||
return undefined;
|
||||
|
||||
if(_cache_klass_map[klass])
|
||||
return _cache_klass_map[klass];
|
||||
|
||||
_div[0].classList.value = 'icon ' + klass;
|
||||
const data = window.getComputedStyle(_div[0]);
|
||||
|
||||
const offset_x = parseInt(data.backgroundPositionX.split(",")[0]);
|
||||
const offset_y = parseInt(data.backgroundPositionY.split(",")[0]);
|
||||
|
||||
//http://localhost/home/TeaSpeak/Web-Client/web/environment/development/img/client_icon_sprite.svg
|
||||
//const hight = element.css('height');
|
||||
//const width = element.css('width');
|
||||
console.log("Offset: x: %o y: %o;", offset_x, offset_y);
|
||||
return _cache_klass_map[klass] = _icon_mask_img.crop({
|
||||
height: 16,
|
||||
width: 16,
|
||||
x: offset_x == 0 ? 0 : -offset_x,
|
||||
y: offset_y == 0 ? 0 : -offset_y
|
||||
});
|
||||
}
|
||||
|
||||
export async function initialize() {
|
||||
if(!_div) {
|
||||
_div = $.spawn("div");
|
||||
_div.css('display', 'none');
|
||||
_div.appendTo(document.body);
|
||||
}
|
||||
|
||||
const image = new Image();
|
||||
image.src = 'img/client_icon_sprite.svg';
|
||||
await new Promise((resolve, reject) => {
|
||||
image.onload = resolve;
|
||||
image.onerror = reject;
|
||||
});
|
||||
|
||||
/* TODO: Get a size! */
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 1024;
|
||||
canvas.height = 1024;
|
||||
canvas.getContext("2d").drawImage(image, 0, 0);
|
||||
|
||||
_cache_klass_map = {};
|
||||
_icon_mash_url = canvas.toDataURL();
|
||||
_icon_mask_img = electron.remote.nativeImage.createFromDataURL(_icon_mash_url);
|
||||
}
|
||||
|
||||
export function finalize() {
|
||||
_icon_mask_img = undefined;
|
||||
_icon_mash_url = undefined;
|
||||
_cache_klass_map = undefined;
|
||||
}
|
4677
modules/renderer/imports/.copy_imports_shared.d.ts
vendored
4677
modules/renderer/imports/.copy_imports_shared.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@ -1,97 +0,0 @@
|
||||
|
||||
/* File: /home/wolverindev/TeaSpeak/Web-Client/shared/loader/loader.ts */
|
||||
declare interface Window {
|
||||
tr(message: string): string;
|
||||
}
|
||||
declare namespace loader {
|
||||
export namespace config {
|
||||
export const loader_groups;
|
||||
export const verbose;
|
||||
export const error;
|
||||
}
|
||||
export type Task = {
|
||||
name: string;
|
||||
priority: number; /* tasks with the same priority will be executed in sync */
|
||||
function: () => Promise<void>;
|
||||
};
|
||||
export enum Stage {
|
||||
/*
|
||||
loading loader required files (incl this)
|
||||
*/
|
||||
INITIALIZING,
|
||||
/*
|
||||
setting up the loading process
|
||||
*/
|
||||
SETUP,
|
||||
/*
|
||||
loading all style sheet files
|
||||
*/
|
||||
STYLE,
|
||||
/*
|
||||
loading all javascript files
|
||||
*/
|
||||
JAVASCRIPT,
|
||||
/*
|
||||
loading all template files
|
||||
*/
|
||||
TEMPLATES,
|
||||
/*
|
||||
initializing static/global stuff
|
||||
*/
|
||||
JAVASCRIPT_INITIALIZING,
|
||||
/*
|
||||
finalizing load process
|
||||
*/
|
||||
FINALIZING,
|
||||
/*
|
||||
invoking main task
|
||||
*/
|
||||
LOADED,
|
||||
DONE
|
||||
}
|
||||
export function get_cache_version();
|
||||
export function finished();
|
||||
export function running();
|
||||
export function register_task(stage: Stage, task: Task);
|
||||
export function execute(): Promise<any>;
|
||||
export function execute_managed();
|
||||
export type DependSource = {
|
||||
url: string;
|
||||
depends: string[];
|
||||
};
|
||||
export type SourcePath = string | DependSource | string[];
|
||||
export class SyntaxError {
|
||||
source: any;
|
||||
constructor(source: any);
|
||||
}
|
||||
export function load_script(path: SourcePath): Promise<void>;
|
||||
export function load_scripts(paths: SourcePath[]): Promise<void>;
|
||||
export function load_style(path: SourcePath): Promise<void>;
|
||||
export function load_styles(paths: SourcePath[]): Promise<void>;
|
||||
export type ErrorHandler = (message: string, detail: string) => void;
|
||||
export function critical_error(message: string, detail?: string);
|
||||
export function critical_error_handler(handler?: ErrorHandler, override?: boolean): ErrorHandler;
|
||||
}
|
||||
declare let _fadeout_warned;
|
||||
declare function fadeoutLoader(duration?, minAge?, ignoreAge?);
|
||||
|
||||
/* File: /home/wolverindev/TeaSpeak/Web-Client/shared/loader/app.ts */
|
||||
declare interface Window {
|
||||
$: JQuery;
|
||||
}
|
||||
declare namespace app {
|
||||
export enum Type {
|
||||
UNKNOWN,
|
||||
CLIENT_RELEASE,
|
||||
CLIENT_DEBUG,
|
||||
WEB_DEBUG,
|
||||
WEB_RELEASE
|
||||
}
|
||||
export let type: Type;
|
||||
export function is_web();
|
||||
export function ui_version();
|
||||
}
|
||||
declare const loader_javascript;
|
||||
declare const loader_webassembly;
|
||||
declare const loader_style;
|
||||
declare function load_templates(): Promise<any>;
|
2
modules/renderer/imports/.gitignore
vendored
2
modules/renderer/imports/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
imports_shared.d.ts
|
||||
imports_shared_loader.d.ts
|
@ -1,259 +0,0 @@
|
||||
/// <reference path="imports/imports_shared.d.ts" />
|
||||
|
||||
import {Arguments, process_args, parse_arguments} from "../shared/process-arguments";
|
||||
import {remote} from "electron";
|
||||
import * as crash_handler from "../crash_handler";
|
||||
import * as electron from "electron";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
|
||||
/* first of all setup crash handler */
|
||||
{
|
||||
const is_electron_run = process.argv[0].endsWith("electron") || process.argv[0].endsWith("electron.exe");
|
||||
crash_handler.initialize_handler("renderer", is_electron_run);
|
||||
}
|
||||
|
||||
interface Window {
|
||||
$: any;
|
||||
jQuery: any;
|
||||
jsrender: any;
|
||||
|
||||
impl_display_critical_error: any;
|
||||
displayCriticalError: any;
|
||||
teaclient_initialize: any;
|
||||
|
||||
open_connected_question: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
declare const window: Window;
|
||||
|
||||
export const require_native: NodeRequireFunction = id => require(id);
|
||||
export const initialize = async () => {
|
||||
/* we use out own jquery resource */
|
||||
loader.register_task(loader.Stage.JAVASCRIPT, {
|
||||
name: "teaclient jquery",
|
||||
function: jquery_initialize,
|
||||
priority: 80
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT, {
|
||||
name: "teaclient general",
|
||||
function: load_basic_modules,
|
||||
priority: 10
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "teaclient javascript init",
|
||||
function: load_modules,
|
||||
priority: 50
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "teaclient initialize modules",
|
||||
function: module_loader_setup,
|
||||
priority: 60
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "teaclient initialize persistent storage",
|
||||
function: async () => {
|
||||
const storage = require("./PersistentLocalStorage");
|
||||
await storage.initialize();
|
||||
},
|
||||
priority: 90
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "teaclient initialize logging",
|
||||
function: initialize_logging,
|
||||
priority: 80
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "teaclient initialize error",
|
||||
function: initialize_error_handler,
|
||||
priority: 100
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "teaclient initialize arguments",
|
||||
function: async () => {
|
||||
parse_arguments();
|
||||
if(process_args.has_value(Arguments.DUMMY_CRASH_RENDERER))
|
||||
crash_handler.handler.crash();
|
||||
if(!process_args.has_flag(Arguments.DEBUG)) {
|
||||
window.open_connected_question = () => remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'question',
|
||||
buttons: ['Yes', 'No'],
|
||||
title: 'Confirm',
|
||||
message: 'Are you really sure?\nYou\'re still connected!'
|
||||
}).then(result => result.response === 0);
|
||||
}
|
||||
},
|
||||
priority: 110
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: 'gdb-waiter',
|
||||
function: async () => {
|
||||
if(process_args.has_flag(Arguments.DEV_TOOLS_GDB)) {
|
||||
console.log("Process ID: %d", process.pid);
|
||||
await new Promise(resolve => {
|
||||
console.log("Waiting for continue!");
|
||||
|
||||
const listener = () => {
|
||||
console.log("Continue");
|
||||
document.removeEventListener('click', listener);
|
||||
resolve();
|
||||
};
|
||||
document.addEventListener('click', listener);
|
||||
});
|
||||
}
|
||||
},
|
||||
priority: 100
|
||||
});
|
||||
};
|
||||
|
||||
const jquery_initialize = async () => {
|
||||
window.$ = require("jquery");
|
||||
window.jQuery = window.$;
|
||||
Object.assign(window.$, window.jsrender = require('jsrender'));
|
||||
};
|
||||
|
||||
const initialize_logging = async () => {
|
||||
const logger = require("./logger");
|
||||
logger.setup();
|
||||
};
|
||||
|
||||
const initialize_error_handler = async () => {
|
||||
const _impl = message => {
|
||||
if(!process_args.has_flag(Arguments.DEBUG)) {
|
||||
console.error("Displaying critical error: %o", message);
|
||||
message = message.replace(/<br>/i, "\n");
|
||||
|
||||
const win = remote.getCurrentWindow();
|
||||
remote.dialog.showMessageBox({
|
||||
type: "error",
|
||||
buttons: ["exit"],
|
||||
title: "A critical error happened!",
|
||||
message: message
|
||||
});
|
||||
|
||||
win.close();
|
||||
} else {
|
||||
console.error("Received critical error: %o", message);
|
||||
console.error("Ignoring error due to the debug mode");
|
||||
}
|
||||
};
|
||||
|
||||
if(window.impl_display_critical_error)
|
||||
window.impl_display_critical_error = _impl;
|
||||
else
|
||||
window.displayCriticalError = _impl;
|
||||
};
|
||||
|
||||
const module_loader_setup = async () => {
|
||||
const native_paths = (() => {
|
||||
const app_path = (remote || electron).app.getAppPath();
|
||||
const result = [];
|
||||
result.push(app_path + "/native/build/" + os.platform() + "_" + os.arch() + "/");
|
||||
if(app_path.endsWith(".asar"))
|
||||
result.push(path.join(path.dirname(app_path), "natives"));
|
||||
return result;
|
||||
})();
|
||||
window["require_setup"] = _mod => {
|
||||
if(!_mod || !_mod.paths) return;
|
||||
|
||||
_mod.paths.push(...native_paths);
|
||||
const original_require = _mod.__proto__.require;
|
||||
if(!_mod.proxied) {
|
||||
_mod.require = (path: string) => {
|
||||
if(path.endsWith("imports/imports_shared")) {
|
||||
console.log("Proxy require for %s. Using 'window' as result.", path);
|
||||
return window;
|
||||
}
|
||||
return original_require.apply(_mod, [path]);
|
||||
};
|
||||
_mod.proxied = true;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const load_basic_modules = async () => {
|
||||
console.dir(require("./audio/AudioPlayer")); /* setup audio */
|
||||
console.dir(require("./audio/AudioRecorder")); /* setup audio */
|
||||
require("./logger");
|
||||
|
||||
|
||||
};
|
||||
|
||||
const load_modules = async () => {
|
||||
window["require_setup"](this);
|
||||
|
||||
console.log(module.paths);
|
||||
console.log("Loading native extensions...");
|
||||
try {
|
||||
try {
|
||||
require("./version");
|
||||
} catch(error) {
|
||||
console.error("Failed to load version extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
const helper = require("./icon-helper");
|
||||
await helper.initialize();
|
||||
} catch(error) {
|
||||
console.error("Failed to load the icon helper extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./ppt");
|
||||
} catch(error) {
|
||||
console.error("Failed to load ppt");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./connection/ServerConnection");
|
||||
} catch(error) {
|
||||
console.error("Failed to load server connection extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./connection/FileTransfer");
|
||||
} catch(error) {
|
||||
console.error("Failed to load file transfer extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./dns/dns_resolver");
|
||||
} catch(error) {
|
||||
console.error("Failed to load dns extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./menu");
|
||||
} catch(error) {
|
||||
console.error("Failed to load menu extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
require("./context-menu");
|
||||
} catch(error) {
|
||||
console.error("Failed to load context menu extension");
|
||||
console.dir(error);
|
||||
throw error;
|
||||
}
|
||||
} catch(error){
|
||||
console.log(error);
|
||||
window.displayCriticalError("Failed to load native extensions: " + error);
|
||||
throw error;
|
||||
}
|
||||
console.log("Loaded native extensions");
|
||||
};
|
@ -1,79 +0,0 @@
|
||||
enum LogType {
|
||||
TRACE,
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARNING,
|
||||
ERROR
|
||||
}
|
||||
|
||||
export interface Logger {
|
||||
trace(message: string, ...args);
|
||||
debug(message: string, ...args);
|
||||
info(message: string, ...args);
|
||||
log(message: string, ...args);
|
||||
warning(message: string, ...args);
|
||||
error(message: string, ...args);
|
||||
|
||||
dir_error(error: any, message?: string);
|
||||
}
|
||||
|
||||
let loggers = {};
|
||||
const original_console: Console = {} as any;
|
||||
|
||||
export function setup() {
|
||||
Object.assign(original_console, console);
|
||||
Object.assign(console, logger("console"));
|
||||
}
|
||||
|
||||
export function logger(name: string = "console") : Logger {
|
||||
if(loggers[name])
|
||||
return loggers[name];
|
||||
|
||||
return loggers[name] = create_logger(name);
|
||||
}
|
||||
|
||||
import * as util from "util";
|
||||
function create_logger(name: string) : Logger {
|
||||
const log = (type, message: string, ...args) => {
|
||||
switch (type) {
|
||||
case LogType.TRACE:
|
||||
original_console.trace(message, ...args);
|
||||
break;
|
||||
case LogType.DEBUG:
|
||||
original_console.debug(message, ...args);
|
||||
break;
|
||||
case LogType.INFO:
|
||||
original_console.info(message, ...args);
|
||||
break;
|
||||
case LogType.WARNING:
|
||||
original_console.warn(message, ...args);
|
||||
break;
|
||||
case LogType.ERROR:
|
||||
original_console.error(message, ...args);
|
||||
break;
|
||||
}
|
||||
|
||||
const log_message = util.format(message, ...args);
|
||||
process.stdout.write(util.format("[%s][%s] %s", name, LogType[type], log_message) + "\n");
|
||||
};
|
||||
|
||||
return {
|
||||
trace: (m, ...a) => log(LogType.TRACE, m, ...a),
|
||||
debug: (m, ...a) => log(LogType.DEBUG, m, ...a),
|
||||
info: (m, ...a) => log(LogType.INFO, m, ...a),
|
||||
log: (m, ...a) => log(LogType.INFO, m, ...a),
|
||||
warning: (m, ...a) => log(LogType.WARNING, m, ...a),
|
||||
error: (m, ...a) => log(LogType.ERROR, m, ...a),
|
||||
|
||||
dir_error: (e, m) => {
|
||||
log(LogType.ERROR, "Caught exception: " + m);
|
||||
log(LogType.ERROR, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
(window as any).logger = {
|
||||
log: function (category, level, message) {
|
||||
console.log("%d | %d | %s", category, level, message);
|
||||
}
|
||||
};
|
@ -1,254 +0,0 @@
|
||||
import {class_to_image} from "./icon-helper";
|
||||
|
||||
window["require_setup"](module);
|
||||
|
||||
import * as electron from "electron";
|
||||
//import {top_menu as dtop_menu, Icon} from "./imports/imports_shared";
|
||||
|
||||
/// <reference types="./imports/import_shared.d.ts" />
|
||||
import dtop_menu = top_menu;
|
||||
|
||||
namespace _top_menu {
|
||||
import ipcRenderer = electron.ipcRenderer;
|
||||
namespace native {
|
||||
import ipcRenderer = electron.ipcRenderer;
|
||||
let _item_index = 1;
|
||||
|
||||
abstract class NativeMenuBase {
|
||||
protected _handle: NativeMenuBar;
|
||||
protected _click: () => any;
|
||||
id: string;
|
||||
|
||||
protected constructor(handle: NativeMenuBar, id?: string) {
|
||||
this._handle = handle;
|
||||
this.id = id || ("item_" + (_item_index++));
|
||||
}
|
||||
|
||||
abstract build() : electron.MenuItemConstructorOptions;
|
||||
abstract items(): (dtop_menu.MenuItem | dtop_menu.HRItem)[];
|
||||
|
||||
trigger_click() {
|
||||
if(this._click)
|
||||
this._click();
|
||||
}
|
||||
}
|
||||
|
||||
class NativeMenuItem extends NativeMenuBase implements dtop_menu.MenuItem {
|
||||
private _items: (NativeMenuItem | NativeHrItem)[] = [];
|
||||
private _label: string;
|
||||
private _enabled: boolean = true;
|
||||
private _visible: boolean = true;
|
||||
|
||||
private _icon_data: string;
|
||||
|
||||
constructor(handle: NativeMenuBar) {
|
||||
super(handle);
|
||||
|
||||
}
|
||||
|
||||
append_hr(): dtop_menu.HRItem {
|
||||
const item = new NativeHrItem(this._handle);
|
||||
this._items.push(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
append_item(label: string): dtop_menu.MenuItem {
|
||||
const item = new NativeMenuItem(this._handle);
|
||||
item.label(label);
|
||||
this._items.push(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
click(callback: () => any): this {
|
||||
this._click = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
delete_item(item: dtop_menu.MenuItem | dtop_menu.HRItem) {
|
||||
const i_index = this._items.indexOf(item as any);
|
||||
if(i_index < 0) return;
|
||||
this._items.splice(i_index, 1);
|
||||
}
|
||||
|
||||
disabled(value?: boolean): boolean {
|
||||
if(typeof(value) === "boolean")
|
||||
this._enabled = !value;
|
||||
return !this._enabled;
|
||||
}
|
||||
|
||||
icon(klass?: string | Promise<Icon> | Icon): string {
|
||||
if(typeof(klass) === "string") {
|
||||
const buffer = class_to_image(klass);
|
||||
if(buffer)
|
||||
this._icon_data = buffer.toDataURL();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
items(): (dtop_menu.MenuItem | dtop_menu.HRItem)[] {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
label(value?: string): string {
|
||||
if(typeof(value) === "string")
|
||||
this._label = value;
|
||||
return this._label;
|
||||
}
|
||||
|
||||
visible(value?: boolean): boolean {
|
||||
if(typeof(value) === "boolean")
|
||||
this._visible = value;
|
||||
return this._visible;
|
||||
}
|
||||
|
||||
build(): Electron.MenuItemConstructorOptions {
|
||||
return {
|
||||
id: this.id,
|
||||
|
||||
label: this._label || "",
|
||||
|
||||
submenu: this._items.length > 0 ? this._items.map(e => e.build()) : undefined,
|
||||
enabled: this._enabled,
|
||||
visible: this._visible,
|
||||
|
||||
icon: this._icon_data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NativeHrItem extends NativeMenuBase implements dtop_menu.HRItem {
|
||||
constructor(handle: NativeMenuBar) {
|
||||
super(handle);
|
||||
}
|
||||
|
||||
build(): Electron.MenuItemConstructorOptions {
|
||||
return {
|
||||
type: 'separator',
|
||||
id: this.id
|
||||
}
|
||||
}
|
||||
|
||||
items(): (dtop_menu.MenuItem | dtop_menu.HRItem)[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function is_similar_deep(a, b) {
|
||||
if(typeof(a) !== typeof(b))
|
||||
return false;
|
||||
if(typeof(a) !== "object")
|
||||
return a === b;
|
||||
|
||||
const aProps = Object.keys(a);
|
||||
const bProps = Object.keys(b);
|
||||
|
||||
if (aProps.length != bProps.length)
|
||||
return false;
|
||||
|
||||
for (let i = 0; i < aProps.length; i++) {
|
||||
const propName = aProps[i];
|
||||
|
||||
if(!is_similar_deep(a[propName], b[propName]))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export class NativeMenuBar implements dtop_menu.MenuBarDriver {
|
||||
private static _instance: NativeMenuBar;
|
||||
|
||||
private menu: electron.Menu;
|
||||
private _items: NativeMenuItem[] = [];
|
||||
private _current_menu: electron.MenuItemConstructorOptions[];
|
||||
|
||||
public static instance() : NativeMenuBar {
|
||||
if(!this._instance)
|
||||
this._instance = new NativeMenuBar();
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
append_item(label: string): dtop_menu.MenuItem {
|
||||
const item = new NativeMenuItem(this);
|
||||
item.label(label);
|
||||
this._items.push(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
delete_item(item: dtop_menu.MenuItem) {
|
||||
const i_index = this._items.indexOf(item as any);
|
||||
if(i_index < 0) return;
|
||||
this._items.splice(i_index, 1);
|
||||
}
|
||||
|
||||
flush_changes() {
|
||||
const target_menu = this.build_menu();
|
||||
if(is_similar_deep(target_menu, this._current_menu))
|
||||
return;
|
||||
|
||||
this._current_menu = target_menu;
|
||||
ipcRenderer.send('top-menu', target_menu);
|
||||
}
|
||||
|
||||
private build_menu() : electron.MenuItemConstructorOptions[] {
|
||||
return this._items.map(e => e.build());
|
||||
}
|
||||
|
||||
items(): dtop_menu.MenuItem[] {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.menu = new electron.remote.Menu();
|
||||
ipcRenderer.on('top-menu', (event, clicked_item) => {
|
||||
console.log("Item %o clicked", clicked_item);
|
||||
const check_item = (item: NativeMenuBase) => {
|
||||
if(item.id == clicked_item) {
|
||||
item.trigger_click();
|
||||
return true;
|
||||
}
|
||||
for(const child of item.items())
|
||||
if(check_item(child as NativeMenuBase))
|
||||
return true;
|
||||
};
|
||||
|
||||
for(const item of this._items)
|
||||
if(check_item(item))
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Global variable
|
||||
// @ts-ignore
|
||||
top_menu.set_driver(native.NativeMenuBar.instance());
|
||||
|
||||
|
||||
const call_basic_action = (name: string, ...args: any[]) => ipcRenderer.send('basic-action', name, ...args);
|
||||
top_menu.native_actions = {
|
||||
open_change_log() {
|
||||
call_basic_action("open-changelog");
|
||||
},
|
||||
|
||||
check_native_update() {
|
||||
call_basic_action("check-native-update");
|
||||
},
|
||||
|
||||
quit() {
|
||||
call_basic_action("quit");
|
||||
},
|
||||
|
||||
open_dev_tools() {
|
||||
call_basic_action("open-dev-tools");
|
||||
},
|
||||
|
||||
reload_page() {
|
||||
call_basic_action("reload-window")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export {};
|
@ -1,134 +0,0 @@
|
||||
window["require_setup"](module);
|
||||
|
||||
import {KeyEvent as NKeyEvent} from "teaclient_ppt";
|
||||
namespace _ppt {
|
||||
let key_listener: ((_: ppt.KeyEvent) => any)[] = [];
|
||||
|
||||
let native_ppt;
|
||||
function listener_key(type: ppt.EventType, nevent: NKeyEvent) {
|
||||
if(nevent.key_code === 'VoidSymbol' || nevent.key_code === 'error')
|
||||
nevent.key_code = undefined; /* trigger event for state update */
|
||||
|
||||
let event: ppt.KeyEvent = {
|
||||
type: type,
|
||||
|
||||
key: nevent.key_code,
|
||||
key_code: nevent.key_code,
|
||||
|
||||
key_ctrl: nevent.key_ctrl,
|
||||
key_shift: nevent.key_shift,
|
||||
key_alt: nevent.key_alt,
|
||||
key_windows: nevent.key_windows
|
||||
} as any;
|
||||
|
||||
//console.debug("Trigger key event %o", key_event);
|
||||
|
||||
for (const listener of key_listener)
|
||||
listener(event);
|
||||
}
|
||||
|
||||
function native_keyhook(event: NKeyEvent) {
|
||||
//console.log("Native event!: %o", event);
|
||||
|
||||
if(event.type == 0)
|
||||
listener_key(ppt.EventType.KEY_PRESS, event);
|
||||
else if(event.type == 1)
|
||||
listener_key(ppt.EventType.KEY_RELEASE, event);
|
||||
else if(event.type == 2)
|
||||
listener_key(ppt.EventType.KEY_TYPED, event);
|
||||
}
|
||||
|
||||
export async function initialize() : Promise<void> {
|
||||
native_ppt = require("teaclient_ppt");
|
||||
|
||||
register_key_listener(listener_hook);
|
||||
native_ppt.RegisterCallback(native_keyhook);
|
||||
}
|
||||
|
||||
export function finalize() {
|
||||
unregister_key_listener(listener_hook);
|
||||
native_ppt.UnregisterCallback(native_keyhook);
|
||||
}
|
||||
|
||||
export function register_key_listener(listener: (_: ppt.KeyEvent) => any) {
|
||||
key_listener.push(listener);
|
||||
}
|
||||
|
||||
export function unregister_key_listener(listener: (_: ppt.KeyEvent) => any) {
|
||||
key_listener.remove(listener);
|
||||
}
|
||||
|
||||
interface CurrentState {
|
||||
event: ppt.KeyEvent;
|
||||
code: string;
|
||||
|
||||
special: { [key:number]:boolean };
|
||||
}
|
||||
|
||||
let current_state: CurrentState = {
|
||||
special: []
|
||||
} as any;
|
||||
|
||||
let key_hooks: ppt.KeyHook[] = [];
|
||||
let key_hooks_active: ppt.KeyHook[] = [];
|
||||
|
||||
function listener_hook(event: ppt.KeyEvent) {
|
||||
if(event.type == ppt.EventType.KEY_TYPED)
|
||||
return;
|
||||
|
||||
let old_hooks = [...key_hooks_active];
|
||||
let new_hooks = [];
|
||||
|
||||
current_state.special[ppt.SpecialKey.ALT] = event.key_alt;
|
||||
current_state.special[ppt.SpecialKey.CTRL] = event.key_ctrl;
|
||||
current_state.special[ppt.SpecialKey.SHIFT] = event.key_shift;
|
||||
current_state.special[ppt.SpecialKey.WINDOWS] = event.key_windows;
|
||||
|
||||
current_state.code = undefined;
|
||||
current_state.event = undefined;
|
||||
|
||||
if(event.type == ppt.EventType.KEY_PRESS) {
|
||||
current_state.event = event;
|
||||
current_state.code = event.key_code;
|
||||
|
||||
for(const hook of key_hooks) {
|
||||
if(hook.key_code && hook.key_code != event.key_code) continue;
|
||||
if(hook.key_alt != event.key_alt) continue;
|
||||
if(hook.key_ctrl != event.key_ctrl) continue;
|
||||
if(hook.key_shift != event.key_shift) continue;
|
||||
if(hook.key_windows != event.key_windows) continue;
|
||||
|
||||
new_hooks.push(hook);
|
||||
if(!old_hooks.remove(hook) && hook.callback_press) {
|
||||
hook.callback_press();
|
||||
console.debug("Trigger key press for %o!", hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//We have a new situation
|
||||
for(const hook of old_hooks)
|
||||
if(hook.callback_release) {
|
||||
hook.callback_release();
|
||||
console.debug("Trigger key release for %o!", hook);
|
||||
}
|
||||
key_hooks_active = new_hooks;
|
||||
}
|
||||
|
||||
export function register_key_hook(hook: ppt.KeyHook) {
|
||||
key_hooks.push(hook);
|
||||
}
|
||||
|
||||
export function unregister_key_hook(hook: ppt.KeyHook) {
|
||||
key_hooks.remove(hook);
|
||||
key_hooks_active.remove(hook);
|
||||
}
|
||||
|
||||
export function key_pressed(code: string | ppt.SpecialKey) : boolean {
|
||||
if(typeof(code) === 'string')
|
||||
return current_state.code == code;
|
||||
return current_state.special[code];
|
||||
}
|
||||
}
|
||||
Object.assign(window["ppt"] || (window["ppt"] = {} as any), _ppt);
|
||||
console.dir(_ppt);
|
@ -1,21 +0,0 @@
|
||||
namespace native {
|
||||
const remote = require('electron').remote;
|
||||
export async function client_version() : Promise<string> {
|
||||
const version = remote.getGlobal("app_version_client");
|
||||
return version || "?.?.?";
|
||||
}
|
||||
}
|
||||
|
||||
window["native"] = native;
|
||||
|
||||
namespace forum {
|
||||
export interface UserData {
|
||||
session_id: string;
|
||||
username: string;
|
||||
|
||||
application_data: string;
|
||||
application_data_sign: string;
|
||||
}
|
||||
}
|
||||
|
||||
//window["forum"] = forum;
|
@ -1,99 +0,0 @@
|
||||
import * as electron from "electron";
|
||||
import {app} from "electron";
|
||||
|
||||
export class Arguments {
|
||||
static readonly DEV_TOOLS = ["t", "dev-tools"];
|
||||
static readonly DEV_TOOLS_GDB = ["gdb"];
|
||||
static readonly DEBUG = ["d", "debug"];
|
||||
static readonly DISABLE_ANIMATION = ["a", "disable-animation"];
|
||||
static readonly SERVER_URL = ["u", "server-url"];
|
||||
static readonly UPDATER_UI_DEBUG = ["updater-debug-ui"];
|
||||
static readonly UPDATER_ENFORCE = ["updater-enforce"];
|
||||
static readonly UPDATER_CHANNEL = ["updater-channel"];
|
||||
static readonly UPDATER_LOCAL_VERSION = ["updater-local-version"];
|
||||
static readonly UPDATER_UI_LOAD_TYPE = ["updater-ui-loader_type"];
|
||||
static readonly UPDATER_UI_NO_CACHE = ["updater-ui-no-cache"];
|
||||
static readonly DISABLE_HARDWARE_ACCELERATION = ["disable-hardware-acceleration"];
|
||||
static readonly NO_SINGLE_INSTANCE = ["no-single-instance"];
|
||||
static readonly DUMMY_CRASH_MAIN = ["dummy-crash-main"];
|
||||
static readonly DUMMY_CRASH_RENDERER = ["dummy-crash-renderer"];
|
||||
|
||||
has_flag: (...keys: (string | string[])[]) => boolean;
|
||||
has_value: (...keys: (string | string[])[]) => boolean;
|
||||
value: (...keys: (string | string[])[]) => string;
|
||||
}
|
||||
|
||||
export interface Window {
|
||||
process_args: Arguments;
|
||||
}
|
||||
|
||||
export const process_args: Arguments = {} as Arguments;
|
||||
|
||||
export function parse_arguments() {
|
||||
if(!process || !process.type || process.type === 'renderer') {
|
||||
Object.assign(process_args, electron.remote.getGlobal("process_arguments"));
|
||||
(window as any).process_args = process_args;
|
||||
} else {
|
||||
const is_electron_run = process.argv[0].endsWith("electron") || process.argv[0].endsWith("electron.exe");
|
||||
|
||||
{
|
||||
const minimist: <T> (args, opts) => T = require("./minimist") as any;
|
||||
let args = minimist<Arguments>(is_electron_run ? process.argv.slice(2) : process.argv.slice(1), {
|
||||
boolean: true,
|
||||
stopEarly: true
|
||||
}) as Arguments;
|
||||
args.has_flag = (...keys) => {
|
||||
for(const key of [].concat(...Array.of(...keys).map(e => Array.isArray(e) ? Array.of(...e) : [e])))
|
||||
if(typeof process_args[key as any as string] === "boolean")
|
||||
return process_args[key as any as string];
|
||||
return false;
|
||||
};
|
||||
|
||||
args.value = (...keys) => {
|
||||
for(const key of [].concat(...Array.of(...keys).map(e => Array.isArray(e) ? Array.of(...e) : [e])))
|
||||
if(typeof process_args[key] !== "undefined")
|
||||
return process_args[key];
|
||||
return undefined;
|
||||
};
|
||||
|
||||
args.has_value = (...keys) => {
|
||||
return args.value(...keys) !== undefined;
|
||||
};
|
||||
|
||||
if(args.has_flag(Arguments.DEBUG)) {
|
||||
const _has_flag = args.has_flag;
|
||||
args.has_flag = (...keys) => {
|
||||
const result = _has_flag(...keys);
|
||||
console.log("Process argument test for parameter %o results in %o", keys, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
const _value = args.value;
|
||||
args.value = (...keys) => {
|
||||
const result = _value(...keys);
|
||||
console.log("Process argument test for parameter %o results in %o", keys, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
console.log("Parsed CMD arguments %o as %o", process.argv, args);
|
||||
Object.assign(process_args, args);
|
||||
Object.assign(global["process_arguments"] = {}, args);
|
||||
}
|
||||
|
||||
if(process_args.has_flag("help", "h")) {
|
||||
console.log("TeaClient command line help page");
|
||||
console.log(" -h --help => Displays this page");
|
||||
console.log(" -d --debug => Enabled the application debug");
|
||||
console.log(" -t --dev-tools => Enables dev tools");
|
||||
console.log(" -u --server-url => Sets the remote client api server url");
|
||||
console.log(" --updater-channel => Set the updater channel");
|
||||
console.log(" -a --disable-animation => Disables some cosmetic animations and loadings");
|
||||
console.log(" --disable-hardware-acceleration => Disables the hardware acceleration for the UI");
|
||||
console.log(" --no-single-instance => Disable multi instance testing");
|
||||
|
||||
//is_debug = process_args.has_flag("debug", "d");
|
||||
//open_dev_tools = process_args.has_flag("dev-tools", "dt");
|
||||
app.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,299 +0,0 @@
|
||||
namespace minimist {
|
||||
export interface Opts {
|
||||
/**
|
||||
* A string or array of strings argument names to always treat as strings
|
||||
*/
|
||||
string?: string | string[];
|
||||
|
||||
/**
|
||||
* A boolean, string or array of strings to always treat as booleans. If true will treat
|
||||
* all double hyphenated arguments without equals signs as boolean (e.g. affects `--foo`, not `-f` or `--foo=bar`)
|
||||
*/
|
||||
boolean?: boolean | string | string[];
|
||||
|
||||
/**
|
||||
* An object mapping string names to strings or arrays of string argument names to use as aliases
|
||||
*/
|
||||
alias?: { [key: string]: string | string[] };
|
||||
|
||||
/**
|
||||
* An object mapping string argument names to default values
|
||||
*/
|
||||
default?: { [key: string]: any };
|
||||
|
||||
/**
|
||||
* When true, populate argv._ with everything after the first non-option
|
||||
*/
|
||||
stopEarly?: boolean;
|
||||
|
||||
/**
|
||||
* A function which is invoked with a command line parameter not defined in the opts
|
||||
* configuration object. If the function returns false, the unknown option is not added to argv
|
||||
*/
|
||||
unknown?: (arg: string) => boolean;
|
||||
|
||||
/**
|
||||
* When true, populate argv._ with everything before the -- and argv['--'] with everything after the --.
|
||||
* Note that with -- set, parsing for arguments still stops after the `--`.
|
||||
*/
|
||||
'--'?: boolean;
|
||||
}
|
||||
|
||||
export interface ParsedArgs {
|
||||
[arg: string]: any;
|
||||
|
||||
/**
|
||||
* If opts['--'] is true, populated with everything after the --
|
||||
*/
|
||||
'--'?: string[];
|
||||
|
||||
/**
|
||||
* Contains all the arguments that didn't have an option associated with them
|
||||
*/
|
||||
_: string[];
|
||||
}
|
||||
}
|
||||
|
||||
const parse = (args: string[], options: minimist.Opts) => {
|
||||
options = options || {};
|
||||
|
||||
var flags = { bools : {}, strings : {}, unknownFn: null, allBools: false };
|
||||
|
||||
if (typeof options['unknown'] === 'function') {
|
||||
flags.unknownFn = options['unknown'];
|
||||
}
|
||||
|
||||
if (typeof options['boolean'] === 'boolean' && options['boolean']) {
|
||||
flags.allBools = true;
|
||||
} else {
|
||||
[].concat(options['boolean']).filter(Boolean).forEach(function (key) {
|
||||
flags.bools[key] = true;
|
||||
});
|
||||
}
|
||||
|
||||
var aliases = {};
|
||||
Object.keys(options.alias || {}).forEach(function (key) {
|
||||
aliases[key] = [].concat(options.alias[key]);
|
||||
aliases[key].forEach(function (x) {
|
||||
aliases[x] = [key].concat(aliases[key].filter(function (y) {
|
||||
return x !== y;
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
[].concat(options.string).filter(Boolean).forEach(function (key) {
|
||||
flags.strings[key] = true;
|
||||
if (aliases[key]) {
|
||||
flags.strings[aliases[key]] = true;
|
||||
}
|
||||
});
|
||||
|
||||
var defaults = options['default'] || {};
|
||||
|
||||
var argv = { _ : [] };
|
||||
Object.keys(flags.bools).forEach(function (key) {
|
||||
setArg(key, defaults[key] === undefined ? false : defaults[key]);
|
||||
});
|
||||
|
||||
var notFlags = [];
|
||||
|
||||
if (args.indexOf('--') !== -1) {
|
||||
notFlags = args.slice(args.indexOf('--')+1);
|
||||
args = args.slice(0, args.indexOf('--'));
|
||||
}
|
||||
|
||||
function argDefined(key, arg) {
|
||||
return (flags.allBools && /^--[^=]+$/.test(arg)) ||
|
||||
flags.strings[key] || flags.bools[key] || aliases[key];
|
||||
}
|
||||
|
||||
function setArg (key, val, arg?) {
|
||||
if (arg && flags.unknownFn && !argDefined(key, arg)) {
|
||||
if (flags.unknownFn(arg) === false) return;
|
||||
}
|
||||
|
||||
var value = !flags.strings[key] && isNumber(val)
|
||||
? Number(val) : val
|
||||
;
|
||||
setKey(argv, key.split('.'), value);
|
||||
|
||||
(aliases[key] || []).forEach(function (x) {
|
||||
setKey(argv, x.split('.'), value);
|
||||
});
|
||||
}
|
||||
|
||||
function setKey (obj, keys, value) {
|
||||
var o = obj;
|
||||
keys.slice(0,-1).forEach(function (key) {
|
||||
if (o[key] === undefined) o[key] = {};
|
||||
o = o[key];
|
||||
});
|
||||
|
||||
var key = keys[keys.length - 1];
|
||||
if (o[key] === undefined || flags.bools[key] || typeof o[key] === 'boolean') {
|
||||
o[key] = value;
|
||||
}
|
||||
else if (Array.isArray(o[key])) {
|
||||
o[key].push(value);
|
||||
}
|
||||
else {
|
||||
o[key] = [ o[key], value ];
|
||||
}
|
||||
}
|
||||
|
||||
function aliasIsBoolean(key) {
|
||||
return aliases[key].some(function (x) {
|
||||
return flags.bools[x];
|
||||
});
|
||||
}
|
||||
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
var arg = args[i];
|
||||
|
||||
if (/^--.+=/.test(arg)) {
|
||||
// Using [\s\S] instead of . because js doesn't support the
|
||||
// 'dotall' regex modifier. See:
|
||||
// http://stackoverflow.com/a/1068308/13216
|
||||
var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
|
||||
var key = m[1];
|
||||
var value: any = m[2];
|
||||
if (flags.bools[key]) {
|
||||
value = value !== 'false';
|
||||
}
|
||||
setArg(key, value, arg);
|
||||
}
|
||||
/*
|
||||
else if (/^--no-.+/.test(arg)) {
|
||||
var key = arg.match(/^--no-(.+)/)[1];
|
||||
setArg(key, false, arg);
|
||||
}
|
||||
*/
|
||||
else if (/^--.+/.test(arg)) {
|
||||
var key = arg.match(/^--(.+)/)[1];
|
||||
var next = args[i + 1];
|
||||
if (next !== undefined && !/^-/.test(next)
|
||||
&& !flags.bools[key]
|
||||
&& !flags.allBools
|
||||
&& (aliases[key] ? !aliasIsBoolean(key) : true)) {
|
||||
setArg(key, next, arg);
|
||||
i++;
|
||||
}
|
||||
else if (/^(true|false)$/.test(next)) {
|
||||
setArg(key, next === 'true', arg);
|
||||
i++;
|
||||
}
|
||||
else {
|
||||
setArg(key, flags.strings[key] ? '' : true, arg);
|
||||
}
|
||||
}
|
||||
else if (/^-[^-]+/.test(arg)) {
|
||||
var letters = arg.slice(1,-1).split('');
|
||||
|
||||
var broken = false;
|
||||
for (var j = 0; j < letters.length; j++) {
|
||||
var next = arg.slice(j+2);
|
||||
|
||||
if (next === '-') {
|
||||
setArg(letters[j], next, arg)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/[A-Za-z]/.test(letters[j]) && /=/.test(next)) {
|
||||
setArg(letters[j], next.split('=')[1], arg);
|
||||
broken = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (/[A-Za-z]/.test(letters[j])
|
||||
&& /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
|
||||
setArg(letters[j], next, arg);
|
||||
broken = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (letters[j+1] && letters[j+1].match(/\W/)) {
|
||||
setArg(letters[j], arg.slice(j+2), arg);
|
||||
broken = true;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg);
|
||||
}
|
||||
}
|
||||
|
||||
var key = arg.slice(-1)[0];
|
||||
if (!broken && key !== '-') {
|
||||
if (args[i+1] && !/^(-|--)[^-]/.test(args[i+1])
|
||||
&& !flags.bools[key]
|
||||
&& (aliases[key] ? !aliasIsBoolean(key) : true)) {
|
||||
setArg(key, args[i+1], arg);
|
||||
i++;
|
||||
}
|
||||
else if (args[i+1] && /true|false/.test(args[i+1])) {
|
||||
setArg(key, args[i+1] === 'true', arg);
|
||||
i++;
|
||||
}
|
||||
else {
|
||||
setArg(key, flags.strings[key] ? '' : true, arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
|
||||
argv._.push(
|
||||
flags.strings['_'] || !isNumber(arg) ? arg : Number(arg)
|
||||
);
|
||||
}
|
||||
if (options.stopEarly) {
|
||||
argv._.push.apply(argv._, args.slice(i + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(defaults).forEach(function (key) {
|
||||
if (!hasKey(argv, key.split('.'))) {
|
||||
setKey(argv, key.split('.'), defaults[key]);
|
||||
|
||||
(aliases[key] || []).forEach(function (x) {
|
||||
setKey(argv, x.split('.'), defaults[key]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (options['--']) {
|
||||
argv['--'] = new Array();
|
||||
notFlags.forEach(function(key) {
|
||||
argv['--'].push(key);
|
||||
});
|
||||
}
|
||||
else {
|
||||
notFlags.forEach(function(key) {
|
||||
argv._.push(key);
|
||||
});
|
||||
}
|
||||
|
||||
return argv;
|
||||
};
|
||||
|
||||
function hasKey (obj, keys) {
|
||||
var o = obj;
|
||||
keys.slice(0,-1).forEach(function (key) {
|
||||
o = (o[key] || {});
|
||||
});
|
||||
|
||||
var key = keys[keys.length - 1];
|
||||
return key in o;
|
||||
}
|
||||
|
||||
function isNumber (x) {
|
||||
if (typeof x === 'number') return true;
|
||||
if (/^0x[0-9a-f]+$/i.test(x)) return true;
|
||||
return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x);
|
||||
}
|
||||
|
||||
|
||||
function minimist<T>(args?: string[], opts?: minimist.Opts): T & minimist.ParsedArgs {
|
||||
return parse(args || [], opts) as any;
|
||||
}
|
||||
export = minimist;
|
@ -1,81 +0,0 @@
|
||||
export class Version {
|
||||
major: number = 0;
|
||||
minor: number = 0;
|
||||
patch: number = 0;
|
||||
build: number = 0;
|
||||
timestamp: number = 0;
|
||||
|
||||
constructor(major: number, minor: number, patch: number, build: number, timestamp: number) {
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
this.patch = patch;
|
||||
this.build = build;
|
||||
}
|
||||
|
||||
toString(timestamp: boolean = false) {
|
||||
let result = "";
|
||||
result += this.major + ".";
|
||||
result += this.minor + ".";
|
||||
result += this.patch;
|
||||
if(this.build > 0)
|
||||
result += "-" + this.build;
|
||||
if(timestamp && this.timestamp > 0)
|
||||
result += " [" + this.timestamp + "]";
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
equals(other: Version) : boolean {
|
||||
if(other == this) return true;
|
||||
if(typeof(other) != typeof(this)) return false;
|
||||
|
||||
if(other.major != this.major) return false;
|
||||
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;
|
||||
}
|
||||
|
||||
newer_than(other: Version) : boolean {
|
||||
if(other.major > this.major) return false;
|
||||
else if(other.major < this.major) return true;
|
||||
|
||||
if(other.minor > this.minor) return false;
|
||||
else if(other.minor < this.minor) return true;
|
||||
|
||||
else if(other.patch < this.patch) return true;
|
||||
if(other.patch > this.patch) return false;
|
||||
|
||||
if(other.build > this.build) return false;
|
||||
else if(other.build < this.build) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
in_dev() : 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 {
|
||||
let result: Version = new Version(0, 0, 0, 0, 0);
|
||||
|
||||
const roots = version.split(" ");
|
||||
{
|
||||
const parts = roots[0].split("-");
|
||||
const numbers = parts[0].split(".");
|
||||
|
||||
if(numbers.length > 0) result.major = parseInt(numbers[0]);
|
||||
if(numbers.length > 1) result.minor = parseInt(numbers[1]);
|
||||
if(numbers.length > 2) result.patch = parseInt(numbers[2]);
|
||||
if(parts.length > 1) result.build = parseInt(parts[1]);
|
||||
}
|
||||
if(roots.length > 1 && ((roots[1] = roots[1].trim()).startsWith("[") && roots[1].endsWith("]"))) {
|
||||
result.timestamp = parseInt(roots[1].substr(1, roots[1].length - 2));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"./core",
|
||||
"./crash_handler",
|
||||
"./shared",
|
||||
"../main.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"declarations",
|
||||
"app/dummy-declarations/*.d.ts"
|
||||
]
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"./renderer",
|
||||
"./crash_handler",
|
||||
"./shared/*.ts",
|
||||
"../native/*/exports/"
|
||||
],
|
||||
"exclude": [
|
||||
"./renderer/imports/.copy_*.d.ts"
|
||||
]
|
||||
}
|
3
native/.gitignore
vendored
3
native/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
build/
|
||||
cmake-build-*
|
||||
out/*
|
@ -1,176 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.1)
|
||||
|
||||
project(TeaClient-Natives VERSION 1.0.0)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_VERBOSE_MAKEFILE ON)
|
||||
|
||||
if (CMAKE_INCLUDE_FILE AND NOT CMAKE_INCLUDE_FILE STREQUAL "")
|
||||
message("Include file ${CMAKE_INCLUDE_FILE}")
|
||||
include("${CMAKE_INCLUDE_FILE}")
|
||||
endif ()
|
||||
|
||||
if(CMAKE_PLATFORM_INCLUDE AND NOT CMAKE_PLATFORM_INCLUDE STREQUAL "")
|
||||
message("Include file ${CMAKE_PLATFORM_INCLUDE}")
|
||||
include("${CMAKE_PLATFORM_INCLUDE}")
|
||||
endif()
|
||||
message("Library path: ${LIBRARY_PATH}")
|
||||
message("Module path: ${CMAKE_MODULE_PATH}")
|
||||
|
||||
#Setup NodeJS
|
||||
function(setup_nodejs)
|
||||
set(NodeJS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake/")
|
||||
set(NODEJS_URL "https://atom.io/download/atom-shell")
|
||||
set(NODEJS_VERSION "v6.0.7")
|
||||
#set(NODEJS_VERSION "v8.0.0")
|
||||
|
||||
#set(NODEJS_URL "https://nodejs.org/download/release/")
|
||||
#set(NODEJS_VERSION "v12.7.0")
|
||||
|
||||
find_package(NodeJS REQUIRED)
|
||||
|
||||
set(NODEJS_NAN_DIR "node_modules/nan")
|
||||
nodejs_init()
|
||||
|
||||
#Fix nan include headers
|
||||
set(NAN_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/../node_modules/nan")
|
||||
if(NOT EXISTS "${NAN_INCLUDE_DIR}")
|
||||
message(FATAL_ERROR "Failed to find nan headers.")
|
||||
endif()
|
||||
list(APPEND NODEJS_INCLUDE_DIRS ${NAN_INCLUDE_DIR})
|
||||
|
||||
set(EXE_DIRECTORY "${CMAKE_SOURCE_DIR}/build/exe/" PARENT_SCOPE)
|
||||
if (MSVC)
|
||||
set(NODE_LIB_DIRECTORY "${CMAKE_SOURCE_DIR}/build/win32_x64/" PARENT_SCOPE)
|
||||
set(NODE_PDB_DIRECTORY "${CMAKE_SOURCE_DIR}/build/symbols/" PARENT_SCOPE)
|
||||
else()
|
||||
set(NODE_LIB_DIRECTORY "${CMAKE_SOURCE_DIR}/build/linux_x64/" PARENT_SCOPE)
|
||||
endif()
|
||||
|
||||
#Set some more variables
|
||||
set(NODEJS_INCLUDE_DIRS "${NODEJS_INCLUDE_DIRS}" PARENT_SCOPE)
|
||||
set(NODEJS_INIT ${NODEJS_INIT} PARENT_SCOPE)
|
||||
|
||||
include_directories("dist/ext_nan")
|
||||
|
||||
|
||||
function(add_nodejs_module NAME)
|
||||
message("Registering module ${NAME}")
|
||||
_add_nodejs_module(${NAME} ${ARGN})
|
||||
target_compile_features(${NAME} PUBLIC cxx_std_17)
|
||||
set_target_properties(${NAME}
|
||||
PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${NODE_LIB_DIRECTORY}/"
|
||||
)
|
||||
target_include_directories(${NAME} PUBLIC ${NODEJS_INCLUDE_DIRS})
|
||||
set_target_properties(${NAME} PROPERTIES CXX_STANDARD 17) #Needs to be overridden after _add_nodejs_module sets it to 11
|
||||
|
||||
if(MSVC)
|
||||
add_custom_command(TARGET ${NAME} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E
|
||||
copy "$<TARGET_FILE:${NAME}>" "${NODE_LIB_DIRECTORY}/${NAME}.node"
|
||||
)
|
||||
add_custom_command(TARGET ${NAME} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E
|
||||
copy "$<TARGET_PDB_FILE:${NAME}>" "${NODE_PDB_DIRECTORY}/${NAME}.pdb"
|
||||
)
|
||||
else()
|
||||
set_target_properties(${NAME}
|
||||
PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${NODE_LIB_DIRECTORY}/"
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
#Forward parameters to global scope
|
||||
set(NODEJS_VERSION ${NODEJS_VERSION} PARENT_SCOPE)
|
||||
set(NODEJS_SOURCES ${NODEJS_SOURCES} PARENT_SCOPE)
|
||||
set(NODEJS_INCLUDE_DIRS ${NODEJS_INCLUDE_DIRS} PARENT_SCOPE)
|
||||
set(NODEJS_LIBRARIES ${NODEJS_LIBRARIES} PARENT_SCOPE)
|
||||
set(NODEJS_LINK_FLAGS ${NODEJS_LINK_FLAGS} PARENT_SCOPE)
|
||||
set(NODEJS_DEFINITIONS ${NODEJS_DEFINITIONS} PARENT_SCOPE)
|
||||
|
||||
# Prevents this function from executing more than once
|
||||
set(NODEJS_INIT TRUE PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
#Setup the compiler (Cant be done within a function!)
|
||||
if (MSVC)
|
||||
set(CompilerFlags
|
||||
CMAKE_C_FLAGS_DEBUG
|
||||
CMAKE_C_FLAGS_MINSIZEREL
|
||||
CMAKE_C_FLAGS_RELEASE
|
||||
CMAKE_C_FLAGS_RELWITHDEBINFO
|
||||
CMAKE_CXX_FLAGS_DEBUG
|
||||
CMAKE_CXX_FLAGS_MINSIZEREL
|
||||
CMAKE_CXX_FLAGS_RELEASE
|
||||
CMAKE_CXX_FLAGS_RELWITHDEBINFO
|
||||
)
|
||||
foreach(CompilerFlag ${CompilerFlags})
|
||||
string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}")
|
||||
endforeach()
|
||||
add_compile_options("/EHsc") #We require exception handling
|
||||
else()
|
||||
#This is a bad thing here!
|
||||
function(resolve_library VARIABLE FALLBACK PATHS)
|
||||
set( _PATHS ${PATHS} ${ARGN} ) # Merge them together
|
||||
|
||||
foreach(PATH IN ITEMS ${_PATHS})
|
||||
message(STATUS "Try to use path ${PATH} for ${VARIABLE}")
|
||||
if(EXISTS ${PATH})
|
||||
message(STATUS "Setting ${VARIABLE} to ${PATH}")
|
||||
set(${VARIABLE} ${PATH} PARENT_SCOPE)
|
||||
return()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(FALLBACK)
|
||||
message(WARNING "Failed to resolve library path for ${VARIABLE}. Using default ${VARIABLE}")
|
||||
else()
|
||||
message(FATAL_ERROR "Failed to find requited library. Variable: ${VARIABLE} Paths: ${_PATHS}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -static-libgcc -static-libstdc++")
|
||||
set(CMAKE_SHARED_LIBRARY_CXX_FLAGS "") # Disable -fPIC (We dont want any relonking with build in electron libraries
|
||||
endif()
|
||||
|
||||
setup_nodejs()
|
||||
if(NOT NODEJS_INCLUDE_DIRS OR NODEJS_INCLUDE_DIRS STREQUAL "")
|
||||
message(FATAL_ERROR "Failed to find node headers")
|
||||
else()
|
||||
message("Including NodeJS headers: ${NODEJS_INCLUDE_DIRS}")
|
||||
endif()
|
||||
include_directories(${NODEJS_INCLUDE_DIRS})
|
||||
|
||||
function(build_update_installer)
|
||||
add_subdirectory(updater)
|
||||
endfunction()
|
||||
build_update_installer()
|
||||
|
||||
|
||||
function(build_codec)
|
||||
add_subdirectory(codec)
|
||||
endfunction()
|
||||
#build_codec()
|
||||
|
||||
function(build_ppt)
|
||||
add_subdirectory(ppt)
|
||||
endfunction()
|
||||
build_ppt()
|
||||
|
||||
function(build_connection)
|
||||
add_subdirectory(serverconnection)
|
||||
endfunction()
|
||||
build_connection()
|
||||
|
||||
function(build_crash_handler)
|
||||
add_subdirectory(crash_handler)
|
||||
endfunction()
|
||||
build_crash_handler()
|
||||
|
||||
function(build_dns)
|
||||
add_subdirectory(dns)
|
||||
endfunction()
|
||||
build_dns()
|
File diff suppressed because it is too large
Load Diff
14
native/codec/.gitignore
vendored
14
native/codec/.gitignore
vendored
@ -1,14 +0,0 @@
|
||||
# Default ignored files
|
||||
/.idea/shelf/
|
||||
/.idea/workspace.xml
|
||||
|
||||
# Datasource local storage ignored files
|
||||
/.idea/dataSources/
|
||||
dataSources.local.xml
|
||||
|
||||
# Editor-based HTTP Client requests
|
||||
/.idea/httpRequests/
|
||||
rest-client.private.env.json
|
||||
http-client.private.env.json
|
||||
|
||||
cmake-build-*/
|
@ -1,60 +0,0 @@
|
||||
set(MODULE_NAME "teaclient_codec")
|
||||
set(CMAKE_MODULE_DIR "${CMAKE_SOURCE_DIR}/codec")
|
||||
|
||||
set(SOURCE_FILES
|
||||
binding.cc
|
||||
codec/NativeCodec.cpp
|
||||
codec/OpusCodec.cpp
|
||||
codec/SpeexCodec.cpp
|
||||
codec/CeltCodec.cpp
|
||||
)
|
||||
|
||||
add_nodejs_module(${MODULE_NAME} ${SOURCE_FILES})
|
||||
|
||||
if(MSVC)
|
||||
set(OPUS_LIBRARY_PATH "${CMAKE_MODULE_DIR}/libraries/generated/opus/lib/opus.lib")
|
||||
set(SPEEX_LIBRARY_PATH "${CMAKE_MODULE_DIR}/libraries/generated/speex/lib/libspeex.lib")
|
||||
else()
|
||||
set(OPUS_LIBRARY_PATH "${CMAKE_MODULE_DIR}/libraries/generated/opus/lib/libopus.a")
|
||||
set(SPEEX_LIBRARY_PATH "${CMAKE_MODULE_DIR}/libraries/generated/speex/lib/libspeex.a")
|
||||
set(CELT_LIBRARY_PATH "${CMAKE_MODULE_DIR}/libraries/generated/celt/lib/libcelt0.a")
|
||||
endif()
|
||||
|
||||
#Detect opus
|
||||
if(EXISTS "${CMAKE_MODULE_DIR}/libraries/generated/opus/include" AND EXISTS ${OPUS_LIBRARY_PATH})
|
||||
set(HAVE_OPUS ON)
|
||||
|
||||
add_definitions(-DHAVE_OPUS)
|
||||
include_directories(${CMAKE_MODULE_DIR}/libraries/generated/opus/include)
|
||||
target_link_libraries(${MODULE_NAME} ${OPUS_LIBRARY_PATH})
|
||||
else()
|
||||
message(WARNING "Missing opus libraries. Building without opus support!\n" "Build opus with the build script given within the libraries foulder")
|
||||
endif()
|
||||
|
||||
#Detect speex
|
||||
if(EXISTS "${CMAKE_MODULE_DIR}/libraries/generated/speex/include" AND EXISTS ${SPEEX_LIBRARY_PATH})
|
||||
set(HAVE_SPEEX ON)
|
||||
|
||||
add_definitions(-DHAVE_SPEEX)
|
||||
include_directories(${CMAKE_MODULE_DIR}/libraries/generated/speex/include)
|
||||
target_link_libraries(${MODULE_NAME} ${SPEEX_LIBRARY_PATH})
|
||||
else()
|
||||
message(WARNING "Missing speex libraries. Building without speex support!\n" "Build speex with the build script given within the libraries foulder")
|
||||
endif()
|
||||
|
||||
#Detect celt
|
||||
set(BUILD_CELT OFF)
|
||||
if(EXISTS "${CMAKE_MODULE_DIR}/libraries/generated/celt/include" AND EXISTS "${CELT_LIBRARY_PATH}" AND BUILD_CELT)
|
||||
set(HAVE_CELT ON)
|
||||
|
||||
add_definitions(-DHAVE_CELT)
|
||||
include_directories(${CMAKE_MODULE_DIR}/libraries/generated/celt/include)
|
||||
|
||||
target_link_libraries(${MODULE_NAME} ${CELT_LIBRARY_PATH})
|
||||
else()
|
||||
message(WARNING "Missing celt libraries. Building without celt support!\n" "Build celt with the build script given within the libraries foulder")
|
||||
endif()
|
||||
|
||||
if(HAVE_OPUS AND HAVE_CELT)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wl,-no-whole-archive,--allow-multiple-definition")
|
||||
endif()
|
@ -1,31 +0,0 @@
|
||||
#include <nan.h>
|
||||
#include <node.h>
|
||||
#include <v8.h>
|
||||
#include <iostream>
|
||||
#include "codec/NativeCodec.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
tc::WorkerPool* tc::codec_workers = nullptr;
|
||||
|
||||
NAN_METHOD(finalize) {
|
||||
auto pool = tc::codec_workers;
|
||||
if(!pool) return;
|
||||
|
||||
tc::codec_workers = nullptr;
|
||||
|
||||
pool->finalize();
|
||||
delete pool;
|
||||
}
|
||||
|
||||
NAN_MODULE_INIT(init) {
|
||||
tc::codec_workers = new tc::WorkerPool();
|
||||
tc::codec_workers->initialize();
|
||||
|
||||
Nan::Set(target, Nan::New<v8::String>("new_instance").ToLocalChecked(), Nan::GetFunction(Nan::New<v8::FunctionTemplate>(tc::NativeCodec::NewInstance)).ToLocalChecked());
|
||||
tc::NativeCodec::Init(target);
|
||||
tc::NativeCodec::CodecType::Init(target);
|
||||
}
|
||||
|
||||
NODE_MODULE(MODULE_NAME, init)
|
@ -1,203 +0,0 @@
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include "CeltCodec.h"
|
||||
#include "NativeCodec.h"
|
||||
#include "NanException.h"
|
||||
#include "NanEventCallback.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc;
|
||||
using namespace v8;
|
||||
using namespace Nan;
|
||||
|
||||
bool CeltCodec::supported() {
|
||||
#ifdef HAVE_CELT
|
||||
return true;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef HAVE_CELT
|
||||
|
||||
CeltCodec::CeltCodec(NativeCodec::CodecType::value type) : NativeCodec(type) {
|
||||
cout << "Allocate celt instance" << endl;
|
||||
|
||||
}
|
||||
|
||||
CeltCodec::~CeltCodec() {
|
||||
cout << "Free celt instance" << endl;
|
||||
|
||||
if(this->decoder || this->encoder) {
|
||||
NAN_THROW_EXCEPTION(Error, "please finalize before releasing!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(CeltCodec::initialize) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
//(≥20kHz; sample rates from 8 kHz to 48 kHz)
|
||||
int error = 0;
|
||||
|
||||
int sample_rate = 48000;
|
||||
this->encoder = celt_encoder_create(sample_rate, this->channels, &error);
|
||||
if(!this->encoder || error) {
|
||||
cout << this->encoder << " - " << error << endl;
|
||||
if(this->encoder)
|
||||
celt_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
|
||||
NAN_THROW_EXCEPTION(Error, ("Failed to create encoder (" + to_string(error) + ")").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
this->decoder = celt_decoder_create(sample_rate, this->channels, &error);
|
||||
if(!this->decoder || error) {
|
||||
if(this->decoder)
|
||||
celt_decoder_destroy(this->decoder);
|
||||
this->decoder = nullptr;
|
||||
if(this->encoder)
|
||||
celt_encoder_destroy(this->encoder);
|
||||
|
||||
this->encoder = nullptr;
|
||||
NAN_THROW_EXCEPTION(Error, ("Failed to create decoder (" + to_string(error) + ")").c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(CeltCodec::finalize) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
if(this->encoder) {
|
||||
celt_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
}
|
||||
|
||||
if(this->decoder) {
|
||||
celt_decoder_destroy(this->decoder);
|
||||
this->decoder = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(CeltCodec::encode) {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
if(!info[0]->IsArrayBuffer()) {
|
||||
NAN_THROW_EXCEPTION(Error, "First argument isn't an array buffer!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_buffer = info[0].As<ArrayBuffer>()->GetContents();
|
||||
auto buffer = make_unique<Chunk>(max(js_buffer.ByteLength(), 256UL));
|
||||
buffer->length = js_buffer.ByteLength();
|
||||
memcpy(buffer->memory, js_buffer.Data(), js_buffer.ByteLength());
|
||||
|
||||
auto callback_success = make_unique<Nan::Callback>(info[1].As<Function>());
|
||||
auto callback_error = make_unique<Nan::Callback>(info[2].As<Function>());
|
||||
|
||||
auto codec = make_unique<v8::Persistent<Object>>(info.GetIsolate(), info.Holder());
|
||||
auto callback = Nan::async_callback([callback_success = move(callback_success), callback_error = move(callback_error), codec = move(codec)](std::unique_ptr<Chunk> result, std::string error) {
|
||||
Nan::HandleScope scope;
|
||||
if(result) {
|
||||
auto _buffer = v8::ArrayBuffer::New(v8::Isolate::GetCurrent(), result->length);
|
||||
memcpy(_buffer->GetContents().Data(), result->memory, result->length);
|
||||
|
||||
Local<Value> argv[] = { _buffer }; //_buffer
|
||||
callback_success->Call(1, argv);
|
||||
} else {
|
||||
Local<Value> argv[] = { Nan::New<String>(error).ToLocalChecked() }; //error
|
||||
callback_error->Call(1, argv);
|
||||
}
|
||||
codec->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
auto frame_size = buffer->length / sizeof(float) / this->channels;
|
||||
if(frame_size != 64 && frame_size != 128 && frame_size != 256 && frame_size != 512) {
|
||||
NAN_THROW_EXCEPTION(Error, ("Invalid frame size (" + to_string(frame_size) + "). Only allow 64, 128, 256, 512bytes").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
tc::codec_workers->enqueue_task(
|
||||
[this, callback, buffer = move(buffer)]() mutable {
|
||||
{
|
||||
lock_guard lock(this->coder_lock);
|
||||
if(!this->encoder) {
|
||||
callback(nullptr, "Please initialize first!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto nbytes = celt_encode_float(this->encoder, (float*) buffer->memory, buffer->length / sizeof(float) / this->channels, (u_char*) buffer->memory, buffer->allocated_length);
|
||||
if(nbytes < 0) {
|
||||
callback(nullptr, "Invalid encode return code (" + to_string(nbytes) + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
buffer->length = nbytes;
|
||||
}
|
||||
callback(move(buffer), "");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
NAN_METHOD(CeltCodec::decode) {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
if(!info[0]->IsArrayBuffer()) {
|
||||
NAN_THROW_EXCEPTION(Error, "First argument isn't an array buffer!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_buffer = info[0].As<ArrayBuffer>()->GetContents();
|
||||
auto buffer = make_unique<Chunk>(max(js_buffer.ByteLength(), this->max_frame_size * this->channels * sizeof(float)));
|
||||
buffer->length = js_buffer.ByteLength();
|
||||
memcpy(buffer->memory, js_buffer.Data(), js_buffer.ByteLength());
|
||||
|
||||
auto callback_success = make_unique<Nan::Callback>(info[1].As<Function>());
|
||||
auto callback_error = make_unique<Nan::Callback>(info[2].As<Function>());
|
||||
|
||||
auto codec = make_unique<v8::Persistent<Object>>(info.GetIsolate(), info.Holder());
|
||||
auto callback = Nan::async_callback([callback_success = move(callback_success), callback_error = move(callback_error), codec = move(codec)](std::unique_ptr<Chunk> result, std::string error) {
|
||||
Nan::HandleScope scope;
|
||||
if(result) {
|
||||
auto _buffer = v8::ArrayBuffer::New(v8::Isolate::GetCurrent(), result->length);
|
||||
memcpy(_buffer->GetContents().Data(), result->memory, result->length);
|
||||
Local<Value> argv[] = { _buffer };
|
||||
callback_success->Call(1, argv);
|
||||
} else {
|
||||
Local<Value> argv[] = { Nan::New<String>(error).ToLocalChecked() };
|
||||
callback_error->Call(1, argv);
|
||||
}
|
||||
codec->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
tc::codec_workers->enqueue_task(
|
||||
[this, callback, buffer = move(buffer)]() mutable {
|
||||
int result;
|
||||
|
||||
{
|
||||
lock_guard lock(this->coder_lock);
|
||||
if(!this->decoder) {
|
||||
callback(nullptr, "Please initialize first!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto code = celt_decode_float(this->decoder, (u_char*) buffer->memory, buffer->length, (float*) buffer->memory, buffer->allocated_length / sizeof(float) / this->channels);
|
||||
if(code < 0) {
|
||||
callback(nullptr, "Invalid decode return code (" + to_string(code) + ")");
|
||||
return;
|
||||
}
|
||||
cout << code << endl;
|
||||
|
||||
buffer->length = this->channels * this->max_frame_size * sizeof(float);
|
||||
}
|
||||
|
||||
callback(move(buffer), "");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#endif
|
@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "NativeCodec.h"
|
||||
|
||||
#ifdef HAVE_CELT
|
||||
#include <celt/celt.h>
|
||||
#endif
|
||||
|
||||
namespace tc {
|
||||
class NativeCodec;
|
||||
|
||||
|
||||
class CeltCodec : public NativeCodec {
|
||||
public:
|
||||
static bool supported();
|
||||
|
||||
#ifdef HAVE_CELT
|
||||
explicit CeltCodec(CodecType::value type);
|
||||
virtual ~CeltCodec();
|
||||
|
||||
virtual NAN_METHOD(initialize);
|
||||
virtual NAN_METHOD(finalize);
|
||||
virtual NAN_METHOD(encode);
|
||||
virtual NAN_METHOD(decode);
|
||||
private:
|
||||
std::mutex coder_lock;
|
||||
int max_frame_size = 512;
|
||||
int channels = 1; /* fixed */
|
||||
|
||||
CELTEncoder* encoder = nullptr;
|
||||
CELTDecoder* decoder = nullptr;
|
||||
#endif
|
||||
};
|
||||
}
|
@ -1,205 +0,0 @@
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include "NativeCodec.h"
|
||||
#include "OpusCodec.h"
|
||||
#include "SpeexCodec.h"
|
||||
#include "CeltCodec.h"
|
||||
#include "NanException.h"
|
||||
#include <iostream>
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc;
|
||||
using namespace v8;
|
||||
using namespace Nan;
|
||||
|
||||
#define DEFINE_ENUM_ENTRY(name, value) \
|
||||
Nan::ForceSet(types, Nan::New<v8::String>(name).ToLocalChecked(), Nan::New<v8::Number>(value), static_cast<PropertyAttribute>(ReadOnly|DontDelete)); \
|
||||
//Nan::ForceSet(types, Nan::New<v8::Number>(value), Nan::New<v8::String>(name).ToLocalChecked(), static_cast<PropertyAttribute>(ReadOnly|DontDelete));
|
||||
|
||||
NAN_MODULE_INIT(NativeCodec::CodecType::Init) {
|
||||
auto types = Nan::New<v8::Object>();
|
||||
|
||||
DEFINE_ENUM_ENTRY("OPUS_VOICE", CodecType::OPUS_VOICE);
|
||||
DEFINE_ENUM_ENTRY("OPUS_MUSIC", CodecType::OPUS_MUSIC);
|
||||
DEFINE_ENUM_ENTRY("SPEEX_NARROWBAND", CodecType::SPEEX_NARROWBAND);
|
||||
DEFINE_ENUM_ENTRY("SPEEX_WIDEBAND", CodecType::SPEEX_WIDEBAND);
|
||||
DEFINE_ENUM_ENTRY("SPEEX_ULTRA_WIDEBAND", CodecType::SPEEX_ULTRA_WIDEBAND);
|
||||
DEFINE_ENUM_ENTRY("CELT_MONO", CodecType::CELT_MONO);
|
||||
|
||||
Nan::Set(target, Nan::New<v8::String>("CodecTypes").ToLocalChecked(), types);
|
||||
Nan::Set(target, Nan::New<v8::String>("codec_supported").ToLocalChecked(), Nan::New<v8::Function>(NativeCodec::CodecType::supported));
|
||||
}
|
||||
|
||||
NAN_METHOD(NativeCodec::CodecType::supported) {
|
||||
if(!info[0]->IsNumber()) {
|
||||
NAN_THROW_EXCEPTION(Error, "Argument 0 shall be a number!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto type = (CodecType::value) Nan::To<int>(info[0]).FromJust();
|
||||
if(type == CodecType::OPUS_MUSIC || type == CodecType::OPUS_VOICE) {
|
||||
info.GetReturnValue().Set(OpusCodec::supported());
|
||||
} else if(type == CodecType::SPEEX_NARROWBAND || type == CodecType::SPEEX_WIDEBAND || type == CodecType::SPEEX_ULTRA_WIDEBAND) {
|
||||
info.GetReturnValue().Set(SpeexCodec::supported());
|
||||
} else if(type == CodecType::CELT_MONO) {
|
||||
info.GetReturnValue().Set(CeltCodec::supported());
|
||||
} else {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid type!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_MODULE_INIT(NativeCodec::Init) {
|
||||
auto klass = New<FunctionTemplate>(NewInstance);
|
||||
klass->SetClassName(Nan::New("NativeCodec").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "decode", NativeCodec::function_decode);
|
||||
Nan::SetPrototypeMethod(klass, "encode", NativeCodec::function_encode);
|
||||
Nan::SetPrototypeMethod(klass, "initialize", NativeCodec::function_initialize);
|
||||
Nan::SetPrototypeMethod(klass, "finalize", NativeCodec::function_finalize);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
//Nan::Set(target, Nan::New("NativeCodec").ToLocalChecked(), Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(NativeCodec::NewInstance) {
|
||||
if (info.IsConstructCall()) {
|
||||
if(!info[0]->IsNumber()) {
|
||||
NAN_THROW_EXCEPTION(Error, "Argument 0 shall be a number!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto type = (CodecType::value) Nan::To<int>(info[0]).FromJust();
|
||||
std::unique_ptr<NativeCodec> instance;
|
||||
if(type == CodecType::OPUS_MUSIC || type == CodecType::OPUS_VOICE) {
|
||||
#ifdef HAVE_OPUS
|
||||
instance = make_unique<OpusCodec>(type);
|
||||
#endif
|
||||
} else if(type == CodecType::SPEEX_NARROWBAND || type == CodecType::SPEEX_WIDEBAND || type == CodecType::SPEEX_ULTRA_WIDEBAND) {
|
||||
#ifdef HAVE_SPEEX
|
||||
instance = make_unique<SpeexCodec>(type);
|
||||
#endif
|
||||
} else if(type == CodecType::CELT_MONO) {
|
||||
#ifdef HAVE_CELT
|
||||
instance = make_unique<CeltCodec>(type);
|
||||
#endif
|
||||
} else {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid type!");
|
||||
return;
|
||||
}
|
||||
if(!instance) {
|
||||
NAN_THROW_EXCEPTION(Error, "Target codec isn't supported!");
|
||||
return;
|
||||
}
|
||||
instance.release()->Wrap(info.This());
|
||||
|
||||
Nan::Set(info.This(), New<String>("type").ToLocalChecked(), New<Number>(type));
|
||||
info.GetReturnValue().Set(info.This());
|
||||
} else {
|
||||
const int argc = 1;
|
||||
v8::Local<v8::Value> argv[argc] = {info[0]};
|
||||
v8::Local<v8::Function> cons = Nan::New(constructor());
|
||||
|
||||
Nan::TryCatch try_catch;
|
||||
auto result = Nan::NewInstance(cons, argc, argv);
|
||||
if(try_catch.HasCaught()) {
|
||||
try_catch.ReThrow();
|
||||
return;
|
||||
}
|
||||
info.GetReturnValue().Set(result.ToLocalChecked());
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(NativeCodec::function_decode) {
|
||||
if(info.Length() != 3) {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid argument count!");
|
||||
return;
|
||||
}
|
||||
if(!info[0]->IsArrayBuffer() || !info[1]->IsFunction() || !info[2]->IsFunction()) {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid argument types!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto codec = ObjectWrap::Unwrap<NativeCodec>(info.Holder());
|
||||
codec->decode(info);
|
||||
}
|
||||
|
||||
NAN_METHOD(NativeCodec::function_encode) {
|
||||
if(info.Length() != 3) {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid argument count!");
|
||||
return;
|
||||
}
|
||||
if(!info[0]->IsArrayBuffer() || !info[1]->IsFunction() || !info[2]->IsFunction()) {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid argument types!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto codec = ObjectWrap::Unwrap<NativeCodec>(info.Holder());
|
||||
codec->encode(info);
|
||||
}
|
||||
|
||||
NAN_METHOD(NativeCodec::function_initialize) {
|
||||
auto codec = ObjectWrap::Unwrap<NativeCodec>(info.Holder());
|
||||
codec->initialize(info);
|
||||
}
|
||||
NAN_METHOD(NativeCodec::function_finalize) {
|
||||
auto codec = ObjectWrap::Unwrap<NativeCodec>(info.Holder());
|
||||
codec->finalize(info);
|
||||
}
|
||||
|
||||
NativeCodec::NativeCodec(tc::NativeCodec::CodecType::value type) : type(type) {}
|
||||
NativeCodec::~NativeCodec() {}
|
||||
|
||||
|
||||
WorkerPool::WorkerPool() {}
|
||||
WorkerPool::~WorkerPool() {}
|
||||
|
||||
void WorkerPool::initialize() {
|
||||
assert(!this->_running);
|
||||
|
||||
this->_running = true;
|
||||
this->worker = thread([&]{
|
||||
while(this->_running) {
|
||||
function<void()> worker;
|
||||
{
|
||||
unique_lock lock(this->worker_lock);
|
||||
this->worker_wait.wait_for(lock, minutes(1), [&]{ return !this->_running || !this->tasks.empty(); });
|
||||
if(!this->_running) break;
|
||||
if(this->tasks.empty()) continue;
|
||||
|
||||
worker = move(this->tasks.front());
|
||||
this->tasks.pop_front();
|
||||
}
|
||||
|
||||
try {
|
||||
worker();
|
||||
} catch(std::exception& ex) {
|
||||
cerr << "failed to invoke opus worker! message: " << ex.what() << endl;
|
||||
} catch (...) {
|
||||
cerr << "failed to invoke opus worker!" << endl;
|
||||
}
|
||||
}
|
||||
});
|
||||
#ifndef WIN32
|
||||
auto worker_handle = this->worker.native_handle();
|
||||
pthread_setname_np(worker_handle, "Codec Worker");
|
||||
#endif
|
||||
}
|
||||
|
||||
void WorkerPool::finalize() {
|
||||
this->_running = false;
|
||||
this->worker_wait.notify_all();
|
||||
this->tasks.clear();
|
||||
|
||||
if(this->worker.joinable())
|
||||
this->worker.join();
|
||||
}
|
||||
|
||||
void WorkerPool::enqueue_task(std::function<void()> task) {
|
||||
lock_guard lock(this->worker_lock);
|
||||
this->tasks.push_back(std::move(task));
|
||||
this->worker_wait.notify_one();
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <nan.h>
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <condition_variable>
|
||||
|
||||
namespace tc {
|
||||
struct WorkerPool {
|
||||
public:
|
||||
WorkerPool();
|
||||
virtual ~WorkerPool();
|
||||
|
||||
void initialize();
|
||||
void finalize();
|
||||
|
||||
void enqueue_task(std::function<void()> /* function */);
|
||||
|
||||
template <typename T>
|
||||
void enqueue_task(T&& closure) {
|
||||
auto handle = std::make_shared<T>(std::forward<T>(closure));
|
||||
this->enqueue_task(std::function<void()>([handle]{ (*handle)(); }));
|
||||
}
|
||||
|
||||
private:
|
||||
bool _running = false;
|
||||
|
||||
std::thread worker;
|
||||
std::mutex worker_lock;
|
||||
std::condition_variable worker_wait;
|
||||
|
||||
std::deque<std::function<void()>> tasks;
|
||||
};
|
||||
extern WorkerPool* codec_workers;
|
||||
|
||||
struct Chunk {
|
||||
char* memory;
|
||||
size_t length;
|
||||
size_t allocated_length;
|
||||
|
||||
Chunk(size_t length) {
|
||||
memory = (char*) malloc(length);
|
||||
this->allocated_length = length;
|
||||
this->length = 0;
|
||||
}
|
||||
|
||||
~Chunk() {
|
||||
free(memory);
|
||||
}
|
||||
};
|
||||
|
||||
class NativeCodec : public Nan::ObjectWrap {
|
||||
public:
|
||||
struct CodecType {
|
||||
enum value {
|
||||
SPEEX_NARROWBAND,
|
||||
SPEEX_WIDEBAND,
|
||||
SPEEX_ULTRA_WIDEBAND,
|
||||
CELT_MONO,
|
||||
OPUS_VOICE,
|
||||
OPUS_MUSIC
|
||||
};
|
||||
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(supported);
|
||||
};
|
||||
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
|
||||
explicit NativeCodec(CodecType::value type);
|
||||
virtual ~NativeCodec();
|
||||
|
||||
virtual NAN_METHOD(initialize) = 0;
|
||||
virtual NAN_METHOD(finalize) = 0;
|
||||
|
||||
virtual NAN_METHOD(encode) = 0;
|
||||
virtual NAN_METHOD(decode) = 0;
|
||||
|
||||
static NAN_METHOD(function_encode);
|
||||
static NAN_METHOD(function_decode);
|
||||
static NAN_METHOD(function_initialize);
|
||||
static NAN_METHOD(function_finalize);
|
||||
|
||||
protected:
|
||||
CodecType::value type;
|
||||
|
||||
};
|
||||
}
|
@ -1,204 +0,0 @@
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include "OpusCodec.h"
|
||||
#include "NativeCodec.h"
|
||||
#include "NanException.h"
|
||||
#include "NanEventCallback.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc;
|
||||
using namespace v8;
|
||||
using namespace Nan;
|
||||
|
||||
bool OpusCodec::supported() {
|
||||
#ifdef HAVE_OPUS
|
||||
return true;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef HAVE_OPUS
|
||||
OpusCodec::OpusCodec(NativeCodec::CodecType::value type) : NativeCodec(type) {
|
||||
cout << "New opus instance" << endl;
|
||||
|
||||
this->sampling_rate = 48000;
|
||||
this->frames = 960;
|
||||
if(type == CodecType::OPUS_MUSIC) {
|
||||
this->channels = 2;
|
||||
} else {
|
||||
this->channels = 1;
|
||||
}
|
||||
}
|
||||
|
||||
OpusCodec::~OpusCodec() {
|
||||
cout << "Free opus instance" << endl;
|
||||
|
||||
if(this->decoder || this->encoder) {
|
||||
NAN_THROW_EXCEPTION(Error, "please finalize before releasing!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(OpusCodec::initialize) {
|
||||
int error = 0;
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
this->encoder = opus_encoder_create(this->sampling_rate, this->channels, this->type == CodecType::OPUS_MUSIC ? OPUS_APPLICATION_AUDIO : OPUS_APPLICATION_VOIP, &error);
|
||||
if(!this->encoder || error) {
|
||||
NAN_THROW_EXCEPTION(Error, ("Failed to create encoder (" + to_string(error) + ")").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
this->decoder = opus_decoder_create(this->sampling_rate, this->channels, &error);
|
||||
if(!this->encoder || error) {
|
||||
opus_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
|
||||
NAN_THROW_EXCEPTION(Error, ("Failed to create decoder (" + to_string(error) + ")").c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(OpusCodec::finalize) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
opus_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
|
||||
opus_decoder_destroy(this->decoder);
|
||||
this->decoder = nullptr;
|
||||
}
|
||||
|
||||
NAN_METHOD(OpusCodec::encode) {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
if(info.Length() != 3 || !info[0]->IsArrayBuffer()) {
|
||||
NAN_THROW_EXCEPTION(Error, "Invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_buffer = info[0].As<ArrayBuffer>()->GetContents();
|
||||
auto buffer = make_unique<Chunk>(max(js_buffer.ByteLength(), 256UL));
|
||||
buffer->length = js_buffer.ByteLength();
|
||||
memcpy(buffer->memory, js_buffer.Data(), js_buffer.ByteLength());
|
||||
|
||||
auto callback_success = make_unique<Nan::Callback>(info[1].As<Function>());
|
||||
auto callback_error = make_unique<Nan::Callback>(info[2].As<Function>());
|
||||
|
||||
auto codec = make_unique<v8::Persistent<Object>>(info.GetIsolate(), info.Holder());
|
||||
auto callback = Nan::async_callback([callback_success = move(callback_success), callback_error = move(callback_error), codec = move(codec)](std::unique_ptr<Chunk> result, std::string error) {
|
||||
if(result) {
|
||||
auto _buffer = v8::ArrayBuffer::New(v8::Isolate::GetCurrent(), result->length);
|
||||
memcpy(_buffer->GetContents().Data(), result->memory, result->length);
|
||||
|
||||
Local<Value> argv[] = { _buffer }; //_buffer
|
||||
callback_success->Call(1, argv);
|
||||
} else {
|
||||
Local<Value> argv[] = { Nan::New<String>(error).ToLocalChecked() }; //error
|
||||
callback_error->Call(1, argv);
|
||||
}
|
||||
codec->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
tc::codec_workers->enqueue_task(
|
||||
[this, callback, buffer = move(buffer)]() mutable {
|
||||
int result;
|
||||
{
|
||||
lock_guard lock(this->coder_lock);
|
||||
if(!this->encoder) {
|
||||
callback(nullptr, "Please initialize first!");
|
||||
return;
|
||||
}
|
||||
|
||||
if(this->channels == 1) {
|
||||
result = opus_encode_float(this->encoder, (float*) buffer->memory, (int) (buffer->length / sizeof(float) / this->channels), (u_char*) buffer->memory, (opus_int32) buffer->allocated_length);
|
||||
} else {
|
||||
auto samples = buffer->length / sizeof(float) / this->channels;
|
||||
float* local_buffer = new float[samples * this->channels];
|
||||
for(size_t channel = 0; channel < this->channels; channel++)
|
||||
for(size_t sample = 0; sample < samples; sample++)
|
||||
local_buffer[sample * this->channels + channel] = ((float*) buffer->memory)[channel * samples + sample];
|
||||
|
||||
result = opus_encode_float(this->encoder, local_buffer, samples, (u_char*) buffer->memory, (opus_int32) buffer->allocated_length);
|
||||
delete[] local_buffer;
|
||||
}
|
||||
}
|
||||
|
||||
if(result <= 0) {
|
||||
callback(nullptr, "Invalid return code (" + to_string(result) + ")");
|
||||
} else {
|
||||
buffer->length = result;
|
||||
callback(move(buffer), "");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
NAN_METHOD(OpusCodec::decode) {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
if(!info[0]->IsArrayBuffer()) {
|
||||
NAN_THROW_EXCEPTION(Error, "First argument isn't an array buffer!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_buffer = info[0].As<ArrayBuffer>()->GetContents();
|
||||
auto buffer = make_unique<Chunk>(max(js_buffer.ByteLength(), this->channels * this->frames * sizeof(float)));
|
||||
buffer->length = js_buffer.ByteLength();
|
||||
memcpy(buffer->memory, js_buffer.Data(), js_buffer.ByteLength());
|
||||
|
||||
auto callback_success = make_unique<Nan::Callback>(info[1].As<Function>());
|
||||
auto callback_error = make_unique<Nan::Callback>(info[2].As<Function>());
|
||||
|
||||
auto codec = make_unique<v8::Persistent<Object>>(info.GetIsolate(), info.Holder());
|
||||
auto callback = Nan::async_callback([callback_success = move(callback_success), callback_error = move(callback_error), codec = move(codec)](std::unique_ptr<Chunk> result, std::string error) {
|
||||
Nan::HandleScope scope;
|
||||
if(result) {
|
||||
auto _buffer = v8::ArrayBuffer::New(v8::Isolate::GetCurrent(), result->length);
|
||||
memcpy(_buffer->GetContents().Data(), result->memory, result->length);
|
||||
Local<Value> argv[] = { _buffer };
|
||||
callback_success->Call(1, argv);
|
||||
} else {
|
||||
Local<Value> argv[] = { Nan::New<String>(error).ToLocalChecked() };
|
||||
callback_error->Call(1, argv);
|
||||
}
|
||||
codec->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
tc::codec_workers->enqueue_task(
|
||||
[this, buffer = move(buffer), callback]() mutable {
|
||||
int result;
|
||||
|
||||
{
|
||||
lock_guard lock(this->coder_lock);
|
||||
if(!this->decoder) {
|
||||
callback(nullptr, "Please initialize first!");
|
||||
return;
|
||||
}
|
||||
|
||||
if(this->channels == 1) {
|
||||
result = opus_decode_float(this->decoder, (u_char*) buffer->memory, (opus_int32) buffer->length, (float*) buffer->memory, this->frames, 0);
|
||||
} else {
|
||||
float* local_buffer = new float[this->frames * this->channels];
|
||||
result = opus_decode_float(this->decoder, (u_char*) buffer->memory, (opus_int32) buffer->length, (float*) local_buffer, this->frames, 0);
|
||||
|
||||
for(size_t channel = 0; channel < this->channels; channel++)
|
||||
for(size_t sample = 0; sample < result; sample++)
|
||||
((float*) buffer->memory)[channel * this->frames + sample] = local_buffer[sample * this->channels + channel];
|
||||
delete[] local_buffer;
|
||||
}
|
||||
}
|
||||
|
||||
if(result <= 0) {
|
||||
callback(nullptr, "Invalid return code (" + to_string(result) + ")");
|
||||
} else {
|
||||
buffer->length = result * sizeof(float) * this->channels;
|
||||
callback(move(buffer), "");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
#endif
|
@ -1,33 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "NativeCodec.h"
|
||||
|
||||
#ifdef HAVE_OPUS
|
||||
#include <opus/opus.h>
|
||||
#endif
|
||||
|
||||
namespace tc {
|
||||
class NativeCodec;
|
||||
|
||||
class OpusCodec : public NativeCodec {
|
||||
public:
|
||||
static bool supported();
|
||||
#ifdef HAVE_OPUS
|
||||
explicit OpusCodec(CodecType::value type);
|
||||
virtual ~OpusCodec();
|
||||
|
||||
virtual NAN_METHOD(initialize);
|
||||
virtual NAN_METHOD(finalize);
|
||||
virtual NAN_METHOD(encode);
|
||||
virtual NAN_METHOD(decode);
|
||||
private:
|
||||
uint16_t sampling_rate = 0;
|
||||
uint16_t frames = 0;
|
||||
uint32_t channels = 0;
|
||||
|
||||
std::mutex coder_lock;
|
||||
OpusDecoder* decoder = nullptr;
|
||||
OpusEncoder* encoder = nullptr;
|
||||
#endif
|
||||
};
|
||||
}
|
@ -1,234 +0,0 @@
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include "SpeexCodec.h"
|
||||
#include "NativeCodec.h"
|
||||
#include "NanException.h"
|
||||
#include "NanEventCallback.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc;
|
||||
using namespace v8;
|
||||
using namespace Nan;
|
||||
|
||||
bool SpeexCodec::supported() {
|
||||
#ifdef HAVE_SPEEX
|
||||
return true;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef HAVE_SPEEX
|
||||
|
||||
SpeexCodec::SpeexCodec(NativeCodec::CodecType::value type) : NativeCodec(type) {
|
||||
cout << "Allocate speex instance" << endl;
|
||||
|
||||
}
|
||||
|
||||
SpeexCodec::~SpeexCodec() {
|
||||
cout << "Free speex instance" << endl;
|
||||
|
||||
if(this->decoder || this->encoder) {
|
||||
NAN_THROW_EXCEPTION(Error, "please finalize before releasing!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(SpeexCodec::initialize) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
const SpeexMode* speex_mode = nullptr;
|
||||
|
||||
if(this->type == CodecType::SPEEX_NARROWBAND)
|
||||
speex_mode = &speex_nb_mode;
|
||||
else if(this->type == CodecType::SPEEX_WIDEBAND)
|
||||
speex_mode = &speex_wb_mode;
|
||||
else if(this->type == CodecType::SPEEX_ULTRA_WIDEBAND)
|
||||
speex_mode = &speex_uwb_mode;
|
||||
|
||||
assert(speex_mode);
|
||||
{
|
||||
|
||||
this->encoder = speex_encoder_init(speex_mode);
|
||||
if(!this->encoder) {
|
||||
NAN_THROW_EXCEPTION(Error, "Failed to create encoder");
|
||||
return;
|
||||
}
|
||||
|
||||
/*Set the quality to 8 (15 kbps)*/
|
||||
int tmp = 8; //FIXME configurable
|
||||
speex_encoder_ctl(this->encoder, SPEEX_SET_QUALITY, &tmp);
|
||||
|
||||
speex_encoder_ctl(this->encoder, SPEEX_GET_FRAME_SIZE, &this->frame_size);
|
||||
}
|
||||
{
|
||||
this->decoder = speex_decoder_init(speex_mode);
|
||||
if(!this->decoder) {
|
||||
speex_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
|
||||
NAN_THROW_EXCEPTION(Error, "Failed to create decoder");
|
||||
return;
|
||||
}
|
||||
|
||||
int tmp = 1; //TODO What is this?
|
||||
speex_decoder_ctl(this->decoder, SPEEX_SET_ENH, &tmp);
|
||||
|
||||
|
||||
int tmp_frame_size;
|
||||
speex_encoder_ctl(this->encoder, SPEEX_GET_FRAME_SIZE, &tmp_frame_size);
|
||||
if(tmp_frame_size != this->frame_size) {
|
||||
NAN_THROW_EXCEPTION(Error, "Decoder and encoder have different frame sizes!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
speex_bits_init(&this->encoder_bits);
|
||||
speex_bits_init(&this->decoder_bits);
|
||||
}
|
||||
|
||||
NAN_METHOD(SpeexCodec::finalize) {
|
||||
lock_guard lock(this->coder_lock);
|
||||
|
||||
if(this->encoder) {
|
||||
speex_encoder_destroy(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
|
||||
speex_bits_destroy(&this->encoder_bits);
|
||||
}
|
||||
|
||||
if(this->decoder) {
|
||||
speex_decoder_destroy(this->decoder);
|
||||
this->decoder = nullptr;
|
||||
|
||||
speex_bits_destroy(&this->decoder_bits);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(SpeexCodec::encode) {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
if(!info[0]->IsArrayBuffer()) {
|
||||
NAN_THROW_EXCEPTION(Error, "First argument isn't an array buffer!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_buffer = info[0].As<ArrayBuffer>()->GetContents();
|
||||
auto buffer = make_unique<Chunk>(max(js_buffer.ByteLength(), 256UL));
|
||||
buffer->length = js_buffer.ByteLength();
|
||||
memcpy(buffer->memory, js_buffer.Data(), js_buffer.ByteLength());
|
||||
|
||||
auto callback_success = make_unique<Nan::Callback>(info[1].As<Function>());
|
||||
auto callback_error = make_unique<Nan::Callback>(info[2].As<Function>());
|
||||
|
||||
auto codec = make_unique<v8::Persistent<Object>>(info.GetIsolate(), info.Holder());
|
||||
auto callback = Nan::async_callback([callback_success = move(callback_success), callback_error = move(callback_error), codec = move(codec)](std::unique_ptr<Chunk> result, std::string error) {
|
||||
Nan::HandleScope scope;
|
||||
if(result) {
|
||||
auto _buffer = v8::ArrayBuffer::New(v8::Isolate::GetCurrent(), result->length);
|
||||
memcpy(_buffer->GetContents().Data(), result->memory, result->length);
|
||||
|
||||
Local<Value> argv[] = { _buffer }; //_buffer
|
||||
callback_success->Call(1, argv);
|
||||
} else {
|
||||
Local<Value> argv[] = { Nan::New<String>(error).ToLocalChecked() }; //error
|
||||
callback_error->Call(1, argv);
|
||||
}
|
||||
codec->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
tc::codec_workers->enqueue_task(
|
||||
[this, callback, buffer = move(buffer)]() mutable {
|
||||
{
|
||||
lock_guard lock(this->coder_lock);
|
||||
if(!this->encoder) {
|
||||
callback(nullptr, "Please initialize first!");
|
||||
return;
|
||||
}
|
||||
|
||||
if(buffer->length < this->frame_size * sizeof(float)) {
|
||||
callback(nullptr, "Input buffer to short! Received " + to_string(buffer->length) + ", Expected " + to_string(this->frame_size * sizeof(float)));
|
||||
return;
|
||||
}
|
||||
|
||||
for(size_t frame = 0; frame < this->frame_size; frame++)
|
||||
((float*) buffer->memory)[frame] *= 8000; //We want a range between 0 and 8000
|
||||
|
||||
speex_bits_reset(&this->encoder_bits);
|
||||
speex_encode(this->encoder, (float*) buffer->memory, &this->encoder_bits);
|
||||
auto nbytes = speex_bits_write(&this->encoder_bits, buffer->memory, buffer->allocated_length);
|
||||
|
||||
if(nbytes < 0) {
|
||||
callback(nullptr, "Invalid write?");
|
||||
return;
|
||||
}
|
||||
buffer->length = nbytes;
|
||||
}
|
||||
callback(move(buffer), "");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
NAN_METHOD(SpeexCodec::decode) {
|
||||
Nan::HandleScope scope;
|
||||
|
||||
if(!info[0]->IsArrayBuffer()) {
|
||||
NAN_THROW_EXCEPTION(Error, "First argument isn't an array buffer!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_buffer = info[0].As<ArrayBuffer>()->GetContents();
|
||||
auto buffer = make_unique<Chunk>(max(js_buffer.ByteLength(), this->frame_size * sizeof(float)));
|
||||
buffer->length = js_buffer.ByteLength();
|
||||
memcpy(buffer->memory, js_buffer.Data(), js_buffer.ByteLength());
|
||||
|
||||
auto callback_success = make_unique<Nan::Callback>(info[1].As<Function>());
|
||||
auto callback_error = make_unique<Nan::Callback>(info[2].As<Function>());
|
||||
|
||||
auto codec = make_unique<v8::Persistent<Object>>(info.GetIsolate(), info.Holder());
|
||||
auto callback = Nan::async_callback([callback_success = move(callback_success), callback_error = move(callback_error), codec = move(codec)](std::unique_ptr<Chunk> result, std::string error) {
|
||||
Nan::HandleScope scope;
|
||||
if(result) {
|
||||
auto _buffer = v8::ArrayBuffer::New(v8::Isolate::GetCurrent(), result->length);
|
||||
memcpy(_buffer->GetContents().Data(), result->memory, result->length);
|
||||
Local<Value> argv[] = { _buffer };
|
||||
callback_success->Call(1, argv);
|
||||
} else {
|
||||
Local<Value> argv[] = { Nan::New<String>(error).ToLocalChecked() };
|
||||
callback_error->Call(1, argv);
|
||||
}
|
||||
codec->Reset();
|
||||
}).option_destroyed_execute(true);
|
||||
|
||||
tc::codec_workers->enqueue_task(
|
||||
[this, callback, buffer = move(buffer)]() mutable {
|
||||
int result;
|
||||
|
||||
{
|
||||
lock_guard lock(this->coder_lock);
|
||||
if(!this->decoder) {
|
||||
callback(nullptr, "Please initialize first!");
|
||||
return;
|
||||
}
|
||||
|
||||
speex_bits_reset(&this->decoder_bits);
|
||||
speex_bits_read_from(&this->decoder_bits, buffer->memory, buffer->length);
|
||||
auto state = speex_decode(this->decoder, &this->decoder_bits, (float*) buffer->memory);
|
||||
if(state != 0) {
|
||||
callback(nullptr, "decode failed (" + to_string(state) + ")");
|
||||
return;
|
||||
}
|
||||
buffer->length = this->frame_size * sizeof(float);
|
||||
for(size_t frame = 0; frame < this->frame_size; frame++)
|
||||
((float*) buffer->memory)[frame] /= 8000; //We want a range between 0 and 1
|
||||
}
|
||||
|
||||
callback(move(buffer), "");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#endif
|
@ -1,36 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "NativeCodec.h"
|
||||
|
||||
#ifdef HAVE_SPEEX
|
||||
#include <speex/speex.h>
|
||||
#endif
|
||||
|
||||
namespace tc {
|
||||
class NativeCodec;
|
||||
|
||||
|
||||
class SpeexCodec : public NativeCodec {
|
||||
public:
|
||||
static bool supported();
|
||||
|
||||
#ifdef HAVE_SPEEX
|
||||
explicit SpeexCodec(CodecType::value type);
|
||||
virtual ~SpeexCodec();
|
||||
|
||||
virtual NAN_METHOD(initialize);
|
||||
virtual NAN_METHOD(finalize);
|
||||
virtual NAN_METHOD(encode);
|
||||
virtual NAN_METHOD(decode);
|
||||
private:
|
||||
int frame_size = 0;
|
||||
|
||||
//TODO are two bits really necessary?
|
||||
std::mutex coder_lock;
|
||||
SpeexBits encoder_bits;
|
||||
void* encoder = nullptr;
|
||||
SpeexBits decoder_bits;
|
||||
void* decoder = nullptr;
|
||||
#endif
|
||||
};
|
||||
}
|
2
native/codec/libraries/.gitignore
vendored
2
native/codec/libraries/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
generated/
|
||||
opus/build
|
@ -1,94 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd $(dirname "$0")
|
||||
install_directory="$(pwd)/generated/celt/"
|
||||
|
||||
machine="$(uname -s)"
|
||||
case "${machine}" in
|
||||
Linux*) machine=Linux;;
|
||||
# Darwin*) machine=Mac;;
|
||||
MINGW*) machine=MinGW;;
|
||||
*) machine="UNKNOWN:${machine}"
|
||||
esac
|
||||
|
||||
if [[ ${machine} == "UNKNOWN"* ]]; then
|
||||
echo "Unknown platform ${machine}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd celt
|
||||
|
||||
if [[ ${machine} == "Linux" ]]; then
|
||||
if [[ ! -e configure ]]; then
|
||||
echo "Generating configure file"
|
||||
./autogen.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate configure file"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
windows_build="win32/VS2015/"
|
||||
windows_build_type="x64"
|
||||
if [[ ( ! -d build ) && ( ! -d "${windows_build}/${windows_build_type}" ) ]] || [[ "$1" == "rebuild" ]]; then
|
||||
if [[ ${machine} == "Linux" ]]; then
|
||||
if [[ -e build ]]; then
|
||||
rm -r build
|
||||
fi
|
||||
mkdir build && cd build
|
||||
|
||||
export CFLAGS="-fPIC"
|
||||
../configure --prefix="${install_directory}" --with-pic
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to configure project!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ..
|
||||
elif [[ ${machine} == "MinGW" ]]; then
|
||||
#Only cleanup last shit
|
||||
if [[ -e "${windows_build}/${windows_build_type}" ]]; then
|
||||
rm -r "${windows_build}/${windows_build_type}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e "${install_directory}" ]]; then
|
||||
echo "deleting old install directory!"
|
||||
rm -r "${install_directory}"
|
||||
echo "rm -r '${install_directory}'"
|
||||
fi
|
||||
|
||||
if [[ ${machine} == "MinGW" ]]; then
|
||||
saved_pwd=$(pwd)
|
||||
cd "${windows_build}"
|
||||
MSBuild.exe -p:Platform=x64 -property:Configuration=Release opus.vcxproj
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build celt!"
|
||||
exit 1
|
||||
fi
|
||||
cd ${saved_pwd}
|
||||
|
||||
mkdir -p "${install_directory}/include/celt"
|
||||
mkdir -p "${install_directory}/lib/"
|
||||
|
||||
cp -r include/* "${install_directory}/include/celt/"
|
||||
cp -r ${windows_build}/${windows_build_type}/Release/*.lib "${install_directory}/lib/"
|
||||
elif [[ ${machine} == "Linux" ]]; then
|
||||
cd build
|
||||
|
||||
make -j 12
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build celt!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
make install
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to install celt!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Celt build successfully"
|
@ -1,98 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd $(dirname "$0")
|
||||
install_directory="$(pwd)/generated/opus/"
|
||||
|
||||
machine="$(uname -s)"
|
||||
case "${machine}" in
|
||||
Linux*) machine=Linux;;
|
||||
# Darwin*) machine=Mac;;
|
||||
MINGW*) machine=MinGW;;
|
||||
*) machine="UNKNOWN:${machine}"
|
||||
esac
|
||||
|
||||
if [[ ${machine} == "UNKNOWN"* ]]; then
|
||||
echo "Unknown platform ${machine}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd opus
|
||||
#if [ ! -e CMakeLists.txt ]; then
|
||||
# echo "Linking CMakeLists"
|
||||
# ln -s ../cmake/opus/CMakeLists.txt .
|
||||
#fi
|
||||
|
||||
if [[ ${machine} == "Linux" ]]; then
|
||||
if [[ ! -e configure ]]; then
|
||||
echo "Generating configure file"
|
||||
./autogen.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate configure file"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
windows_build="win32/VS2015/"
|
||||
windows_build_type="x64"
|
||||
if [[ ( ! -d build ) && ( ! -d "${windows_build}/${windows_build_type}" ) ]] || [[ "$1" == "rebuild" ]]; then
|
||||
if [[ ${machine} == "Linux" ]]; then
|
||||
if [[ -e build ]]; then
|
||||
rm -r build
|
||||
fi
|
||||
mkdir build && cd build
|
||||
|
||||
export CFLAGS="-fPIC"
|
||||
../configure --prefix="${install_directory}" --with-pic
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to configure project!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ..
|
||||
elif [[ ${machine} == "MinGW" ]]; then
|
||||
#Only cleanup last shit
|
||||
if [[ -e "${windows_build}/${windows_build_type}" ]]; then
|
||||
rm -r "${windows_build}/${windows_build_type}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e "${install_directory}" ]]; then
|
||||
echo "deleting old install directory!"
|
||||
rm -r "${install_directory}"
|
||||
echo "rm -r '${install_directory}'"
|
||||
fi
|
||||
|
||||
if [[ ${machine} == "MinGW" ]]; then
|
||||
saved_pwd=$(pwd)
|
||||
cd "${windows_build}"
|
||||
MSBuild.exe -p:Platform=x64 -property:Configuration=Release opus.vcxproj
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build opus!"
|
||||
exit 1
|
||||
fi
|
||||
cd ${saved_pwd}
|
||||
|
||||
mkdir -p "${install_directory}/include/opus"
|
||||
mkdir -p "${install_directory}/lib/"
|
||||
|
||||
cp -r include/* "${install_directory}/include/opus/"
|
||||
cp -r ${windows_build}/${windows_build_type}/Release/*.lib "${install_directory}/lib/"
|
||||
elif [[ ${machine} == "Linux" ]]; then
|
||||
cd build
|
||||
|
||||
make -j 12
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build opus!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
make install
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to install opus!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Opus build successfully"
|
@ -1,97 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd $(dirname "$0")
|
||||
install_directory="$(pwd)/generated/speex/"
|
||||
|
||||
machine="$(uname -s)"
|
||||
case "${machine}" in
|
||||
Linux*) machine=Linux;;
|
||||
# Darwin*) machine=Mac;;
|
||||
MINGW*) machine=MinGW;;
|
||||
*) machine="UNKNOWN:${machine}"
|
||||
esac
|
||||
|
||||
if [[ ${machine} == "UNKNOWN"* ]]; then
|
||||
echo "Unknown platform ${machine}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd speex
|
||||
if [[ ${machine} == "Linux" ]]; then
|
||||
if [[ ! -e configure ]]; then
|
||||
echo "Generating configure file"
|
||||
./autogen.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate configure file"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
windows_build="win32/VS2015/"
|
||||
windows_build_type="x64"
|
||||
if [[ ( ! -d build ) && ( ! -d "${windows_build}/${windows_build_type}" ) ]] || [[ "$1" == "rebuild" ]]; then
|
||||
if [[ ${machine} == "Linux" ]]; then
|
||||
if [[ -e build ]]; then
|
||||
rm -r build
|
||||
fi
|
||||
mkdir build && cd build
|
||||
|
||||
export CFLAGS="-fPIC"
|
||||
../configure --prefix="${install_directory}" --with-pic
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to configure project!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ..
|
||||
elif [[ ${machine} == "MinGW" ]]; then
|
||||
#Only cleanup last shit
|
||||
if [[ -e "${windows_build}" ]]; then
|
||||
rm -r "${windows_build}"
|
||||
fi
|
||||
|
||||
mkdir -p ${windows_build}
|
||||
cp -r ../template/speex_VS2015/* ${windows_build}/
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e "${install_directory}" ]]; then
|
||||
echo "deleting old install directory!"
|
||||
rm -r "${install_directory}"
|
||||
echo "rm -r '${install_directory}'"
|
||||
fi
|
||||
|
||||
if [[ ${machine} == "MinGW" ]]; then
|
||||
saved_pwd=$(pwd)
|
||||
cd "${windows_build}"
|
||||
MSBuild.exe -p:Platform=x64 -property:Configuration=Release libspeex/libspeex.vcxproj
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build speex!"
|
||||
exit 1
|
||||
fi
|
||||
cd ${saved_pwd}
|
||||
|
||||
mkdir -p "${install_directory}/include/speex"
|
||||
mkdir -p "${install_directory}/lib/"
|
||||
|
||||
cp -r include/speex/*.h "${install_directory}/include/speex/"
|
||||
cp -r ${windows_build}/libspeex/${windows_build_type}/Release/*.lib "${install_directory}/lib/"
|
||||
elif [[ ${machine} == "Linux" ]]; then
|
||||
cd build
|
||||
|
||||
make -j 12
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build speex!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
make install
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to install speex!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Speex build successfully"
|
33
native/codec/libraries/celt/.gitignore
vendored
33
native/codec/libraries/celt/.gitignore
vendored
@ -1,33 +0,0 @@
|
||||
Makefile
|
||||
Makefile.in
|
||||
aclocal.m4
|
||||
autom4te.cache
|
||||
*.kdevelop.pcs
|
||||
*.kdevses
|
||||
config.guess
|
||||
config.h
|
||||
config.h.in
|
||||
config.log
|
||||
config.status
|
||||
config.sub
|
||||
configure
|
||||
depcomp
|
||||
install-sh
|
||||
.deps
|
||||
.libs
|
||||
*.la
|
||||
testcelt
|
||||
libtool
|
||||
ltmain.sh
|
||||
missing
|
||||
stamp-h1
|
||||
*.sw
|
||||
*.o
|
||||
*.lo
|
||||
*~
|
||||
tests/*test
|
||||
tools/celtdec
|
||||
tools/celtenc
|
||||
celt.pc
|
||||
libcelt.spec
|
||||
libcelt/dump_modes
|
@ -1,25 +0,0 @@
|
||||
Copyright 2001-2009 Jean-Marc Valin, Timothy B. Terriberry,
|
||||
CSIRO, and other contributors
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -1,283 +0,0 @@
|
||||
# Doxyfile 1.5.3
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Project related configuration options
|
||||
#---------------------------------------------------------------------------
|
||||
DOXYFILE_ENCODING = UTF-8
|
||||
PROJECT_NAME = CELT
|
||||
PROJECT_NUMBER = 0.11.4
|
||||
OUTPUT_DIRECTORY = doc/API
|
||||
CREATE_SUBDIRS = NO
|
||||
OUTPUT_LANGUAGE = English
|
||||
BRIEF_MEMBER_DESC = YES
|
||||
REPEAT_BRIEF = YES
|
||||
ABBREVIATE_BRIEF = "The $name class " \
|
||||
"The $name widget " \
|
||||
"The $name file " \
|
||||
is \
|
||||
provides \
|
||||
specifies \
|
||||
contains \
|
||||
represents \
|
||||
a \
|
||||
an \
|
||||
the
|
||||
ALWAYS_DETAILED_SEC = NO
|
||||
INLINE_INHERITED_MEMB = NO
|
||||
FULL_PATH_NAMES = YES
|
||||
STRIP_FROM_PATH =
|
||||
STRIP_FROM_INC_PATH =
|
||||
SHORT_NAMES = NO
|
||||
JAVADOC_AUTOBRIEF = NO
|
||||
QT_AUTOBRIEF = NO
|
||||
MULTILINE_CPP_IS_BRIEF = NO
|
||||
DETAILS_AT_TOP = NO
|
||||
INHERIT_DOCS = YES
|
||||
SEPARATE_MEMBER_PAGES = NO
|
||||
TAB_SIZE = 8
|
||||
ALIASES =
|
||||
OPTIMIZE_OUTPUT_FOR_C = YES
|
||||
OPTIMIZE_OUTPUT_JAVA = NO
|
||||
BUILTIN_STL_SUPPORT = NO
|
||||
CPP_CLI_SUPPORT = NO
|
||||
DISTRIBUTE_GROUP_DOC = NO
|
||||
SUBGROUPING = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# Build related configuration options
|
||||
#---------------------------------------------------------------------------
|
||||
EXTRACT_ALL = NO
|
||||
EXTRACT_PRIVATE = NO
|
||||
EXTRACT_STATIC = NO
|
||||
EXTRACT_LOCAL_CLASSES = YES
|
||||
EXTRACT_LOCAL_METHODS = NO
|
||||
EXTRACT_ANON_NSPACES = NO
|
||||
HIDE_UNDOC_MEMBERS = YES
|
||||
HIDE_UNDOC_CLASSES = YES
|
||||
HIDE_FRIEND_COMPOUNDS = NO
|
||||
HIDE_IN_BODY_DOCS = NO
|
||||
INTERNAL_DOCS = NO
|
||||
CASE_SENSE_NAMES = YES
|
||||
HIDE_SCOPE_NAMES = NO
|
||||
SHOW_INCLUDE_FILES = YES
|
||||
INLINE_INFO = YES
|
||||
SORT_MEMBER_DOCS = YES
|
||||
SORT_BRIEF_DOCS = NO
|
||||
SORT_BY_SCOPE_NAME = NO
|
||||
GENERATE_TODOLIST = YES
|
||||
GENERATE_TESTLIST = YES
|
||||
GENERATE_BUGLIST = YES
|
||||
GENERATE_DEPRECATEDLIST= YES
|
||||
ENABLED_SECTIONS =
|
||||
MAX_INITIALIZER_LINES = 30
|
||||
SHOW_USED_FILES = YES
|
||||
SHOW_DIRECTORIES = NO
|
||||
FILE_VERSION_FILTER =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to warning and progress messages
|
||||
#---------------------------------------------------------------------------
|
||||
QUIET = NO
|
||||
WARNINGS = YES
|
||||
WARN_IF_UNDOCUMENTED = YES
|
||||
WARN_IF_DOC_ERROR = YES
|
||||
WARN_NO_PARAMDOC = NO
|
||||
WARN_FORMAT = "$file:$line: $text "
|
||||
WARN_LOGFILE =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the input files
|
||||
#---------------------------------------------------------------------------
|
||||
INPUT = libcelt/celt.h \
|
||||
libcelt/celt_types.h \
|
||||
libcelt/celt_header.h
|
||||
INPUT_ENCODING = UTF-8
|
||||
FILE_PATTERNS = *.c \
|
||||
*.cc \
|
||||
*.cxx \
|
||||
*.cpp \
|
||||
*.c++ \
|
||||
*.d \
|
||||
*.java \
|
||||
*.ii \
|
||||
*.ixx \
|
||||
*.ipp \
|
||||
*.i++ \
|
||||
*.inl \
|
||||
*.h \
|
||||
*.hh \
|
||||
*.hxx \
|
||||
*.hpp \
|
||||
*.h++ \
|
||||
*.idl \
|
||||
*.odl \
|
||||
*.cs \
|
||||
*.php \
|
||||
*.php3 \
|
||||
*.inc \
|
||||
*.m \
|
||||
*.mm \
|
||||
*.dox \
|
||||
*.py \
|
||||
*.C \
|
||||
*.CC \
|
||||
*.C++ \
|
||||
*.II \
|
||||
*.I++ \
|
||||
*.H \
|
||||
*.HH \
|
||||
*.H++ \
|
||||
*.CS \
|
||||
*.PHP \
|
||||
*.PHP3 \
|
||||
*.M \
|
||||
*.MM \
|
||||
*.PY
|
||||
RECURSIVE = NO
|
||||
EXCLUDE =
|
||||
EXCLUDE_SYMLINKS = NO
|
||||
EXCLUDE_PATTERNS = *.c
|
||||
EXCLUDE_SYMBOLS =
|
||||
EXAMPLE_PATH =
|
||||
EXAMPLE_PATTERNS = *
|
||||
EXAMPLE_RECURSIVE = NO
|
||||
IMAGE_PATH =
|
||||
INPUT_FILTER =
|
||||
FILTER_PATTERNS =
|
||||
FILTER_SOURCE_FILES = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to source browsing
|
||||
#---------------------------------------------------------------------------
|
||||
SOURCE_BROWSER = YES
|
||||
INLINE_SOURCES = NO
|
||||
STRIP_CODE_COMMENTS = YES
|
||||
REFERENCED_BY_RELATION = YES
|
||||
REFERENCES_RELATION = YES
|
||||
REFERENCES_LINK_SOURCE = YES
|
||||
USE_HTAGS = NO
|
||||
VERBATIM_HEADERS = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the alphabetical class index
|
||||
#---------------------------------------------------------------------------
|
||||
ALPHABETICAL_INDEX = NO
|
||||
COLS_IN_ALPHA_INDEX = 5
|
||||
IGNORE_PREFIX =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the HTML output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_HTML = YES
|
||||
HTML_OUTPUT = html
|
||||
HTML_FILE_EXTENSION = .html
|
||||
HTML_HEADER =
|
||||
HTML_FOOTER =
|
||||
HTML_STYLESHEET =
|
||||
HTML_ALIGN_MEMBERS = YES
|
||||
GENERATE_HTMLHELP = NO
|
||||
HTML_DYNAMIC_SECTIONS = NO
|
||||
CHM_FILE =
|
||||
HHC_LOCATION =
|
||||
GENERATE_CHI = NO
|
||||
BINARY_TOC = NO
|
||||
TOC_EXPAND = NO
|
||||
DISABLE_INDEX = NO
|
||||
ENUM_VALUES_PER_LINE = 4
|
||||
GENERATE_TREEVIEW = NO
|
||||
TREEVIEW_WIDTH = 250
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the LaTeX output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_LATEX = YES
|
||||
LATEX_OUTPUT = latex
|
||||
LATEX_CMD_NAME = latex
|
||||
MAKEINDEX_CMD_NAME = makeindex
|
||||
COMPACT_LATEX = NO
|
||||
PAPER_TYPE = a4wide
|
||||
EXTRA_PACKAGES =
|
||||
LATEX_HEADER =
|
||||
PDF_HYPERLINKS = YES
|
||||
USE_PDFLATEX = YES
|
||||
LATEX_BATCHMODE = NO
|
||||
LATEX_HIDE_INDICES = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the RTF output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_RTF = NO
|
||||
RTF_OUTPUT = rtf
|
||||
COMPACT_RTF = NO
|
||||
RTF_HYPERLINKS = NO
|
||||
RTF_STYLESHEET_FILE =
|
||||
RTF_EXTENSIONS_FILE =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the man page output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_MAN = YES
|
||||
MAN_OUTPUT = man
|
||||
MAN_EXTENSION = .3
|
||||
MAN_LINKS = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the XML output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_XML = NO
|
||||
XML_OUTPUT = xml
|
||||
XML_SCHEMA =
|
||||
XML_DTD =
|
||||
XML_PROGRAMLISTING = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options for the AutoGen Definitions output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_AUTOGEN_DEF = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the Perl module output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_PERLMOD = NO
|
||||
PERLMOD_LATEX = NO
|
||||
PERLMOD_PRETTY = YES
|
||||
PERLMOD_MAKEVAR_PREFIX =
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration options related to the preprocessor
|
||||
#---------------------------------------------------------------------------
|
||||
ENABLE_PREPROCESSING = YES
|
||||
MACRO_EXPANSION = NO
|
||||
EXPAND_ONLY_PREDEF = NO
|
||||
SEARCH_INCLUDES = YES
|
||||
INCLUDE_PATH =
|
||||
INCLUDE_FILE_PATTERNS =
|
||||
PREDEFINED =
|
||||
EXPAND_AS_DEFINED =
|
||||
SKIP_FUNCTION_MACROS = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration::additions related to external references
|
||||
#---------------------------------------------------------------------------
|
||||
TAGFILES =
|
||||
GENERATE_TAGFILE =
|
||||
ALLEXTERNALS = NO
|
||||
EXTERNAL_GROUPS = YES
|
||||
PERL_PATH = /usr/bin/perl
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration options related to the dot tool
|
||||
#---------------------------------------------------------------------------
|
||||
CLASS_DIAGRAMS = NO
|
||||
MSCGEN_PATH =
|
||||
HIDE_UNDOC_RELATIONS = YES
|
||||
HAVE_DOT = YES
|
||||
CLASS_GRAPH = YES
|
||||
COLLABORATION_GRAPH = YES
|
||||
GROUP_GRAPHS = YES
|
||||
UML_LOOK = NO
|
||||
TEMPLATE_RELATIONS = NO
|
||||
INCLUDE_GRAPH = NO
|
||||
INCLUDED_BY_GRAPH = NO
|
||||
CALL_GRAPH = NO
|
||||
CALLER_GRAPH = NO
|
||||
GRAPHICAL_HIERARCHY = YES
|
||||
DIRECTORY_GRAPH = YES
|
||||
DOT_IMAGE_FORMAT = png
|
||||
DOT_PATH =
|
||||
DOTFILE_DIRS =
|
||||
DOT_GRAPH_MAX_NODES = 50
|
||||
MAX_DOT_GRAPH_DEPTH = 1000
|
||||
DOT_TRANSPARENT = NO
|
||||
DOT_MULTI_TARGETS = NO
|
||||
GENERATE_LEGEND = YES
|
||||
DOT_CLEANUP = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration::additions related to the search engine
|
||||
#---------------------------------------------------------------------------
|
||||
SEARCHENGINE = NO
|
@ -1,281 +0,0 @@
|
||||
# Doxyfile 1.5.3
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Project related configuration options
|
||||
#---------------------------------------------------------------------------
|
||||
DOXYFILE_ENCODING = UTF-8
|
||||
PROJECT_NAME = CELT
|
||||
PROJECT_NUMBER = 0.11.4
|
||||
OUTPUT_DIRECTORY = doc/devel
|
||||
CREATE_SUBDIRS = NO
|
||||
OUTPUT_LANGUAGE = English
|
||||
BRIEF_MEMBER_DESC = YES
|
||||
REPEAT_BRIEF = YES
|
||||
ABBREVIATE_BRIEF = "The $name class " \
|
||||
"The $name widget " \
|
||||
"The $name file " \
|
||||
is \
|
||||
provides \
|
||||
specifies \
|
||||
contains \
|
||||
represents \
|
||||
a \
|
||||
an \
|
||||
the
|
||||
ALWAYS_DETAILED_SEC = NO
|
||||
INLINE_INHERITED_MEMB = NO
|
||||
FULL_PATH_NAMES = YES
|
||||
STRIP_FROM_PATH =
|
||||
STRIP_FROM_INC_PATH =
|
||||
SHORT_NAMES = NO
|
||||
JAVADOC_AUTOBRIEF = NO
|
||||
QT_AUTOBRIEF = NO
|
||||
MULTILINE_CPP_IS_BRIEF = NO
|
||||
DETAILS_AT_TOP = NO
|
||||
INHERIT_DOCS = YES
|
||||
SEPARATE_MEMBER_PAGES = NO
|
||||
TAB_SIZE = 8
|
||||
ALIASES =
|
||||
OPTIMIZE_OUTPUT_FOR_C = YES
|
||||
OPTIMIZE_OUTPUT_JAVA = NO
|
||||
BUILTIN_STL_SUPPORT = NO
|
||||
CPP_CLI_SUPPORT = NO
|
||||
DISTRIBUTE_GROUP_DOC = NO
|
||||
SUBGROUPING = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# Build related configuration options
|
||||
#---------------------------------------------------------------------------
|
||||
EXTRACT_ALL = YES
|
||||
EXTRACT_PRIVATE = NO
|
||||
EXTRACT_STATIC = NO
|
||||
EXTRACT_LOCAL_CLASSES = YES
|
||||
EXTRACT_LOCAL_METHODS = NO
|
||||
EXTRACT_ANON_NSPACES = NO
|
||||
HIDE_UNDOC_MEMBERS = YES
|
||||
HIDE_UNDOC_CLASSES = YES
|
||||
HIDE_FRIEND_COMPOUNDS = NO
|
||||
HIDE_IN_BODY_DOCS = NO
|
||||
INTERNAL_DOCS = NO
|
||||
CASE_SENSE_NAMES = YES
|
||||
HIDE_SCOPE_NAMES = NO
|
||||
SHOW_INCLUDE_FILES = YES
|
||||
INLINE_INFO = YES
|
||||
SORT_MEMBER_DOCS = YES
|
||||
SORT_BRIEF_DOCS = NO
|
||||
SORT_BY_SCOPE_NAME = NO
|
||||
GENERATE_TODOLIST = YES
|
||||
GENERATE_TESTLIST = YES
|
||||
GENERATE_BUGLIST = YES
|
||||
GENERATE_DEPRECATEDLIST= YES
|
||||
ENABLED_SECTIONS =
|
||||
MAX_INITIALIZER_LINES = 30
|
||||
SHOW_USED_FILES = YES
|
||||
SHOW_DIRECTORIES = NO
|
||||
FILE_VERSION_FILTER =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to warning and progress messages
|
||||
#---------------------------------------------------------------------------
|
||||
QUIET = NO
|
||||
WARNINGS = YES
|
||||
WARN_IF_UNDOCUMENTED = YES
|
||||
WARN_IF_DOC_ERROR = YES
|
||||
WARN_NO_PARAMDOC = NO
|
||||
WARN_FORMAT = "$file:$line: $text "
|
||||
WARN_LOGFILE =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the input files
|
||||
#---------------------------------------------------------------------------
|
||||
INPUT = libcelt
|
||||
INPUT_ENCODING = UTF-8
|
||||
FILE_PATTERNS = *.c \
|
||||
*.cc \
|
||||
*.cxx \
|
||||
*.cpp \
|
||||
*.c++ \
|
||||
*.d \
|
||||
*.java \
|
||||
*.ii \
|
||||
*.ixx \
|
||||
*.ipp \
|
||||
*.i++ \
|
||||
*.inl \
|
||||
*.h \
|
||||
*.hh \
|
||||
*.hxx \
|
||||
*.hpp \
|
||||
*.h++ \
|
||||
*.idl \
|
||||
*.odl \
|
||||
*.cs \
|
||||
*.php \
|
||||
*.php3 \
|
||||
*.inc \
|
||||
*.m \
|
||||
*.mm \
|
||||
*.dox \
|
||||
*.py \
|
||||
*.C \
|
||||
*.CC \
|
||||
*.C++ \
|
||||
*.II \
|
||||
*.I++ \
|
||||
*.H \
|
||||
*.HH \
|
||||
*.H++ \
|
||||
*.CS \
|
||||
*.PHP \
|
||||
*.PHP3 \
|
||||
*.M \
|
||||
*.MM \
|
||||
*.PY
|
||||
RECURSIVE = NO
|
||||
EXCLUDE =
|
||||
EXCLUDE_SYMLINKS = NO
|
||||
EXCLUDE_PATTERNS =
|
||||
EXCLUDE_SYMBOLS =
|
||||
EXAMPLE_PATH =
|
||||
EXAMPLE_PATTERNS = *
|
||||
EXAMPLE_RECURSIVE = NO
|
||||
IMAGE_PATH =
|
||||
INPUT_FILTER =
|
||||
FILTER_PATTERNS =
|
||||
FILTER_SOURCE_FILES = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to source browsing
|
||||
#---------------------------------------------------------------------------
|
||||
SOURCE_BROWSER = YES
|
||||
INLINE_SOURCES = NO
|
||||
STRIP_CODE_COMMENTS = YES
|
||||
REFERENCED_BY_RELATION = YES
|
||||
REFERENCES_RELATION = YES
|
||||
REFERENCES_LINK_SOURCE = YES
|
||||
USE_HTAGS = NO
|
||||
VERBATIM_HEADERS = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the alphabetical class index
|
||||
#---------------------------------------------------------------------------
|
||||
ALPHABETICAL_INDEX = NO
|
||||
COLS_IN_ALPHA_INDEX = 5
|
||||
IGNORE_PREFIX =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the HTML output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_HTML = YES
|
||||
HTML_OUTPUT = html
|
||||
HTML_FILE_EXTENSION = .html
|
||||
HTML_HEADER =
|
||||
HTML_FOOTER =
|
||||
HTML_STYLESHEET =
|
||||
HTML_ALIGN_MEMBERS = YES
|
||||
GENERATE_HTMLHELP = NO
|
||||
HTML_DYNAMIC_SECTIONS = NO
|
||||
CHM_FILE =
|
||||
HHC_LOCATION =
|
||||
GENERATE_CHI = NO
|
||||
BINARY_TOC = NO
|
||||
TOC_EXPAND = NO
|
||||
DISABLE_INDEX = NO
|
||||
ENUM_VALUES_PER_LINE = 4
|
||||
GENERATE_TREEVIEW = NO
|
||||
TREEVIEW_WIDTH = 250
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the LaTeX output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_LATEX = YES
|
||||
LATEX_OUTPUT = latex
|
||||
LATEX_CMD_NAME = latex
|
||||
MAKEINDEX_CMD_NAME = makeindex
|
||||
COMPACT_LATEX = NO
|
||||
PAPER_TYPE = a4wide
|
||||
EXTRA_PACKAGES =
|
||||
LATEX_HEADER =
|
||||
PDF_HYPERLINKS = YES
|
||||
USE_PDFLATEX = YES
|
||||
LATEX_BATCHMODE = NO
|
||||
LATEX_HIDE_INDICES = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the RTF output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_RTF = NO
|
||||
RTF_OUTPUT = rtf
|
||||
COMPACT_RTF = NO
|
||||
RTF_HYPERLINKS = NO
|
||||
RTF_STYLESHEET_FILE =
|
||||
RTF_EXTENSIONS_FILE =
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the man page output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_MAN = YES
|
||||
MAN_OUTPUT = man
|
||||
MAN_EXTENSION = .3
|
||||
MAN_LINKS = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the XML output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_XML = NO
|
||||
XML_OUTPUT = xml
|
||||
XML_SCHEMA =
|
||||
XML_DTD =
|
||||
XML_PROGRAMLISTING = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options for the AutoGen Definitions output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_AUTOGEN_DEF = NO
|
||||
#---------------------------------------------------------------------------
|
||||
# configuration options related to the Perl module output
|
||||
#---------------------------------------------------------------------------
|
||||
GENERATE_PERLMOD = NO
|
||||
PERLMOD_LATEX = NO
|
||||
PERLMOD_PRETTY = YES
|
||||
PERLMOD_MAKEVAR_PREFIX =
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration options related to the preprocessor
|
||||
#---------------------------------------------------------------------------
|
||||
ENABLE_PREPROCESSING = YES
|
||||
MACRO_EXPANSION = NO
|
||||
EXPAND_ONLY_PREDEF = NO
|
||||
SEARCH_INCLUDES = YES
|
||||
INCLUDE_PATH =
|
||||
INCLUDE_FILE_PATTERNS =
|
||||
PREDEFINED =
|
||||
EXPAND_AS_DEFINED =
|
||||
SKIP_FUNCTION_MACROS = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration::additions related to external references
|
||||
#---------------------------------------------------------------------------
|
||||
TAGFILES =
|
||||
GENERATE_TAGFILE =
|
||||
ALLEXTERNALS = NO
|
||||
EXTERNAL_GROUPS = YES
|
||||
PERL_PATH = /usr/bin/perl
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration options related to the dot tool
|
||||
#---------------------------------------------------------------------------
|
||||
CLASS_DIAGRAMS = NO
|
||||
MSCGEN_PATH =
|
||||
HIDE_UNDOC_RELATIONS = YES
|
||||
HAVE_DOT = YES
|
||||
CLASS_GRAPH = YES
|
||||
COLLABORATION_GRAPH = YES
|
||||
GROUP_GRAPHS = YES
|
||||
UML_LOOK = NO
|
||||
TEMPLATE_RELATIONS = NO
|
||||
INCLUDE_GRAPH = NO
|
||||
INCLUDED_BY_GRAPH = NO
|
||||
CALL_GRAPH = NO
|
||||
CALLER_GRAPH = NO
|
||||
GRAPHICAL_HIERARCHY = YES
|
||||
DIRECTORY_GRAPH = YES
|
||||
DOT_IMAGE_FORMAT = png
|
||||
DOT_PATH =
|
||||
DOTFILE_DIRS =
|
||||
DOT_GRAPH_MAX_NODES = 50
|
||||
MAX_DOT_GRAPH_DEPTH = 1000
|
||||
DOT_TRANSPARENT = NO
|
||||
DOT_MULTI_TARGETS = NO
|
||||
GENERATE_LEGEND = YES
|
||||
DOT_CLEANUP = YES
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration::additions related to the search engine
|
||||
#---------------------------------------------------------------------------
|
||||
SEARCHENGINE = NO
|
@ -1,5 +0,0 @@
|
||||
To compile:
|
||||
|
||||
./configure
|
||||
make
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user