TeaSpeak-Client/installer/deploy/index.ts

239 lines
8.5 KiB
TypeScript

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();