mirror of
https://github.com/hanzoai/code.git
synced 2026-09-30 20:53:35 +00:00
354 lines
11 KiB
TypeScript
354 lines
11 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as crypto from 'crypto';
|
|
import Log from './common/logger';
|
|
import { getVSCodeServerConfig } from './serverConfig';
|
|
import { WSLManager } from './wsl/wslManager';
|
|
|
|
export interface ServerInstallOptions {
|
|
id: string;
|
|
quality: string;
|
|
commit: string;
|
|
version: string;
|
|
release?: string; // void specific
|
|
extensionIds: string[];
|
|
envVariables: string[];
|
|
serverApplicationName: string;
|
|
serverDataFolderName: string;
|
|
serverDownloadUrlTemplate: string;
|
|
}
|
|
|
|
export interface ServerInstallResult {
|
|
exitCode: number;
|
|
listeningOn: number;
|
|
connectionToken: string;
|
|
logFile: string;
|
|
osReleaseId: string;
|
|
arch: string;
|
|
platform: string;
|
|
tmpDir: string;
|
|
[key: string]: any;
|
|
}
|
|
|
|
export class ServerInstallError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz';
|
|
|
|
export async function installCodeServer(wslManager: WSLManager, distroName: string, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], logger: Log): Promise<ServerInstallResult> {
|
|
const scriptId = crypto.randomBytes(12).toString('hex');
|
|
|
|
const vscodeServerConfig = await getVSCodeServerConfig();
|
|
const installOptions: ServerInstallOptions = {
|
|
id: scriptId,
|
|
version: vscodeServerConfig.version,
|
|
commit: vscodeServerConfig.commit,
|
|
quality: vscodeServerConfig.quality,
|
|
release: vscodeServerConfig.release,
|
|
extensionIds,
|
|
envVariables,
|
|
serverApplicationName: vscodeServerConfig.serverApplicationName,
|
|
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
|
|
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
|
|
};
|
|
|
|
const installServerScript = generateBashInstallScript(installOptions);
|
|
|
|
// Fish shell does not support heredoc so let's workaround it using -c option,
|
|
// also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676
|
|
const resp = await wslManager.exec('bash', ['-c', `'${installServerScript.replace(/'/g, `'\\''`)}'`], distroName);
|
|
|
|
const endScriptRegex = new RegExp(`${scriptId}: Server installation script done`, 'm');
|
|
const commandOutput = await Promise.race([
|
|
resp.exitPromise.then(result => ({ stdout: resp.stdout, stderr: resp.stderr, exitCode: result.exitCode })),
|
|
new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve) => {
|
|
resp.onStdoutData(buffer => {
|
|
if (endScriptRegex.test(buffer.toString('utf8'))) {
|
|
resolve({ stdout: resp.stdout, stderr: resp.stderr, exitCode: 0 });
|
|
}
|
|
});
|
|
})
|
|
]);
|
|
|
|
if (commandOutput.exitCode) {
|
|
logger.trace('Server install command stderr:', commandOutput.stderr);
|
|
}
|
|
logger.trace('Server install command stdout:', commandOutput.stdout);
|
|
|
|
const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId);
|
|
if (!resultMap) {
|
|
throw new ServerInstallError(`Failed parsing install script output`);
|
|
}
|
|
|
|
const exitCode = parseInt(resultMap.exitCode, 10);
|
|
if (exitCode !== 0) {
|
|
throw new ServerInstallError(`Couldn't install void server on remote server, install script returned non-zero exit status`);
|
|
}
|
|
|
|
const listeningOn = parseInt(resultMap.listeningOn, 10);
|
|
|
|
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
|
|
|
|
return {
|
|
exitCode,
|
|
listeningOn,
|
|
connectionToken: resultMap.connectionToken,
|
|
logFile: resultMap.logFile,
|
|
osReleaseId: resultMap.osReleaseId,
|
|
arch: resultMap.arch,
|
|
platform: resultMap.platform,
|
|
tmpDir: resultMap.tmpDir,
|
|
...remoteEnvVars
|
|
};
|
|
}
|
|
|
|
function parseServerInstallOutput(str: string, scriptId: string): { [k: string]: string } | undefined {
|
|
const startResultStr = `${scriptId}: start`;
|
|
const endResultStr = `${scriptId}: end`;
|
|
|
|
const startResultIdx = str.indexOf(startResultStr);
|
|
if (startResultIdx < 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
|
|
if (endResultIdx < 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx);
|
|
|
|
const resultMap: { [k: string]: string } = {};
|
|
const resultArr = installResult.split(/\r?\n/);
|
|
for (const line of resultArr) {
|
|
const [key, value] = line.split('==');
|
|
resultMap[key] = value;
|
|
}
|
|
|
|
return resultMap;
|
|
}
|
|
|
|
function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
|
|
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
|
|
return `
|
|
# Server installation script
|
|
|
|
TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"
|
|
|
|
DISTRO_VERSION="${version}"
|
|
DISTRO_COMMIT="${commit}"
|
|
DISTRO_QUALITY="${quality}"
|
|
DISTRO_VSCODIUM_RELEASE="${release ?? ''}"
|
|
|
|
SERVER_APP_NAME="${serverApplicationName}"
|
|
SERVER_INITIAL_EXTENSIONS="${extensions}"
|
|
SERVER_LISTEN_FLAG="--port=0"
|
|
SERVER_DATA_DIR="$HOME/${serverDataFolderName}"
|
|
SERVER_DIR="$SERVER_DATA_DIR/bin/$DISTRO_COMMIT"
|
|
SERVER_SCRIPT="$SERVER_DIR/bin/$SERVER_APP_NAME"
|
|
SERVER_LOGFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.log"
|
|
SERVER_PIDFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.pid"
|
|
SERVER_TOKENFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.token"
|
|
SERVER_OS=
|
|
SERVER_ARCH=
|
|
SERVER_CONNECTION_TOKEN=
|
|
SERVER_DOWNLOAD_URL=
|
|
|
|
LISTENING_ON=
|
|
OS_RELEASE_ID=
|
|
ARCH=
|
|
PLATFORM=
|
|
|
|
# Mimic output from logs of remote-ssh extension
|
|
print_install_results_and_exit() {
|
|
echo "${id}: start"
|
|
echo "exitCode==$1=="
|
|
echo "listeningOn==$LISTENING_ON=="
|
|
echo "connectionToken==$SERVER_CONNECTION_TOKEN=="
|
|
echo "logFile==$SERVER_LOGFILE=="
|
|
echo "osReleaseId==$OS_RELEASE_ID=="
|
|
echo "arch==$ARCH=="
|
|
echo "platform==$PLATFORM=="
|
|
echo "tmpDir==$TMP_DIR=="
|
|
${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')}
|
|
echo "${id}: end"
|
|
exit 0
|
|
}
|
|
|
|
# Check if platform is supported
|
|
PLATFORM="$(uname -s)"
|
|
case $PLATFORM in
|
|
Linux)
|
|
SERVER_OS="linux"
|
|
;;
|
|
*)
|
|
echo "Error platform not supported: $PLATFORM"
|
|
print_install_results_and_exit 1
|
|
;;
|
|
esac
|
|
|
|
# Check machine architecture
|
|
ARCH="$(uname -m)"
|
|
case $ARCH in
|
|
x86_64 | amd64)
|
|
SERVER_ARCH="x64"
|
|
;;
|
|
armv7l | armv8l)
|
|
SERVER_ARCH="armhf"
|
|
;;
|
|
arm64 | aarch64)
|
|
SERVER_ARCH="arm64"
|
|
;;
|
|
*)
|
|
echo "Error architecture not supported: $ARCH"
|
|
print_install_results_and_exit 1
|
|
;;
|
|
esac
|
|
|
|
# https://www.freedesktop.org/software/systemd/man/os-release.html
|
|
OS_RELEASE_ID="$(grep -i '^ID=' /etc/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
|
|
if [[ -z $OS_RELEASE_ID ]]; then
|
|
OS_RELEASE_ID="$(grep -i '^ID=' /usr/lib/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
|
|
if [[ -z $OS_RELEASE_ID ]]; then
|
|
OS_RELEASE_ID="unknown"
|
|
fi
|
|
fi
|
|
|
|
# Create installation folder
|
|
if [[ ! -d $SERVER_DIR ]]; then
|
|
mkdir -p $SERVER_DIR
|
|
if (( $? > 0 )); then
|
|
echo "Error creating server install directory"
|
|
print_install_results_and_exit 1
|
|
fi
|
|
fi
|
|
|
|
SERVER_DOWNLOAD_URL="$(echo "${serverDownloadUrlTemplate.replace(/\$\{/g, '\\${')}" | sed "s/\\\${quality}/$DISTRO_QUALITY/g" | sed "s/\\\${version}/$DISTRO_VERSION/g" | sed "s/\\\${commit}/$DISTRO_COMMIT/g" | sed "s/\\\${os}/$SERVER_OS/g" | sed "s/\\\${arch}/$SERVER_ARCH/g" | sed "s/\\\${release}/$DISTRO_VSCODIUM_RELEASE/g")"
|
|
|
|
# Check if server script is already installed
|
|
if [[ ! -f $SERVER_SCRIPT ]]; then
|
|
if [[ "$SERVER_OS" = "dragonfly" ]] || [[ "$SERVER_OS" = "freebsd" ]]; then
|
|
echo "Error "$SERVER_OS" needs manual installation of remote extension host"
|
|
print_install_results_and_exit 1
|
|
fi
|
|
|
|
pushd $SERVER_DIR > /dev/null
|
|
|
|
if [[ ! -z $(which wget) ]]; then
|
|
wget --tries=3 --timeout=10 --continue --no-verbose -O vscode-server.tar.gz $SERVER_DOWNLOAD_URL
|
|
elif [[ ! -z $(which curl) ]]; then
|
|
curl --retry 3 --connect-timeout 10 --location --show-error --silent --output vscode-server.tar.gz $SERVER_DOWNLOAD_URL
|
|
else
|
|
echo "Error no tool to download server binary"
|
|
print_install_results_and_exit 1
|
|
fi
|
|
|
|
if (( $? > 0 )); then
|
|
echo "Error downloading server from $SERVER_DOWNLOAD_URL"
|
|
print_install_results_and_exit 1
|
|
fi
|
|
|
|
tar -xf vscode-server.tar.gz --strip-components 1
|
|
if (( $? > 0 )); then
|
|
echo "Error while extracting server contents"
|
|
print_install_results_and_exit 1
|
|
fi
|
|
|
|
if [[ ! -f $SERVER_SCRIPT ]]; then
|
|
echo "Error server contents are corrupted"
|
|
print_install_results_and_exit 1
|
|
fi
|
|
|
|
rm -f vscode-server.tar.gz
|
|
|
|
popd > /dev/null
|
|
else
|
|
echo "Server script already installed in $SERVER_SCRIPT"
|
|
fi
|
|
|
|
# Try to find if server is already running
|
|
if [[ -f $SERVER_PIDFILE ]]; then
|
|
SERVER_PID="$(cat $SERVER_PIDFILE)"
|
|
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
|
else
|
|
SERVER_RUNNING_PROCESS="$(ps -o pid,args -A | grep $SERVER_SCRIPT | grep -v grep)"
|
|
fi
|
|
|
|
if [[ -z $SERVER_RUNNING_PROCESS ]]; then
|
|
if [[ -f $SERVER_LOGFILE ]]; then
|
|
rm $SERVER_LOGFILE
|
|
fi
|
|
if [[ -f $SERVER_TOKENFILE ]]; then
|
|
rm $SERVER_TOKENFILE
|
|
fi
|
|
|
|
touch $SERVER_TOKENFILE
|
|
chmod 600 $SERVER_TOKENFILE
|
|
SERVER_CONNECTION_TOKEN="${crypto.randomUUID()}"
|
|
echo $SERVER_CONNECTION_TOKEN > $SERVER_TOKENFILE
|
|
|
|
$SERVER_SCRIPT --start-server --host=127.0.0.1 $SERVER_LISTEN_FLAG $SERVER_INITIAL_EXTENSIONS --connection-token-file $SERVER_TOKENFILE --telemetry-level off --use-host-proxy --disable-websocket-compression --without-browser-env-var --enable-remote-auto-shutdown --accept-server-license-terms &> $SERVER_LOGFILE &
|
|
echo $! > $SERVER_PIDFILE
|
|
else
|
|
echo "Server script is already running $SERVER_SCRIPT"
|
|
fi
|
|
|
|
if [[ -f $SERVER_TOKENFILE ]]; then
|
|
SERVER_CONNECTION_TOKEN="$(cat $SERVER_TOKENFILE)"
|
|
else
|
|
echo "Error server token file not found $SERVER_TOKENFILE"
|
|
print_install_results_and_exit 1
|
|
fi
|
|
|
|
if [[ -f $SERVER_LOGFILE ]]; then
|
|
for i in {1..5}; do
|
|
LISTENING_ON="$(cat $SERVER_LOGFILE | grep -E 'Extension host agent listening on .+' | sed 's/Extension host agent listening on //')"
|
|
if [[ -n $LISTENING_ON ]]; then
|
|
break
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
|
|
if [[ -z $LISTENING_ON ]]; then
|
|
echo "Error server did not start sucessfully"
|
|
print_install_results_and_exit 1
|
|
fi
|
|
else
|
|
echo "Error server log file not found $SERVER_LOGFILE"
|
|
print_install_results_and_exit 1
|
|
fi
|
|
|
|
# Finish server setup and keep script running
|
|
if [[ -z $SERVER_RUNNING_PROCESS ]]; then
|
|
echo "${id}: start"
|
|
echo "exitCode==0=="
|
|
echo "listeningOn==$LISTENING_ON=="
|
|
echo "connectionToken==$SERVER_CONNECTION_TOKEN=="
|
|
echo "logFile==$SERVER_LOGFILE=="
|
|
echo "osReleaseId==$OS_RELEASE_ID=="
|
|
echo "arch==$ARCH=="
|
|
echo "platform==$PLATFORM=="
|
|
echo "tmpDir==$TMP_DIR=="
|
|
${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')}
|
|
echo "${id}: end"
|
|
|
|
echo "${id}: Server installation script done"
|
|
|
|
SERVER_PID="$(cat $SERVER_PIDFILE)"
|
|
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
|
while [[ -n $SERVER_RUNNING_PROCESS ]]; do
|
|
sleep 300;
|
|
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
|
done
|
|
else
|
|
print_install_results_and_exit 0
|
|
fi
|
|
`;
|
|
}
|