diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index 70e15affd192..2e545fd62294 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -39,14 +39,20 @@ export class Time { ) { } } +// tslint:disable-next-line no-any +export type FieldArray = Array>; + +// Same as FieldArray but supports taking an object or undefined. // `undefined` is allowed to make it easy to conditionally display a field. -// For example: `error && field("error", error)` +// For example: `error && field("error", error)`. +// For objects, the logger will iterate over the keys and turn them into +// instances of Field. // tslint:disable-next-line no-any -export type FieldArray = Array | undefined>; +export type LogArgument = Array | undefined | object>; // Functions can be used to remove the need to perform operations when the // logging level won't output the result anyway. -export type LogCallback = () => [string, ...FieldArray]; +export type LogCallback = () => [string, ...LogArgument]; /** * Creates a time field @@ -82,13 +88,19 @@ export abstract class Formatter { public abstract tag(name: string, color: string): void; /** - * Add string or arbitrary variable. + * Add string variable. */ public abstract push(arg: string, color?: string, weight?: string): void; + /** + * Add arbitrary variable. + */ public abstract push(arg: any): void; // tslint:disable-line no-any + /** + * Add fields. + */ // tslint:disable-next-line no-any - public abstract fields(fields: Array>): void; + public abstract fields(fields: FieldArray): void; /** * Flush out the built arguments. @@ -120,6 +132,9 @@ export abstract class Formatter { * Browser formatter. */ export class BrowserFormatter extends Formatter { + /** + * Add a tag. + */ public tag(name: string, color: string): void { this.format += `%c ${name} `; this.args.push( @@ -131,6 +146,9 @@ export class BrowserFormatter extends Formatter { this.push(" "); } + /** + * Add an argument. + */ public push(arg: any, color: string = "inherit", weight: string = "normal"): void { // tslint:disable-line no-any if (color || weight) { this.format += "%c"; @@ -143,8 +161,11 @@ export class BrowserFormatter extends Formatter { this.args.push(arg); } + /** + * Add fields. + */ // tslint:disable-next-line no-any - public fields(fields: Array>): void { + public fields(fields: FieldArray): void { // tslint:disable-next-line no-console console.groupCollapsed(...this.flush()); fields.forEach((field) => { @@ -166,6 +187,9 @@ export class BrowserFormatter extends Formatter { * Server (Node) formatter. */ export class ServerFormatter extends Formatter { + /** + * Add a tag. + */ public tag(name: string, color: string): void { const [r, g, b] = this.hexToRgb(color); while (name.length < 5) { @@ -175,6 +199,9 @@ export class ServerFormatter extends Formatter { this.format += `\u001B[38;2;${r};${g};${b}m${name} \u001B[0m`; } + /** + * Add an argument. + */ public push(arg: any, color?: string, weight?: string): void { // tslint:disable-line no-any if (weight === "bold") { this.format += "\u001B[1m"; @@ -190,8 +217,11 @@ export class ServerFormatter extends Formatter { this.args.push(arg); } + /** + * Add fields. + */ // tslint:disable-next-line no-any - public fields(fields: Array>): void { + public fields(fields: FieldArray): void { // tslint:disable-next-line no-any const obj: { [key: string]: any} = {}; this.format += "\u001B[38;2;140;140;140m"; @@ -257,80 +287,83 @@ export class Logger { this.muted = true; } + /** + * Extend the logger (for example to send the logs elsewhere). + */ public extend(extender: Extender): void { this.extenders.push(extender); } /** - * Outputs information. + * Log information. */ public info(fn: LogCallback): void; - public info(message: string, ...fields: FieldArray): void; - public info(message: LogCallback | string, ...fields: FieldArray): void { + public info(message: string, ...args: LogArgument): void; + public info(message: LogCallback | string, ...args: LogArgument): void { this.handle({ type: "info", message, - fields, + args, tagColor: "#008FBF", level: Level.Info, }); } /** - * Outputs a warning. + * Log a warning. */ public warn(fn: LogCallback): void; - public warn(message: string, ...fields: FieldArray): void; - public warn(message: LogCallback | string, ...fields: FieldArray): void { + public warn(message: string, ...args: LogArgument): void; + public warn(message: LogCallback | string, ...args: LogArgument): void { this.handle({ type: "warn", message, - fields, + args, tagColor: "#FF9D00", level: Level.Warning, }); } /** - * Outputs a trace message. + * Log a trace message. */ public trace(fn: LogCallback): void; - public trace(message: string, ...fields: FieldArray): void; - public trace(message: LogCallback | string, ...fields: FieldArray): void { + public trace(message: string, ...args: LogArgument): void; + public trace(message: LogCallback | string, ...args: LogArgument): void { this.handle({ type: "trace", message, - fields, + args, tagColor: "#888888", level: Level.Trace, }); } /** - * Outputs a debug message. + * Log a debug message. */ public debug(fn: LogCallback): void; - public debug(message: string, ...fields: FieldArray): void; - public debug(message: LogCallback | string, ...fields: FieldArray): void { + public debug(message: string, ...args: LogArgument): void; + public debug(message: LogCallback | string, ...args: LogArgument): void { this.handle({ type: "debug", message, - fields, + args, tagColor: "#84009E", level: Level.Debug, }); } /** - * Outputs an error. + * Log an error. */ public error(fn: LogCallback): void; - public error(message: string, ...fields: FieldArray): void; - public error(message: LogCallback | string, ...fields: FieldArray): void { + public error(message: string, ...args: LogArgument): void; + public error(message: LogCallback | string, ...args: LogArgument): void { this.handle({ type: "error", message, - fields, + args, tagColor: "#B00000", level: Level.Error, }); @@ -350,12 +383,12 @@ export class Logger { } /** - * Outputs a message. + * Log a message. */ private handle(options: { type: "trace" | "info" | "warn" | "debug" | "error"; message: string | LogCallback; - fields?: FieldArray; + args?: LogArgument; level: Level; tagColor: string; }): void { @@ -363,16 +396,28 @@ export class Logger { return; } - let passedFields = options.fields || []; if (typeof options.message === "function") { const values = options.message(); options.message = values.shift() as string; - passedFields = values as FieldArray; + options.args = values as FieldArray; } - const fields = (this.defaultFields - ? passedFields.filter((f) => !!f).concat(this.defaultFields) - : passedFields.filter((f) => !!f)) as Array>; // tslint:disable-line no-any + const fields: FieldArray = []; + if (options.args) { + options.args.forEach((arg) => { + if (arg instanceof Field) { + fields.push(arg); + } else if (arg) { + Object.keys(arg).forEach((k) => { + // tslint:disable-next-line no-any + fields.push(field(k, (arg as any)[k])); + }); + } + }); + } + if (this.defaultFields) { + fields.push(...this.defaultFields); + } const now = Date.now(); let times: Array> = []; @@ -410,7 +455,7 @@ export class Logger { this.extenders.forEach((extender) => { extender({ section: this.name, - fields: options.fields, + fields, level: options.level, message: options.message as string, type: options.type, diff --git a/packages/protocol/src/browser/client.ts b/packages/protocol/src/browser/client.ts index 77bba1b69f21..6f75d5f2391d 100644 --- a/packages/protocol/src/browser/client.ts +++ b/packages/protocol/src/browser/client.ts @@ -278,6 +278,7 @@ export class Client { shell: init.getShell(), extensionsDirectory: init.getExtensionsDirectory(), builtInExtensionsDirectory: init.getBuiltinExtensionsDir(), + languageData: init.getLanguageData(), }; this.initDataEmitter.emit(this._initData); break; diff --git a/packages/protocol/src/common/connection.ts b/packages/protocol/src/common/connection.ts index f4f60deddce7..07004d1d2197 100644 --- a/packages/protocol/src/common/connection.ts +++ b/packages/protocol/src/common/connection.ts @@ -25,6 +25,7 @@ export interface InitData { readonly shell: string; readonly extensionsDirectory: string; readonly builtInExtensionsDirectory: string; + readonly languageData: string; } export interface SharedProcessData { diff --git a/packages/protocol/src/node/server.ts b/packages/protocol/src/node/server.ts index be4abbf847d5..1af5668c6f9e 100644 --- a/packages/protocol/src/node/server.ts +++ b/packages/protocol/src/node/server.ts @@ -9,6 +9,10 @@ import { ChildProcessModuleProxy, ForkProvider, FsModuleProxy, NetModuleProxy, N // tslint:disable no-any +export interface LanguageConfiguration { + locale: string; +} + export interface ServerOptions { readonly workingDirectory: string; readonly dataDirectory: string; @@ -16,6 +20,7 @@ export interface ServerOptions { readonly builtInExtensionsDirectory: string; readonly extensionsDirectory: string; readonly fork?: ForkProvider; + readonly getLanguageData?: () => Promise; } interface ProxyData { @@ -99,9 +104,25 @@ export class Server { initMsg.setTmpDirectory(os.tmpdir()); initMsg.setOperatingSystem(platformToProto(os.platform())); initMsg.setShell(os.userInfo().shell || global.process.env.SHELL || ""); - const srvMsg = new ServerMessage(); - srvMsg.setInit(initMsg); - connection.send(srvMsg.serializeBinary()); + + const getLanguageData = this.options.getLanguageData + || ((): Promise => Promise.resolve({ + locale: "en", + })); + + getLanguageData().then((languageData) => { + try { + initMsg.setLanguageData(JSON.stringify(languageData)); + } catch (error) { + logger.error("Unable to send language config", field("error", error)); + } + + const srvMsg = new ServerMessage(); + srvMsg.setInit(initMsg); + connection.send(srvMsg.serializeBinary()); + }).catch((error) => { + logger.error(error.message, field("error", error)); + }); } /** diff --git a/packages/protocol/src/proto/client.proto b/packages/protocol/src/proto/client.proto index 4681ecf6f49b..c89c2146ceb7 100644 --- a/packages/protocol/src/proto/client.proto +++ b/packages/protocol/src/proto/client.proto @@ -42,4 +42,5 @@ message WorkingInit { string shell = 6; string builtin_extensions_dir = 7; string extensions_directory = 8; + string language_data = 9; } diff --git a/packages/protocol/src/proto/client_pb.d.ts b/packages/protocol/src/proto/client_pb.d.ts index 9c4c1dd591a6..2b75aab65efd 100644 --- a/packages/protocol/src/proto/client_pb.d.ts +++ b/packages/protocol/src/proto/client_pb.d.ts @@ -135,6 +135,9 @@ export class WorkingInit extends jspb.Message { getExtensionsDirectory(): string; setExtensionsDirectory(value: string): void; + getLanguageData(): string; + setLanguageData(value: string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): WorkingInit.AsObject; static toObject(includeInstance: boolean, msg: WorkingInit): WorkingInit.AsObject; @@ -155,6 +158,7 @@ export namespace WorkingInit { shell: string, builtinExtensionsDir: string, extensionsDirectory: string, + languageData: string, } export enum OperatingSystem { diff --git a/packages/protocol/src/proto/client_pb.js b/packages/protocol/src/proto/client_pb.js index 20bf41eacd06..01fe44782124 100644 --- a/packages/protocol/src/proto/client_pb.js +++ b/packages/protocol/src/proto/client_pb.js @@ -795,7 +795,8 @@ proto.WorkingInit.toObject = function(includeInstance, msg) { operatingSystem: jspb.Message.getFieldWithDefault(msg, 5, 0), shell: jspb.Message.getFieldWithDefault(msg, 6, ""), builtinExtensionsDir: jspb.Message.getFieldWithDefault(msg, 7, ""), - extensionsDirectory: jspb.Message.getFieldWithDefault(msg, 8, "") + extensionsDirectory: jspb.Message.getFieldWithDefault(msg, 8, ""), + languageData: jspb.Message.getFieldWithDefault(msg, 9, "") }; if (includeInstance) { @@ -864,6 +865,10 @@ proto.WorkingInit.deserializeBinaryFromReader = function(msg, reader) { var value = /** @type {string} */ (reader.readString()); msg.setExtensionsDirectory(value); break; + case 9: + var value = /** @type {string} */ (reader.readString()); + msg.setLanguageData(value); + break; default: reader.skipField(); break; @@ -949,6 +954,13 @@ proto.WorkingInit.serializeBinaryToWriter = function(message, writer) { f ); } + f = message.getLanguageData(); + if (f.length > 0) { + writer.writeString( + 9, + f + ); + } }; @@ -1081,4 +1093,19 @@ proto.WorkingInit.prototype.setExtensionsDirectory = function(value) { }; +/** + * optional string language_data = 9; + * @return {string} + */ +proto.WorkingInit.prototype.getLanguageData = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 9, "")); +}; + + +/** @param {string} value */ +proto.WorkingInit.prototype.setLanguageData = function(value) { + jspb.Message.setProto3StringField(this, 9, value); +}; + + goog.object.extend(exports, proto); diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index 54a98b962abb..8da7a5d6ef09 100644 --- a/packages/server/src/cli.ts +++ b/packages/server/src/cli.ts @@ -1,5 +1,6 @@ import { field, logger } from "@coder/logger"; import { ServerMessage, SharedProcessActive } from "@coder/protocol/src/proto"; +import { LanguageConfiguration } from "@coder/protocol/src/node/server"; import { withEnv } from "@coder/protocol"; import { ChildProcess, fork, ForkOptions } from "child_process"; import { randomFillSync } from "crypto"; @@ -11,6 +12,7 @@ import * as WebSocket from "ws"; import { buildDir, cacheHome, dataHome, isCli, serveStatic } from "./constants"; import { createApp } from "./server"; import { forkModule, requireModule } from "./vscode/bootstrapFork"; +import { getNlsConfiguration } from "./vscode/language"; import { SharedProcess, SharedProcessState } from "./vscode/sharedProcess"; import opn = require("opn"); @@ -257,6 +259,7 @@ const bold = (text: string | number): string | number => { return fork(modulePath, args, options); }, + getLanguageData: (): Promise => getNlsConfiguration(dataDir, builtInExtensionsDir), }, password, httpsOptions: hasCustomHttps ? { diff --git a/packages/server/src/vscode/language.ts b/packages/server/src/vscode/language.ts new file mode 100644 index 000000000000..5c3dd0fc57c1 --- /dev/null +++ b/packages/server/src/vscode/language.ts @@ -0,0 +1,70 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as util from "util"; +import { logger } from "@coder/logger"; +import * as lp from "vs/base/node/languagePacks"; + +// NOTE: This code was pulled from lib/vscode/src/main.js. + +const stripComments = (content: string): string => { + const regexp = /("(?:[^\\"]*(?:\\.)?)*")|('(?:[^\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g; + + return content.replace(regexp, (match, _m1, _m2, m3, m4) => { + if (m3) { // Only one of m1, m2, m3, m4 matches. + return ""; // A block comment. Replace with nothing. + } else if (m4) { // A line comment. If it ends in \r?\n then keep it. + const length_1 = m4.length; + if (length_1 > 2 && m4[length_1 - 1] === "\n") { + return m4[length_1 - 2] === "\r" ? "\r\n" : "\n"; + } else { + return ""; + } + } else { + return match; // We match a string. + } + }); +}; + +/** + * Get the locale from the locale file. + */ +const getUserDefinedLocale = async (userDataPath: string): Promise => { + const localeConfig = path.join(userDataPath, "User/locale.json"); + + try { + const content = stripComments(await util.promisify(fs.readFile)(localeConfig, "utf8")); + const value = JSON.parse(content).locale; + + return value && typeof value === "string" ? value.toLowerCase() : undefined; + } catch (e) { + return undefined; + } +}; + +export const getNlsConfiguration = (userDataPath: string, builtInDirectory: string): Promise => { + const defaultConfig = { locale: "en", availableLanguages: {} }; + + return new Promise(async (resolve): Promise => { + try { + const metaDataFile = require("path").join(builtInDirectory, "nls.metadata.json"); + const locale = await getUserDefinedLocale(userDataPath); + if (!locale) { + logger.debug("No locale, using default"); + + return resolve(defaultConfig); + } + + const config = (await lp.getNLSConfiguration( + process.env.VERSION || "development", userDataPath, + metaDataFile, locale, + )) || defaultConfig; + + (config as lp.InternalNLSConfiguration)._languagePackSupport = true; + logger.debug(`Locale is ${locale}`, config); + resolve(config); + } catch (error) { + logger.error(error.message); + resolve(defaultConfig); + } + }); +}; diff --git a/packages/vscode/src/client.ts b/packages/vscode/src/client.ts index c1a544d5b663..1eb15e694f7e 100644 --- a/packages/vscode/src/client.ts +++ b/packages/vscode/src/client.ts @@ -22,6 +22,7 @@ class VSClient extends IdeClient { paths._paths.initialize(data, sharedData); product.initialize(data); process.env.SHELL = data.shell; + process.env.VSCODE_NLS_CONFIG = data.languageData; // At this point everything should be filled, including `os`. `os` also // relies on `initData` but it listens first so it initialize before this // callback, meaning we are safe to include everything from VS Code now. diff --git a/packages/vscode/src/fill/platform.ts b/packages/vscode/src/fill/platform.ts index efe98c757626..1bf578efcaa7 100644 --- a/packages/vscode/src/fill/platform.ts +++ b/packages/vscode/src/fill/platform.ts @@ -1,11 +1,64 @@ +import * as fs from "fs"; import * as os from "os"; +import * as path from "path"; +import * as util from "util"; import * as platform from "vs/base/common/platform"; import * as browser from "vs/base/browser/browser"; +import * as nls from "vs/nls"; +import * as lp from "vs/base/node/languagePacks"; // tslint:disable no-any to override const -// Use en instead of en-US since that's vscode default and it uses -// that to determine whether to output aliases which will be redundant. +interface IBundledStrings { + [moduleId: string]: string[]; +} + +const rawNlsConfig = process.env.VSCODE_NLS_CONFIG; +if (rawNlsConfig) { + const nlsConfig = JSON.parse(rawNlsConfig) as lp.InternalNLSConfiguration; + const resolved = nlsConfig.availableLanguages["*"]; + (platform as any).locale = nlsConfig.locale; + (platform as any).language = resolved ? resolved : "en"; + (platform as any).translationsConfigFile = nlsConfig._translationsConfigFile; + + // TODO: Each time this is imported, VS Code's loader creates a different + // module with an array of all the translations for that file. We don't use + // their loader (we're using Webpack) so we need to figure out a good way to + // handle that. + if (nlsConfig._resolvedLanguagePackCoreLocation) { + const bundles = Object.create(null); + (nls as any).load("nls", undefined, (mod: any) => { + Object.keys(mod).forEach((k) => (nls as any)[k] = mod[k]); + }, { + "vs/nls": { + ...nlsConfig, + // Taken from lib/vscode/src/bootstrap.js. + loadBundle: async ( + bundle: string, language: string, + cb: (error?: Error, messages?: string[] | IBundledStrings) => void, + ): Promise => { + let result = bundles[bundle]; + if (result) { + return cb(undefined, result); + } + + const bundleFile = path.join(nlsConfig._resolvedLanguagePackCoreLocation, bundle.replace(/\//g, "!") + ".nls.json"); + + try { + const content = await util.promisify(fs.readFile)(bundleFile, "utf8"); + bundles[bundle] = JSON.parse(content); + cb(undefined, bundles[bundle]); + } catch (error) { + cb(error, undefined); + } + }, + }, + }); + } +} + +// Anything other than "en" will cause English aliases to display, which ends up +// just displaying a lot of redundant text when the locale is en-US. if (platform.locale === "en-US") { (platform as any).locale = "en"; } @@ -20,8 +73,12 @@ if (platform.language === "en-US") { (platform as any).isLinux = os.platform() === "linux"; (platform as any).isWindows = os.platform() === "win32"; (platform as any).isMacintosh = os.platform() === "darwin"; -(platform as any).platform = os.platform() === "linux" ? platform.Platform.Linux : os.platform() === "win32" ? platform.Platform.Windows : platform.Platform.Mac; +(platform as any).platform = os.platform() === "linux" + ? platform.Platform.Linux : os.platform() === "win32" + ? platform.Platform.Windows : platform.Platform.Mac; // This is used for keybindings, and in one place to choose between \r\n and \n // (which we change to use platform.isWindows instead). -(platform as any).OS = (browser.isMacintosh ? platform.OperatingSystem.Macintosh : (browser.isWindows ? platform.OperatingSystem.Windows : platform.OperatingSystem.Linux)); +(platform as any).OS = browser.isMacintosh + ? platform.OperatingSystem.Macintosh : browser.isWindows + ? platform.OperatingSystem.Windows : platform.OperatingSystem.Linux; diff --git a/packages/vscode/src/fill/windowsService.ts b/packages/vscode/src/fill/windowsService.ts index d89c8420dc6d..ed135851c941 100644 --- a/packages/vscode/src/fill/windowsService.ts +++ b/packages/vscode/src/fill/windowsService.ts @@ -255,8 +255,8 @@ export class WindowsService implements IWindowsService { throw new Error("not implemented"); } - public relaunch(_options: { addArgs?: string[], removeArgs?: string[] }): Promise { - throw new Error("not implemented"); + public async relaunch(_options: { addArgs?: string[], removeArgs?: string[] }): Promise { + this.window.reload(); } // macOS Native Tabs