bdd11f741b
Also too the opportunity to rewrite the build script since there was a change in the build steps (mainly how the product JSON is inserted) and to get the build changes out of the patch. It also no longer relies on external caching (we'll want to do this within CI instead).
144 lines
5.4 KiB
TypeScript
144 lines
5.4 KiB
TypeScript
import * as cp from "child_process";
|
|
import * as os from "os";
|
|
import * as path from "path";
|
|
import { Stream } from "stream";
|
|
import * as util from "util";
|
|
import { toVSBufferReadableStream } from "vs/base/common/buffer";
|
|
import { CancellationToken } from "vs/base/common/cancellation";
|
|
import { URI } from "vs/base/common/uri";
|
|
import * as pfs from "vs/base/node/pfs";
|
|
import { IConfigurationService } from "vs/platform/configuration/common/configuration";
|
|
import { IEnvironmentService } from "vs/platform/environment/common/environment";
|
|
import { IFileService } from "vs/platform/files/common/files";
|
|
import { ILogService } from "vs/platform/log/common/log";
|
|
import product from "vs/platform/product/common/product";
|
|
import { asJson, IRequestService } from "vs/platform/request/common/request";
|
|
import { AvailableForDownload, State, UpdateType } from "vs/platform/update/common/update";
|
|
import { AbstractUpdateService } from "vs/platform/update/electron-main/abstractUpdateService";
|
|
import { ipcMain } from "vs/server/src/node/ipc";
|
|
import { extract } from "vs/server/src/node/marketplace";
|
|
import { tmpdir } from "vs/server/src/node/util";
|
|
import * as zlib from "zlib";
|
|
|
|
interface IUpdate {
|
|
name: string;
|
|
}
|
|
|
|
export class UpdateService extends AbstractUpdateService {
|
|
_serviceBrand: any;
|
|
|
|
constructor(
|
|
@IConfigurationService configurationService: IConfigurationService,
|
|
@IEnvironmentService environmentService: IEnvironmentService,
|
|
@IRequestService requestService: IRequestService,
|
|
@ILogService logService: ILogService,
|
|
@IFileService private readonly fileService: IFileService,
|
|
) {
|
|
super(null, configurationService, environmentService, requestService, logService);
|
|
}
|
|
|
|
public async isLatestVersion(latest?: IUpdate | null): Promise<boolean | undefined> {
|
|
if (!latest) {
|
|
latest = await this.getLatestVersion();
|
|
}
|
|
if (latest) {
|
|
const latestMajor = parseInt(latest.name);
|
|
const currentMajor = parseInt(product.codeServerVersion);
|
|
return !isNaN(latestMajor) && !isNaN(currentMajor) &&
|
|
currentMajor <= latestMajor && latest.name === product.codeServerVersion;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
protected buildUpdateFeedUrl(quality: string): string {
|
|
return `${product.updateUrl}/${quality}`;
|
|
}
|
|
|
|
public async doQuitAndInstall(): Promise<void> {
|
|
ipcMain.relaunch();
|
|
}
|
|
|
|
protected async doCheckForUpdates(context: any): Promise<void> {
|
|
this.setState(State.CheckingForUpdates(context));
|
|
try {
|
|
const update = await this.getLatestVersion();
|
|
if (!update || this.isLatestVersion(update)) {
|
|
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.Downloading(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.
|
|
// HACK: This uses knowledge of the internals of the request service.
|
|
if (target !== "darwin" && context.res.headers["content-encoding"] !== "gzip") {
|
|
const stream = (context.res as any as Stream);
|
|
stream.removeAllListeners();
|
|
context.stream = toVSBufferReadableStream(stream.pipe(zlib.createGunzip()));
|
|
}
|
|
await this.fileService.writeFile(URI.file(downloadPath), context.stream);
|
|
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);
|
|
this.setState(State.Idle(UpdateType.Archive, showNotification ? (error.message || error.toString()) : undefined));
|
|
}
|
|
|
|
private async buildReleaseName(release: string): Promise<string> {
|
|
let target: string = os.platform();
|
|
if (target === "linux") {
|
|
const result = await util.promisify(cp.exec)("ldd --version").catch((error) => ({
|
|
stderr: error.message,
|
|
stdout: "",
|
|
}));
|
|
if (/^musl/.test(result.stderr) || /^musl/.test(result.stdout)) {
|
|
target = "alpine";
|
|
}
|
|
}
|
|
let arch = os.arch();
|
|
if (arch === "x64") {
|
|
arch = "x86_64";
|
|
}
|
|
return `code-server${release}-${target}-${arch}`;
|
|
}
|
|
}
|