Implement update service
This commit is contained in:
217
src/cli.ts
217
src/cli.ts
@ -1,16 +1,27 @@
|
||||
import * as cp from "child_process";
|
||||
import * as os from "os";
|
||||
|
||||
import { main as vsCli } from "vs/code/node/cliProcessMain";
|
||||
import { validatePaths } from "vs/code/node/paths";
|
||||
import { parseMainProcessArgv } from "vs/platform/environment/node/argvHelper";
|
||||
import { ParsedArgs } from "vs/platform/environment/common/environment";
|
||||
import { buildHelpMessage, buildVersionMessage, options } from "vs/platform/environment/node/argv";
|
||||
import { ParsedArgs } from "vs/platform/environment/common/environment";
|
||||
import pkg from "vs/platform/product/node/package";
|
||||
import product from "vs/platform/product/node/product";
|
||||
|
||||
import { ipcMain } from "vs/server/src/ipc";
|
||||
|
||||
product.extensionsGallery = {
|
||||
serviceUrl: process.env.SERVICE_URL || "https://v1.extapi.coder.com",
|
||||
itemUrl: process.env.ITEM_URL || "",
|
||||
controlUrl: "",
|
||||
recommendationsUrl: "",
|
||||
...(product.extensionsGallery || {}),
|
||||
};
|
||||
|
||||
import { MainServer } from "vs/server/src/server";
|
||||
import { enableExtensionTars } from "vs/server/src/tar";
|
||||
import { AuthType, buildAllowedMessage, generateCertificate, generatePassword, localRequire, open, unpackExecutables } from "vs/server/src/util";
|
||||
import { main as vsCli } from "vs/code/node/cliProcessMain";
|
||||
|
||||
const { logger } = localRequire<typeof import("@coder/logger/out/index")>("@coder/logger/out/index");
|
||||
|
||||
@ -27,88 +38,57 @@ interface Args extends ParsedArgs {
|
||||
socket?: string;
|
||||
}
|
||||
|
||||
// The last item is _ which is like -- so our options need to come before it.
|
||||
const last = options.pop()!;
|
||||
const getArgs = (): Args => {
|
||||
// The last item is _ which is like -- so our options need to come before it.
|
||||
const last = options.pop()!;
|
||||
|
||||
// Remove options that won't work or don't make sense.
|
||||
let i = options.length;
|
||||
while (i--) {
|
||||
switch (options[i].id) {
|
||||
case "add":
|
||||
case "diff":
|
||||
case "file-uri":
|
||||
case "folder-uri":
|
||||
case "goto":
|
||||
case "new-window":
|
||||
case "reuse-window":
|
||||
case "wait":
|
||||
case "disable-gpu":
|
||||
// TODO: pretty sure these don't work but not 100%.
|
||||
case "max-memory":
|
||||
case "prof-startup":
|
||||
case "inspect-extensions":
|
||||
case "inspect-brk-extensions":
|
||||
options.splice(i, 1);
|
||||
break;
|
||||
// Remove options that won't work or don't make sense.
|
||||
let i = options.length;
|
||||
while (i--) {
|
||||
switch (options[i].id) {
|
||||
case "add":
|
||||
case "diff":
|
||||
case "file-uri":
|
||||
case "folder-uri":
|
||||
case "goto":
|
||||
case "new-window":
|
||||
case "reuse-window":
|
||||
case "wait":
|
||||
case "disable-gpu":
|
||||
// TODO: pretty sure these don't work but not 100%.
|
||||
case "max-memory":
|
||||
case "prof-startup":
|
||||
case "inspect-extensions":
|
||||
case "inspect-brk-extensions":
|
||||
options.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
options.push({ id: "base-path", type: "string", cat: "o", description: "Base path of the URL at which code-server is hosted (used for login redirects)." });
|
||||
options.push({ id: "cert", type: "string", cat: "o", description: "Path to certificate. If the path is omitted, both this and --cert-key will be generated." });
|
||||
options.push({ id: "cert-key", type: "string", cat: "o", description: "Path to the certificate's key if one was provided." });
|
||||
options.push({ id: "extra-builtin-extensions-dir", type: "string", cat: "o", description: "Path to an extra builtin extension directory." });
|
||||
options.push({ id: "extra-extensions-dir", type: "string", cat: "o", description: "Path to an extra user extension directory." });
|
||||
options.push({ id: "host", type: "string", cat: "o", description: "Host for the server." });
|
||||
options.push({ id: "auth", type: "string", cat: "o", description: `The type of authentication to use. ${buildAllowedMessage(AuthType)}.` });
|
||||
options.push({ id: "open", type: "boolean", cat: "o", description: "Open in the browser on startup." });
|
||||
options.push({ id: "port", type: "string", cat: "o", description: "Port for the main server." });
|
||||
options.push({ id: "socket", type: "string", cat: "o", description: "Listen on a socket instead of host:port." });
|
||||
options.push({ id: "base-path", type: "string", cat: "o", description: "Base path of the URL at which code-server is hosted (used for login redirects)." });
|
||||
options.push({ id: "cert", type: "string", cat: "o", description: "Path to certificate. If the path is omitted, both this and --cert-key will be generated." });
|
||||
options.push({ id: "cert-key", type: "string", cat: "o", description: "Path to the certificate's key if one was provided." });
|
||||
options.push({ id: "extra-builtin-extensions-dir", type: "string", cat: "o", description: "Path to an extra builtin extension directory." });
|
||||
options.push({ id: "extra-extensions-dir", type: "string", cat: "o", description: "Path to an extra user extension directory." });
|
||||
options.push({ id: "host", type: "string", cat: "o", description: "Host for the server." });
|
||||
options.push({ id: "auth", type: "string", cat: "o", description: `The type of authentication to use. ${buildAllowedMessage(AuthType)}.` });
|
||||
options.push({ id: "open", type: "boolean", cat: "o", description: "Open in the browser on startup." });
|
||||
options.push({ id: "port", type: "string", cat: "o", description: "Port for the main server." });
|
||||
options.push({ id: "socket", type: "string", cat: "o", description: "Listen on a socket instead of host:port." });
|
||||
|
||||
options.push(last);
|
||||
options.push(last);
|
||||
|
||||
const main = async (): Promise<void | void[]> => {
|
||||
const args = validatePaths(parseMainProcessArgv(process.argv)) as Args;
|
||||
["extra-extensions-dir", "extra-builtin-extensions-dir"].forEach((key) => {
|
||||
if (typeof args[key] === "string") {
|
||||
args[key] = [args[key]];
|
||||
}
|
||||
});
|
||||
return args;
|
||||
};
|
||||
|
||||
if (!product.extensionsGallery) {
|
||||
product.extensionsGallery = {
|
||||
serviceUrl: process.env.SERVICE_URL || "https://v1.extapi.coder.com",
|
||||
itemUrl: process.env.ITEM_URL || "",
|
||||
controlUrl: "",
|
||||
recommendationsUrl: "",
|
||||
};
|
||||
}
|
||||
|
||||
const version = `${(pkg as any).codeServerVersion || "development"}-vsc${pkg.version}`;
|
||||
if (args.help) {
|
||||
const executable = `${product.applicationName}${os.platform() === "win32" ? ".exe" : ""}`;
|
||||
return console.log(buildHelpMessage(product.nameLong, executable, version, undefined, false));
|
||||
}
|
||||
|
||||
if (args.version) {
|
||||
return buildVersionMessage(version, product.commit).split("\n").map((line) => logger.info(line));
|
||||
}
|
||||
|
||||
enableExtensionTars();
|
||||
|
||||
const shouldSpawnCliProcess = (): boolean => {
|
||||
return !!args["install-source"]
|
||||
|| !!args["list-extensions"]
|
||||
|| !!args["install-extension"]
|
||||
|| !!args["uninstall-extension"]
|
||||
|| !!args["locate-extension"]
|
||||
|| !!args["telemetry"];
|
||||
};
|
||||
|
||||
if (shouldSpawnCliProcess()) {
|
||||
await vsCli(args);
|
||||
return process.exit(0); // There is a WriteStream instance keeping it open.
|
||||
}
|
||||
|
||||
const startVscode = async (): Promise<void | void[]> => {
|
||||
const args = getArgs();
|
||||
const extra = args["_"] || [];
|
||||
const options = {
|
||||
auth: args.auth,
|
||||
@ -136,6 +116,8 @@ const main = async (): Promise<void | void[]> => {
|
||||
options.certKey = certKey;
|
||||
}
|
||||
|
||||
enableExtensionTars();
|
||||
|
||||
const server = new MainServer({
|
||||
...options,
|
||||
port: typeof args.port !== "undefined" && parseInt(args.port, 10) || 8443,
|
||||
@ -168,14 +150,99 @@ const main = async (): Promise<void | void[]> => {
|
||||
}
|
||||
|
||||
if (!server.options.socket && args.open) {
|
||||
// The web socket doesn't seem to work if using 0.0.0.0.
|
||||
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
||||
const openAddress = `http://localhost:${server.options.port}`;
|
||||
await open(openAddress).catch(console.error);
|
||||
logger.info(` - Opened ${openAddress}`);
|
||||
}
|
||||
};
|
||||
|
||||
const startCli = (): boolean | Promise<void> => {
|
||||
const args = getArgs();
|
||||
if (args.help) {
|
||||
const executable = `${product.applicationName}${os.platform() === "win32" ? ".exe" : ""}`;
|
||||
console.log(buildHelpMessage(product.nameLong, executable, pkg.codeServerVersion, undefined, false));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.version) {
|
||||
buildVersionMessage(pkg.codeServerVersion, product.commit).split("\n").map((line) => logger.info(line));
|
||||
return true;
|
||||
}
|
||||
|
||||
const shouldSpawnCliProcess = (): boolean => {
|
||||
return !!args["install-source"]
|
||||
|| !!args["list-extensions"]
|
||||
|| !!args["install-extension"]
|
||||
|| !!args["uninstall-extension"]
|
||||
|| !!args["locate-extension"]
|
||||
|| !!args["telemetry"];
|
||||
};
|
||||
|
||||
if (shouldSpawnCliProcess()) {
|
||||
enableExtensionTars();
|
||||
return vsCli(args);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export class WrapperProcess {
|
||||
private process?: cp.ChildProcess;
|
||||
private started?: Promise<void>;
|
||||
|
||||
public constructor() {
|
||||
ipcMain.onMessage(async (message) => {
|
||||
switch (message) {
|
||||
case "relaunch":
|
||||
logger.info("Relaunching...");
|
||||
this.started = undefined;
|
||||
if (this.process) {
|
||||
this.process.kill();
|
||||
}
|
||||
try {
|
||||
await this.start();
|
||||
} catch (error) {
|
||||
logger.error(error.message);
|
||||
process.exit(typeof error.code === "number" ? error.code : 1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.error(`Unrecognized message ${message}`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public start(): Promise<void> {
|
||||
if (!this.started) {
|
||||
const child = this.spawn();
|
||||
this.started = ipcMain.handshake(child);
|
||||
this.process = child;
|
||||
}
|
||||
return this.started;
|
||||
}
|
||||
|
||||
private spawn(): cp.ChildProcess {
|
||||
return cp.spawn(process.argv[0], process.argv.slice(1), {
|
||||
env: {
|
||||
...process.env,
|
||||
LAUNCH_VSCODE: "true",
|
||||
},
|
||||
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const main = async(): Promise<boolean | void | void[]> => {
|
||||
if (process.env.LAUNCH_VSCODE) {
|
||||
await ipcMain.handshake();
|
||||
return startVscode();
|
||||
}
|
||||
return startCli() || new WrapperProcess().start();
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
logger.error(error.message);
|
||||
process.exit(typeof error.code === "number" ? error.code : 1);
|
||||
});
|
||||
|
53
src/ipc.ts
Normal file
53
src/ipc.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import * as cp from "child_process";
|
||||
|
||||
import { Emitter } from "vs/base/common/event";
|
||||
|
||||
enum ControlMessage {
|
||||
okToChild = "ok>",
|
||||
okFromChild = "ok<",
|
||||
}
|
||||
|
||||
export type Message = "relaunch";
|
||||
|
||||
class IpcMain {
|
||||
protected readonly _onMessage = new Emitter<Message>();
|
||||
public readonly onMessage = this._onMessage.event;
|
||||
|
||||
public handshake(child?: cp.ChildProcess): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const target = child || process;
|
||||
if (!target.send) {
|
||||
throw new Error("Not spawned with IPC enabled");
|
||||
}
|
||||
target.on("message", (message) => {
|
||||
if (message === child ? ControlMessage.okFromChild : ControlMessage.okToChild) {
|
||||
target.removeAllListeners();
|
||||
target.on("message", (msg) => this._onMessage.fire(msg));
|
||||
if (child) {
|
||||
target.send!(ControlMessage.okToChild);
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
if (child) {
|
||||
child.once("error", reject);
|
||||
child.once("exit", (code) => {
|
||||
const error = new Error(`Unexpected exit with code ${code}`);
|
||||
(error as any).code = code;
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
target.send(ControlMessage.okFromChild);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public relaunch(): void {
|
||||
if (!process.send) {
|
||||
throw new Error("Not a child process with IPC enabled");
|
||||
}
|
||||
process.send("relaunch");
|
||||
}
|
||||
}
|
||||
|
||||
export const ipcMain = new IpcMain();
|
@ -53,6 +53,7 @@ import { AppInsightsAppender } from "vs/platform/telemetry/node/appInsightsAppen
|
||||
import { resolveCommonProperties } from "vs/platform/telemetry/node/commonProperties";
|
||||
import { RemoteExtensionLogFileName } from "vs/workbench/services/remote/common/remoteAgentService";
|
||||
import { TelemetryChannel } from "vs/platform/telemetry/node/telemetryIpc";
|
||||
import { UpdateChannel } from "vs/platform/update/node/updateIpc";
|
||||
import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api";
|
||||
|
||||
import { Connection, ManagementConnection, ExtensionHostConnection } from "vs/server/src/connection";
|
||||
@ -60,6 +61,7 @@ import { ExtensionEnvironmentChannel, FileProviderChannel , } from "vs/server/sr
|
||||
import { TelemetryClient } from "vs/server/src/insights";
|
||||
import { getNlsConfiguration, getLocaleFromConfig } from "vs/server/src/nls";
|
||||
import { Protocol } from "vs/server/src/protocol";
|
||||
import { UpdateService } from "vs/server/src/update";
|
||||
import { AuthType, getMediaMime, getUriTransformer, localRequire, tmpdir } from "vs/server/src/util";
|
||||
|
||||
export enum HttpCode {
|
||||
@ -482,7 +484,11 @@ export class MainServer extends Server {
|
||||
REMOTE_USER_DATA_URI: transformer.transformOutgoing(
|
||||
(this.services.get(IEnvironmentService) as EnvironmentService).webUserDataHome,
|
||||
),
|
||||
PRODUCT_CONFIGURATION: product,
|
||||
PRODUCT_CONFIGURATION: {
|
||||
...product,
|
||||
// @ts-ignore workaround for getting the VS Code version to the browser.
|
||||
version: pkg.version,
|
||||
},
|
||||
CONNECTION_AUTH_TOKEN: "",
|
||||
NLS_CONFIGURATION: await getNlsConfiguration(locale, environment.userDataPath),
|
||||
};
|
||||
@ -560,14 +566,13 @@ export class MainServer extends Server {
|
||||
this.services.set(IRequestService, new SyncDescriptor(RequestService));
|
||||
this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService));
|
||||
if (!environmentService.args["disable-telemetry"]) {
|
||||
const version = `${(pkg as any).codeServerVersion || "development"}-vsc${pkg.version}`;
|
||||
this.services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [{
|
||||
appender: combinedAppender(
|
||||
new AppInsightsAppender("code-server", null, () => new TelemetryClient(), logService),
|
||||
new LogAppender(logService),
|
||||
),
|
||||
commonProperties: resolveCommonProperties(
|
||||
product.commit, version, await getMachineId(),
|
||||
product.commit, pkg.codeServerVersion, await getMachineId(),
|
||||
environmentService.installSourcePath, "code-server",
|
||||
),
|
||||
piiPaths: [
|
||||
@ -601,6 +606,8 @@ export class MainServer extends Server {
|
||||
this.ipc.registerChannel("gallery", galleryChannel);
|
||||
const telemetryChannel = new TelemetryChannel(telemetryService);
|
||||
this.ipc.registerChannel("telemetry", telemetryChannel);
|
||||
const updateChannel = new UpdateChannel(instantiationService.createInstance(UpdateService));
|
||||
this.ipc.registerChannel("update", updateChannel);
|
||||
resolve(new ErrorTelemetry(telemetryService));
|
||||
});
|
||||
});
|
||||
|
136
src/update.ts
Normal file
136
src/update.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import * as cp from "child_process";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as util from "util";
|
||||
import * as zlib from 'zlib';
|
||||
|
||||
import { CancellationToken } from "vs/base/common/cancellation";
|
||||
import * as pfs from "vs/base/node/pfs";
|
||||
import { asJson, download } from "vs/base/node/request";
|
||||
import { IConfigurationService } from "vs/platform/configuration/common/configuration";
|
||||
import { IEnvironmentService } from "vs/platform/environment/common/environment";
|
||||
import { ILogService } from "vs/platform/log/common/log";
|
||||
import pkg from "vs/platform/product/node/package";
|
||||
import { IRequestService } from "vs/platform/request/node/request";
|
||||
import { State, UpdateType, StateType, AvailableForDownload } from "vs/platform/update/common/update";
|
||||
import { AbstractUpdateService } from "vs/platform/update/electron-main/abstractUpdateService";
|
||||
|
||||
import { ipcMain } from "vs/server/src/ipc";
|
||||
import { tmpdir } from "vs/server/src/util";
|
||||
import { extract } from "vs/server/src/tar";
|
||||
|
||||
interface IUpdate {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class UpdateService extends AbstractUpdateService {
|
||||
_serviceBrand: any;
|
||||
|
||||
constructor(
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IRequestService requestService: IRequestService,
|
||||
@ILogService logService: ILogService
|
||||
) {
|
||||
super(null, configurationService, environmentService, requestService, logService);
|
||||
}
|
||||
|
||||
public async isLatestVersion(): Promise<boolean | undefined> {
|
||||
const latest = await this.getLatestVersion();
|
||||
return !latest || latest.name === pkg.codeServerVersion;
|
||||
}
|
||||
|
||||
protected buildUpdateFeedUrl(): string {
|
||||
return "https://api.github.com/repos/cdr/code-server/releases/latest";
|
||||
}
|
||||
|
||||
protected doQuitAndInstall(): void {
|
||||
ipcMain.relaunch();
|
||||
}
|
||||
|
||||
protected async doCheckForUpdates(context: any): Promise<void> {
|
||||
if (this.state.type !== StateType.Idle) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.setState(State.CheckingForUpdates(context));
|
||||
try {
|
||||
const update = await this.getLatestVersion();
|
||||
if (!update || !update.name || update.name === pkg.codeServerVersion) {
|
||||
this.setState(State.Idle(UpdateType.Archive));
|
||||
} else {
|
||||
this.setState(State.AvailableForDownload({
|
||||
version: update.name,
|
||||
productVersion: update.name,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
this.onRequestError(error, !!context);
|
||||
}
|
||||
}
|
||||
|
||||
private async getLatestVersion(): Promise<IUpdate | null> {
|
||||
const data = await this.requestService.request({
|
||||
url: this.url,
|
||||
headers: {
|
||||
"User-Agent": "code-server",
|
||||
},
|
||||
}, CancellationToken.None);
|
||||
return asJson(data);
|
||||
}
|
||||
|
||||
protected async doDownloadUpdate(state: AvailableForDownload): Promise<void> {
|
||||
this.setState(State.Updating(state.update));
|
||||
const target = os.platform();
|
||||
const releaseName = await this.buildReleaseName(state.update.version);
|
||||
const url = "https://github.com/cdr/code-server/releases/download/"
|
||||
+ `${state.update.version}/${releaseName}`
|
||||
+ `.${target === "darwin" ? "zip" : "tar.gz"}`;
|
||||
const downloadPath = path.join(tmpdir, `${state.update.version}-archive`);
|
||||
const extractPath = path.join(tmpdir, state.update.version);
|
||||
try {
|
||||
await pfs.mkdirp(tmpdir);
|
||||
const context = await this.requestService.request({ url }, CancellationToken.None);
|
||||
// Decompress the gzip as we download. If the gzip encoding is set then
|
||||
// the request service already does this.
|
||||
if (target !== "darwin" && context.res.headers["content-encoding"] !== "gzip") {
|
||||
context.stream = context.stream.pipe(zlib.createGunzip());
|
||||
}
|
||||
await download(downloadPath, context);
|
||||
await extract(downloadPath, extractPath, undefined, CancellationToken.None);
|
||||
const newBinary = path.join(extractPath, releaseName, "code-server");
|
||||
if (!pfs.exists(newBinary)) {
|
||||
throw new Error("No code-server binary in extracted archive");
|
||||
}
|
||||
await pfs.unlink(process.argv[0]); // Must unlink first to avoid ETXTBSY.
|
||||
await pfs.move(newBinary, process.argv[0]);
|
||||
this.setState(State.Ready(state.update));
|
||||
} catch (error) {
|
||||
this.onRequestError(error, true);
|
||||
}
|
||||
await Promise.all([downloadPath, extractPath].map((p) => pfs.rimraf(p)));
|
||||
}
|
||||
|
||||
private onRequestError(error: Error, showNotification?: boolean): void {
|
||||
this.logService.error(error);
|
||||
const message: string | undefined = showNotification ? (error.message || error.toString()) : undefined;
|
||||
this.setState(State.Idle(UpdateType.Archive, message));
|
||||
}
|
||||
|
||||
private async buildReleaseName(release: string): Promise<string> {
|
||||
let target: string = os.platform();
|
||||
if (target === "linux") {
|
||||
const result = await util.promisify(cp.exec)("ldd --version");
|
||||
if (result.stderr) {
|
||||
throw new Error(result.stderr);
|
||||
}
|
||||
if (result.stdout.indexOf("musl") !== -1) {
|
||||
target = "alpine";
|
||||
}
|
||||
}
|
||||
let arch = os.arch();
|
||||
if (arch === "x64") {
|
||||
arch = "x86_64";
|
||||
}
|
||||
return `code-server${release}-${target}-${arch}`;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user