Fixed prev del

This commit is contained in:
WolverinDEV 2019-10-26 01:51:40 +02:00
parent b956bad3f7
commit 07dfa94740
1039 changed files with 262643 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
.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 Normal file
View File

@ -0,0 +1,14 @@
[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 Normal file
View File

@ -0,0 +1,27 @@
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!

117
build_declarations.sh Executable file
View File

@ -0,0 +1,117 @@
#!/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 Submodule

@ -0,0 +1 @@
Subproject commit 14645dca78396c915ad4ad122d532f24fdfd2969

44
installer/WinInstall.ejs Normal file
View File

@ -0,0 +1,44 @@
; 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

399
installer/build.ts Normal file
View File

@ -0,0 +1,399 @@
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 {}

View File

@ -0,0 +1,143 @@
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();

View File

@ -0,0 +1,3 @@
```$bash
ssh-keygen -t rsa -b 4096 -f ssh_key -N ''
```

239
installer/deploy/index.ts Normal file
View File

@ -0,0 +1,239 @@
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();

View File

@ -0,0 +1,24 @@
@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!
)

51
installer/deploy/ssh_key Normal file
View File

@ -0,0 +1,51 @@
-----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-----

View File

@ -0,0 +1 @@
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

179
installer/package.ts Normal file
View File

@ -0,0 +1,179 @@
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();
});
})
}

View File

@ -0,0 +1,78 @@
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)
});

View File

@ -0,0 +1,88 @@
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);
});

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"sourceMap": true,
"moduleResolution": "node"
},
"include": [
"./deploy/",
"build.ts",
"package.ts",
"package_linux.ts"
]
}

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"sourceMap": true,
"moduleResolution": "node"
},
"include": [
"./deploy/",
"build.ts",
"package.ts",
"package_windows.ts"
]
}

124
jenkins/create_build.sh Executable file
View File

@ -0,0 +1,124 @@
#!/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

11
libraries.txt Normal file
View File

@ -0,0 +1,11 @@
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 Normal file
View File

@ -0,0 +1,31 @@
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();
}

View File

@ -0,0 +1,42 @@
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;
}
}

View File

@ -0,0 +1,863 @@
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";
}

View File

@ -0,0 +1,9 @@
.page {
display: none;
}
.info {
display: block;
}
/*# sourceMappingURL=index.css.map */

View File

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["index.scss"],"names":[],"mappings":"AAAA;EACC;;;AAGD;EACC","file":"index.css"}

View File

@ -0,0 +1,36 @@
<!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>

View File

@ -0,0 +1,7 @@
.page {
display: none;
}
.info {
display: block;
}

View File

@ -0,0 +1,51 @@
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))};
*/

209
modules/core/main.ts Normal file
View File

@ -0,0 +1,209 @@
// 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;

206
modules/core/main_window.ts Normal file
View File

@ -0,0 +1,206 @@
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);
}

View File

@ -0,0 +1,22 @@
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();
}
});

View File

@ -0,0 +1,34 @@
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);
}
});

View File

@ -0,0 +1,147 @@
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");
}
}

View File

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

View File

@ -0,0 +1,515 @@
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

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -0,0 +1,21 @@
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 {}

View File

@ -0,0 +1,98 @@
<!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>

View File

@ -0,0 +1,25 @@
<!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>

View File

@ -0,0 +1,86 @@
#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
}

View File

@ -0,0 +1,34 @@
<!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>

View File

@ -0,0 +1,78 @@
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));

View File

@ -0,0 +1,118 @@
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();
};

View File

@ -0,0 +1,105 @@
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();
}
});

109
modules/core/window.ts Normal file
View File

@ -0,0 +1,109 @@
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);
}
}
}
}
}

View File

@ -0,0 +1,103 @@
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

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,42 @@
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 */

View File

@ -0,0 +1 @@
{"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"}

View File

@ -0,0 +1,36 @@
<!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>

View File

@ -0,0 +1,51 @@
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;
}
}

View File

@ -0,0 +1,30 @@
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));

View File

@ -0,0 +1,82 @@
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)));
}

View File

@ -0,0 +1,121 @@
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);

View File

@ -0,0 +1,510 @@
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 */

View File

@ -0,0 +1,186 @@
/// <reference path="../imports/imports_shared.d.ts" />
window["require_setup"](module);
import * as native from "teaclient_connection";
import * as path from "path";
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(path.dirname(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);

View File

@ -0,0 +1,318 @@
/// <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);

View File

@ -0,0 +1,161 @@
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);
}
}
}
}

View File

@ -0,0 +1,130 @@
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 {};

View File

@ -0,0 +1,38 @@
/// <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
});

View File

@ -0,0 +1,63 @@
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;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,97 @@
/* 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 Normal file
View File

@ -0,0 +1,2 @@
imports_shared.d.ts
imports_shared_loader.d.ts

259
modules/renderer/index.ts Normal file
View File

@ -0,0 +1,259 @@
/// <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");
};

View File

@ -0,0 +1,79 @@
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);
}
};

254
modules/renderer/menu.ts Normal file
View File

@ -0,0 +1,254 @@
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 {};

134
modules/renderer/ppt.ts Normal file
View File

@ -0,0 +1,134 @@
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);

View File

@ -0,0 +1,21 @@
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;

View File

@ -0,0 +1,99 @@
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);
}
}
}

View File

@ -0,0 +1,299 @@
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;

View File

@ -0,0 +1,81 @@
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;
}

View File

@ -0,0 +1,19 @@
{
"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"
]
}

View File

@ -0,0 +1,17 @@
{
"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 Normal file
View File

@ -0,0 +1,3 @@
build/
cmake-build-*
out/*

176
native/CMakeLists.txt Normal file
View File

@ -0,0 +1,176 @@
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 Normal file
View File

@ -0,0 +1,14 @@
# 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-*/

View File

@ -0,0 +1,60 @@
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()

31
native/codec/binding.cc Normal file
View File

@ -0,0 +1,31 @@
#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)

View File

@ -0,0 +1,203 @@
#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

View File

@ -0,0 +1,34 @@
#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
};
}

View File

@ -0,0 +1,205 @@
#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();
}

View File

@ -0,0 +1,95 @@
#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;
};
}

View File

@ -0,0 +1,204 @@
#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

View File

@ -0,0 +1,33 @@
#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
};
}

View File

@ -0,0 +1,234 @@
#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

View File

@ -0,0 +1,36 @@
#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 Normal file
View File

@ -0,0 +1,2 @@
generated/
opus/build

View File

@ -0,0 +1,94 @@
#!/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"

View File

@ -0,0 +1,98 @@
#!/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"

View File

@ -0,0 +1,97 @@
#!/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 Normal file
View File

@ -0,0 +1,33 @@
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

View File

View File

@ -0,0 +1,25 @@
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.

View File

View File

@ -0,0 +1,283 @@
# 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

View File

@ -0,0 +1,281 @@
# 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

View File

@ -0,0 +1,5 @@
To compile:
./configure
make

Some files were not shown because too many files have changed in this diff Show More