未验证 提交 f5a6f14a 编写于 作者: A Asher

Implement update service

上级 01a9ab33
......@@ -74,7 +74,7 @@ function build-code-server() {
cd "${buildPath}" && yarn --production --force --build-from-source
rm "${buildPath}/"{package.json,yarn.lock,.yarnrc}
local packageJson="{\"codeServerVersion\": \"${codeServerVersion}\"}"
local packageJson="{\"codeServerVersion\": \"${codeServerVersion}-vsc${vscodeVersion}\"}"
cp -r "${sourcePath}/.build/extensions" "${buildPath}"
node "${rootPath}/scripts/merge.js" "${sourcePath}/package.json" "${rootPath}/scripts/package.json" "${buildPath}/package.json" "${packageJson}"
node "${rootPath}/scripts/merge.js" "${sourcePath}/.build/product.json" "${rootPath}/scripts/product.json" "${buildPath}/product.json"
......
此差异已折叠。
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()!;
// 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;
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;
}
}
}
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);
});
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));
});
});
......
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}`;
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册