Archived
1
0

Getting the client to run (#12)

* Clean up workbench and integrate initialization data

* Uncomment Electron fill

* Run server & client together

* Clean up Electron fill & patch

* Bind fs methods

This makes them usable with the promise form:
`promisify(access)(...)`.

* Add space between tag and title to browser logger

* Add typescript dep to server and default __dirname for path

* Serve web files from server

* Adjust some dev options

* Rework workbench a bit to use a class and catch unexpected errors

* No mkdirs for now, fix util fill, use bash with exec

* More fills, make general client abstract

* More fills

* Fix cp.exec

* Fix require calls in fs fill being aliased

* Create data and storage dir

* Implement fs.watch

Using exec for now.

* Implement storage database fill

* Fix os export and homedir

* Add comment to use navigator.sendBeacon

* Fix fs callbacks (some args are optional)

* Make sure data directory exists when passing it back

* Update patch

* Target es5

* More fills

* Add APIs required for bootstrap-fork to function (#15)

* Add bootstrap-fork execution

* Add createConnection

* Bundle bootstrap-fork into cli

* Remove .node directory created from spdlog

* Fix npm start

* Remove unnecessary comment

* Add webpack-hot-middleware if CLI env is not set

* Add restarting to shared process

* Fix starting with yarn
This commit is contained in:
Asher
2019-01-18 15:46:40 -06:00
committed by Kyle Carberry
parent 05899b5edf
commit 72bf4547d4
80 changed files with 5183 additions and 9697 deletions

View File

@ -0,0 +1,11 @@
import { URI } from "vs/base/common/uri";
export const getPathFromAmdModule = (_: typeof require, relativePath: string): string => {
if (process.mainModule && process.mainModule.filename) {
const index = process.mainModule.filename.lastIndexOf("/");
return process.mainModule.filename.slice(0, index);
}
return relativePath ? URI.parse(require.toUrl(relativePath)).fsPath : "";
};

View File

@ -0,0 +1 @@
export const gracefulify = (): void => undefined;

View File

@ -1,8 +1,13 @@
module.exports = {
getCurrentKeyboardLayout: (): null => {
class NativeKeymap {
public getCurrentKeyboardLayout(): null {
return null;
},
getKeyMap: (): undefined[] => {
}
public getKeyMap(): undefined[] {
return [];
},
};
}
}
export = new NativeKeymap();

View File

@ -70,4 +70,4 @@ const ptyType: nodePtyType = {
};
module.exports = ptyType;
exports = ptyType;

View File

@ -0,0 +1,2 @@
// TODO: obtain this in a reasonable way.
export default { name: "vscode", version: "1.31.1" };

View File

@ -0,0 +1,7 @@
const paths = {
appData: "/tmp",
defaultUserData: "/tmp",
};
export let getAppDataPath = (): string => paths.appData;
export let getDefaultUserDataPath = (): string => paths.defaultUserData;

View File

@ -0,0 +1,24 @@
import { IProductConfiguration } from "vs/platform/node/product";
const product = {
nameShort: "VSCode",
nameLong: "vscode online",
dataFolderName: ".vscode-online",
extensionsGallery: {
serviceUrl: "",
},
extensionExecutionEnvironments: {
"wayou.vscode-todo-highlight": "worker",
"vscodevim.vim": "worker",
"coenraads.bracket-pair-colorizer": "worker",
},
fetchUrl: "",
} as IProductConfiguration;
if (process.env['VSCODE_DEV']) {
product.nameShort += ' Dev';
product.nameLong += ' Dev';
product.dataFolderName += '-dev';
}
export default product;

View File

@ -0,0 +1,3 @@
// TODO: ?
// tslint:disable-next-line no-any
(global as any).requireToUrl = (path: string): string => `${location.protocol}//{location.host}/${path}`;

View File

@ -0,0 +1,186 @@
import { exec } from "child_process";
import { promisify } from "util";
import { appendFile, stat, readdir } from "fs";
import { RotatingLogger as NodeRotatingLogger } from "spdlog";
import { logger, field } from "@coder/logger";
import { escapePath } from "@coder/protocol";
// TODO: It would be better to spawn an actual spdlog instance on the server and
// use that for the logging. Or maybe create an instance when the server starts,
// and just always use that one (make it part of the protocol).
export class RotatingLogger implements NodeRotatingLogger {
private format = true;
private buffer = "";
private flushPromise: Promise<void> | undefined;
private name: string;
private logDirectory: string;
private escapedLogDirectory: string;
private fullFilePath: string;
private fileName: string;
private fileExt: string | undefined;
private escapedFilePath: string;
private filesize: number;
private filecount: number;
public constructor(name: string, filePath: string, filesize: number, filecount: number) {
this.name = name;
this.filesize = filesize;
this.filecount = filecount;
this.fullFilePath = filePath;
const slashIndex = filePath.lastIndexOf("/");
const dotIndex = filePath.lastIndexOf(".");
this.logDirectory = slashIndex !== -1 ? filePath.substring(0, slashIndex) : "/";
this.fileName = filePath.substring(slashIndex + 1, dotIndex !== -1 ? dotIndex : undefined);
this.fileExt = dotIndex !== -1 ? filePath.substring(dotIndex + 1) : undefined;
this.escapedLogDirectory = escapePath(this.logDirectory);
this.escapedFilePath = escapePath(filePath);
this.flushPromise = new Promise((resolve): void => {
exec(`mkdir -p ${this.escapedLogDirectory}; touch ${this.escapedFilePath}`, async (error) => {
if (!error) {
try {
await this.doFlush();
} catch (e) {
error = e;
}
}
if (error) {
logger.error(error.message, field("error", error));
}
this.flushPromise = undefined;
resolve();
});
});
}
public trace(message: string): void {
this.write("trace", message);
}
public debug(message: string): void {
this.write("debug", message);
}
public info(message: string): void {
this.write("info", message);
}
public warn(message: string): void {
this.write("warn", message);
}
public error(message: string): void {
this.write("error", message);
}
public critical(message: string): void {
this.write("critical", message);
}
public setLevel(): void {
// Should output everything.
}
public clearFormatters(): void {
this.format = false;
}
/**
* Flushes the buffer. Only one process runs at a time to prevent race
* conditions.
*/
public flush(): Promise<void> {
if (!this.flushPromise) {
this.flushPromise = this.doFlush().then(() => {
this.flushPromise = undefined;
}).catch((error) => {
this.flushPromise = undefined;
logger.error(error.message, field("error", error));
});
}
return this.flushPromise;
}
public drop(): void {
this.buffer = "";
}
private pad(num: number, length: number = 2, prefix: string = "0"): string {
const str = num.toString();
return (length > str.length ? prefix.repeat(length - str.length) : "") + str;
}
private write(severity: string, message: string): void {
if (this.format) {
const date = new Date();
const dateStr = `${date.getFullYear()}-${this.pad(date.getMonth() + 1)}-${this.pad(date.getDate())}`
+ ` ${this.pad(date.getHours())}:${this.pad(date.getMinutes())}:${this.pad(date.getSeconds())}.${this.pad(date.getMilliseconds(), 3)}`;
this.buffer += `[${dateStr}] [${this.name}] [${severity}] `;
}
this.buffer += message;
if (this.format) {
this.buffer += "\n";
}
this.flush();
}
private async rotate(): Promise<void> {
const stats = await promisify(stat)(this.fullFilePath);
if (stats.size < this.filesize) {
return;
}
const reExt = typeof this.fileExt !== "undefined" ? `\\.${this.fileExt}` : "";
const re = new RegExp(`^${this.fileName}(?:\\.(\\d+))?${reExt}$`);
const orderedFiles: string[] = [];
(await promisify(readdir)(this.logDirectory)).forEach((file) => {
const match = re.exec(file);
if (match) {
orderedFiles[typeof match[1] !== "undefined" ? parseInt(match[1], 10) : 0] = file;
}
});
// Rename in reverse so we don't overwrite before renaming something.
let count = 0;
const command = orderedFiles.map((file) => {
const fileExt = typeof this.fileExt !== "undefined" ? `.${this.fileExt}` : "";
const newFile = `${this.logDirectory}/${this.fileName}.${++count}${fileExt}`;
return count >= this.filecount
? `rm ${escapePath(this.logDirectory + "/" + file)}`
: `mv ${escapePath(this.logDirectory + "/" + file)} ${escapePath(newFile)}`;
}).reverse().concat([
`touch ${escapePath(this.fullFilePath)}`,
]).join(";");
await promisify(exec)(command);
}
/**
* Flushes the entire buffer, including anything added in the meantime, and
* rotates the log if necessary.
*/
private async doFlush(): Promise<void> {
const writeBuffer = async (): Promise<void> => {
const toWrite = this.buffer;
this.buffer = "";
await promisify(appendFile)(this.fullFilePath, toWrite);
};
while (this.buffer.length > 0) {
await writeBuffer();
await this.rotate();
}
}
}
export const setAsyncMode = (): void => {
// Nothing to do.
};

View File

@ -0,0 +1,22 @@
import { StdioIpcHandler } from "@coder/server/src/ipc";
import { IpcRenderer } from "electron";
export * from "@coder/ide/src/fill/electron";
class StdioIpcRenderer extends StdioIpcHandler implements IpcRenderer {
public sendTo(windowId: number, channel: string, ...args: any[]): void {
throw new Error("Method not implemented.");
}
public sendToHost(channel: string, ...args: any[]): void {
throw new Error("Method not implemented.");
}
public eventNames(): string[] {
return super.eventNames() as string[];
}
}
export const ipcRenderer = new StdioIpcRenderer();

View File

@ -0,0 +1,78 @@
import { readFile, writeFile } from "fs";
import { promisify } from "util";
import { Event } from "vs/base/common/event";
import * as storage from "vs/base/node/storage";
export class StorageDatabase implements storage.IStorageDatabase {
public readonly onDidChangeItemsExternal = Event.None;
private items = new Map<string, string>();
private fetched: boolean = false;
public constructor(private readonly path: string) {
window.addEventListener("unload", () => {
if (!navigator.sendBeacon) {
throw new Error("cannot save state");
}
// TODO: Need to use navigator.sendBeacon instead of the web socket, or we
// need to save when there is a change. Should we save as a sqlite3
// database instead of JSON? Could send to the server the way the global
// storage works. Or maybe fill `vscode-sqlite3` to do that.
this.save();
});
}
public async getItems(): Promise<Map<string, string>> {
if (this.fetched) {
return this.items;
}
try {
const contents = await promisify(readFile)(this.path, "utf8");
const json = JSON.parse(contents);
Object.keys(json).forEach((key) => {
this.items.set(key, json[key]);
});
} catch (error) {
if (error.code && error.code !== "ENOENT") {
throw error;
}
}
this.fetched = true;
return this.items;
}
public updateItems(request: storage.IUpdateRequest): Promise<void> {
if (request.insert) {
request.insert.forEach((value, key) => this.items.set(key, value));
}
if (request.delete) {
request.delete.forEach(key => this.items.delete(key));
}
return Promise.resolve();
}
public close(): Promise<void> {
return Promise.resolve();
}
public checkIntegrity(): Promise<string> {
return Promise.resolve("ok");
}
private save(): Promise<void> {
const json: { [key: string]: string } = {};
this.items.forEach((value, key) => {
json[key] = value;
});
return promisify(writeFile)(this.path, JSON.stringify(json));
}
}
// @ts-ignore
storage.SQLiteStorageDatabase = StorageDatabase;

View File

@ -0,0 +1,274 @@
import * as electron from "electron";
import { Emitter } from "@coder/events";
import * as windowsIpc from "vs/platform/windows/node/windowsIpc";
import { IWindowsService, INativeOpenDialogOptions, MessageBoxOptions, SaveDialogOptions, OpenDialogOptions, IMessageBoxResult, IDevToolsOptions, IEnterWorkspaceResult, CrashReporterStartOptions, INewWindowOptions } from "vs/platform/windows/common/windows";
import { ParsedArgs } from "vs/platform/environment/common/environment";
import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces";
import { URI } from "vs/base/common/uri";
import { IRecentlyOpened } from "vs/platform/history/common/history";
import { ISerializableCommandAction } from "vs/platform/actions/common/actions";
// TODO: Might make sense to hook these straight in if we can.
// import { WindowsService as VSWindowsService } from "vs/platform/windows/electron-main/windowsService";
// import { WindowsManager } from "vs/code/electron-main/windows";
/**
* Instead of going to the shared process, we'll directly run these methods on
* the client. This setup means we can only control the current window.
*/
class WindowsService implements IWindowsService {
// tslint:disable-next-line no-any
public _serviceBrand: any;
private openEmitter = new Emitter<number>();
private focusEmitter = new Emitter<number>();
private blurEmitter = new Emitter<number>();
private maximizeEmitter = new Emitter<number>();
private unmaximizeEmitter = new Emitter<number>();
private recentlyOpenedChangeEmitter = new Emitter<void>();
public onWindowOpen = this.openEmitter.event;
public onWindowFocus = this.focusEmitter.event;
public onWindowBlur = this.blurEmitter.event;
public onWindowMaximize = this.maximizeEmitter.event;
public onWindowUnmaximize = this.unmaximizeEmitter.event;
public onRecentlyOpenedChange = this.recentlyOpenedChangeEmitter.event;
private window = new electron.BrowserWindow();
// Dialogs
public pickFileFolderAndOpen(_options: INativeOpenDialogOptions): Promise<void> {
throw new Error("not implemented");
}
public pickFileAndOpen(_options: INativeOpenDialogOptions): Promise<void> {
throw new Error("not implemented");
}
public pickFolderAndOpen(_options: INativeOpenDialogOptions): Promise<void> {
throw new Error("not implemented");
}
public pickWorkspaceAndOpen(_options: INativeOpenDialogOptions): Promise<void> {
throw new Error("not implemented");
}
public showMessageBox(_windowId: number, _options: MessageBoxOptions): Promise<IMessageBoxResult> {
throw new Error("not implemented");
}
public showSaveDialog(_windowId: number, _options: SaveDialogOptions): Promise<string> {
throw new Error("not implemented");
}
public showOpenDialog(_windowId: number, _options: OpenDialogOptions): Promise<string[]> {
throw new Error("not implemented");
}
public reloadWindow(windowId: number, _args?: ParsedArgs): Promise<void> {
return Promise.resolve(this.getWindowById(windowId).reload());
}
public openDevTools(_windowId: number, _options?: IDevToolsOptions): Promise<void> {
throw new Error("not implemented");
}
public toggleDevTools(_windowId: number): Promise<void> {
throw new Error("not implemented");
}
public closeWorkspace(_windowId: number): Promise<void> {
throw new Error("not implemented");
}
public enterWorkspace(_windowId: number, _path: string): Promise<IEnterWorkspaceResult> {
throw new Error("not implemented");
}
public createAndEnterWorkspace(_windowId: number, _folders?: IWorkspaceFolderCreationData[], _path?: string): Promise<IEnterWorkspaceResult> {
throw new Error("not implemented");
}
public saveAndEnterWorkspace(_windowId: number, _path: string): Promise<IEnterWorkspaceResult> {
throw new Error("not implemented");
}
public toggleFullScreen(windowId: number): Promise<void> {
const win = this.getWindowById(windowId);
return Promise.resolve(win.setFullScreen(!win.isFullScreen()));
}
public setRepresentedFilename(windowId: number, fileName: string): Promise<void> {
return Promise.resolve(this.getWindowById(windowId).setRepresentedFilename(fileName));
}
public addRecentlyOpened(_files: URI[]): Promise<void> {
throw new Error("not implemented");
}
public removeFromRecentlyOpened(_paths: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string)[]): Promise<void> {
throw new Error("not implemented");
}
public clearRecentlyOpened(): Promise<void> {
throw new Error("not implemented");
}
public getRecentlyOpened(_windowId: number): Promise<IRecentlyOpened> {
// TODO: properly implement.
return Promise.resolve({
workspaces: [],
files: [],
});
}
public focusWindow(windowId: number): Promise<void> {
return Promise.resolve(this.getWindowById(windowId).focus());
}
public closeWindow(_windowId: number): Promise<void> {
throw new Error("not implemented");
}
public isFocused(windowId: number): Promise<boolean> {
return Promise.resolve(this.getWindowById(windowId).isFocused());
}
public isMaximized(_windowId: number): Promise<boolean> {
throw new Error("not implemented");
}
public maximizeWindow(_windowId: number): Promise<void> {
throw new Error("not implemented");
}
public unmaximizeWindow(_windowId: number): Promise<void> {
throw new Error("not implemented");
}
public minimizeWindow(_windowId: number): Promise<void> {
throw new Error("not implemented");
}
public onWindowTitleDoubleClick(_windowId: number): Promise<void> {
throw new Error("not implemented");
}
public setDocumentEdited(_windowId: number, _flag: boolean): Promise<void> {
throw new Error("not implemented");
}
public quit(): Promise<void> {
throw new Error("not implemented");
}
public relaunch(_options: { addArgs?: string[], removeArgs?: string[] }): Promise<void> {
throw new Error("not implemented");
}
// macOS Native Tabs
public newWindowTab(): Promise<void> {
throw new Error("not implemented");
}
public showPreviousWindowTab(): Promise<void> {
throw new Error("not implemented");
}
public showNextWindowTab(): Promise<void> {
throw new Error("not implemented");
}
public moveWindowTabToNewWindow(): Promise<void> {
throw new Error("not implemented");
}
public mergeAllWindowTabs(): Promise<void> {
throw new Error("not implemented");
}
public toggleWindowTabsBar(): Promise<void> {
throw new Error("not implemented");
}
// macOS TouchBar
public updateTouchBar(_windowId: number, _items: ISerializableCommandAction[][]): Promise<void> {
throw new Error("not implemented");
}
// Shared process
public whenSharedProcessReady(): Promise<void> {
// TODO: Update once shared process is tied in.
return Promise.resolve();
}
public toggleSharedProcess(): Promise<void> {
throw new Error("not implemented");
}
// Global methods
public openWindow(_windowId: number, _paths: URI[], _options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }): Promise<void> {
throw new Error("not implemented");
}
public openNewWindow(_options?: INewWindowOptions): Promise<void> {
throw new Error("not implemented");
}
public showWindow(windowId: number): Promise<void> {
return Promise.resolve(this.getWindowById(windowId).show());
}
public getWindows(): Promise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> {
throw new Error("not implemented");
}
public getWindowCount(): Promise<number> {
return Promise.resolve(1);
}
public log(_severity: string, ..._messages: string[]): Promise<void> {
throw new Error("not implemented");
}
public showItemInFolder(_path: string): Promise<void> {
throw new Error("not implemented");
}
public getActiveWindowId(): Promise<number | undefined> {
return Promise.resolve(1);
}
public openExternal(_url: string): Promise<boolean> {
throw new Error("not implemented");
}
public startCrashReporter(_config: CrashReporterStartOptions): Promise<void> {
throw new Error("not implemented");
}
public openAboutDialog(): Promise<void> {
throw new Error("not implemented");
}
public resolveProxy(windowId: number, url: string): Promise<string | undefined> {
return new Promise((resolve): void => {
this.getWindowById(windowId).webContents.session.resolveProxy(url, (proxy) => {
resolve(proxy);
});
});
}
/**
* Get window by ID. For now this is always the current window.
*/
private getWindowById(_windowId: number): electron.BrowserWindow {
return this.window;
}
}
// @ts-ignore
windowsIpc.WindowsChannelClient = WindowsService;