From e597d49912fc0299877899fe687b69da5214794d Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 12 Mar 2019 11:12:50 -0500 Subject: [PATCH] Fix issues with configuration directories - Move the old data directory if possible. - Fix extension path to not use a hard-coded path and instead use the data directory. - Create every part of the path during startup. - Create each path when a connection is made as well in case they are deleted while the server is running. - Create every part of the path before saving settings or writing a file using the resource endpoint. --- packages/protocol/src/common/util.ts | 19 +++++++++ packages/protocol/src/node/server.ts | 28 ++++--------- packages/protocol/test/server.test.ts | 4 +- packages/server/src/cli.ts | 30 ++++++++------ packages/server/src/server.ts | 7 ++-- packages/vscode/src/client.ts | 2 + .../vscode/src/fill/environmentService.ts | 5 +++ packages/vscode/src/fill/product.ts | 41 ++++++++++++------- packages/vscode/src/fill/storageDatabase.ts | 7 ++-- 9 files changed, 86 insertions(+), 57 deletions(-) diff --git a/packages/protocol/src/common/util.ts b/packages/protocol/src/common/util.ts index 45ff225a..983376c6 100644 --- a/packages/protocol/src/common/util.ts +++ b/packages/protocol/src/common/util.ts @@ -97,3 +97,22 @@ export const parse = (arg: string): any => { // tslint:disable-line no-any return arg ? convert(JSON.parse(arg)) : arg; }; + +export const mkdirP = async (path: string): Promise => { + // Since our fills require this file, we can't import them up top or we get + // circular dependency issue. + const { mkdir } = require("fs") as typeof import("fs"); + const { promisify } = require("util") as typeof import("util"); + const split = path.replace(/^\/*|\/*$/g, "").split("/"); + let dir = ""; + while (split.length > 0) { + dir += "/" + split.shift(); + try { + await promisify(mkdir)(dir); + } catch (error) { + if (error.code !== "EEXIST" && error.code !== "EISDIR") { + throw error; + } + } + } +}; diff --git a/packages/protocol/src/node/server.ts b/packages/protocol/src/node/server.ts index ac973163..fa36129c 100644 --- a/packages/protocol/src/node/server.ts +++ b/packages/protocol/src/node/server.ts @@ -1,16 +1,15 @@ import * as os from "os"; -import * as path from "path"; -import { mkdir } from "fs"; -import { promisify } from "util"; import { logger, field } from "@coder/logger"; import { Pong, ClientMessage, WorkingInitMessage, ServerMessage } from "../proto"; import { evaluate, ActiveEvaluation } from "./evaluate"; import { ForkProvider } from "../common/helpers"; import { ReadWriteConnection } from "../common/connection"; +import { mkdirP } from "../common/util"; export interface ServerOptions { readonly workingDirectory: string; readonly dataDirectory: string; + readonly cacheDirectory: string; readonly builtInExtensionsDirectory: string; readonly fork?: ForkProvider; } @@ -42,24 +41,11 @@ export class Server { return; } - // Ensure the data directory exists. - const mkdirP = async (path: string): Promise => { - const split = path.replace(/^\/*|\/*$/g, "").split("/"); - let dir = ""; - while (split.length > 0) { - dir += "/" + split.shift(); - try { - await promisify(mkdir)(dir); - } catch (error) { - if (error.code !== "EEXIST" && error.code !== "EISDIR") { - throw error; - } - } - } - }; - Promise.all([ mkdirP(path.join(this.options.dataDirectory, "User", "workspaceStorage")) ]).then(() => { - logger.info("Created data directory"); - }).catch((error) => { + Promise.all([ + mkdirP(this.options.cacheDirectory), + mkdirP(this.options.dataDirectory), + mkdirP(this.options.workingDirectory), + ]).catch((error) => { logger.error(error.message, field("error", error)); }); diff --git a/packages/protocol/test/server.test.ts b/packages/protocol/test/server.test.ts index bc11bbb2..23331089 100644 --- a/packages/protocol/test/server.test.ts +++ b/packages/protocol/test/server.test.ts @@ -4,10 +4,12 @@ describe("Server", () => { const dataDirectory = "/tmp/example"; const workingDirectory = "/working/dir"; const builtInExtensionsDirectory = "/tmp/example"; + const cacheDirectory = "/tmp/cache"; const client = createClient({ + builtInExtensionsDirectory, + cacheDirectory, dataDirectory, workingDirectory, - builtInExtensionsDirectory, }); it("should get init msg", (done) => { diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index 49706a7c..b44cdc4a 100644 --- a/packages/server/src/cli.ts +++ b/packages/server/src/cli.ts @@ -1,9 +1,11 @@ import { field, logger } from "@coder/logger"; +import { mkdirP } from "@coder/protocol"; import { ServerMessage, SharedProcessActiveMessage } from "@coder/protocol/src/proto"; import { Command, flags } from "@oclif/command"; import { fork, ForkOptions, ChildProcess } from "child_process"; import { randomFillSync } from "crypto"; import * as fs from "fs"; +import * as os from "os"; import * as path from "path"; import * as WebSocket from "ws"; import { createApp } from "./server"; @@ -50,6 +52,20 @@ export class Entry extends Command { const dataDir = path.resolve(flags["data-dir"] || path.join(dataHome, "code-server")); const workingDir = path.resolve(args["workdir"]); + if (!fs.existsSync(dataDir)) { + const oldDataDir = path.resolve(path.join(os.homedir(), ".code-server")); + if (fs.existsSync(oldDataDir)) { + fs.renameSync(oldDataDir, dataDir); + logger.info(`Moved data directory from ${oldDataDir} to ${dataDir}`); + } + } + + await Promise.all([ + mkdirP(cacheHome), + mkdirP(dataDir), + mkdirP(workingDir), + ]); + setupNativeModules(dataDir); const builtInExtensionsDir = path.resolve(buildDir || path.join(__dirname, ".."), "build/extensions"); if (flags["bootstrap-fork"]) { @@ -74,14 +90,6 @@ export class Entry extends Command { return requireFork(modulePath, JSON.parse(flags.args!), builtInExtensionsDir); } - if (!fs.existsSync(dataDir)) { - fs.mkdirSync(dataDir); - } - - if (!fs.existsSync(cacheHome)) { - fs.mkdirSync(cacheHome); - } - const logDir = path.join(cacheHome, "code-server/logs", new Date().toISOString().replace(/[-:.TZ]/g, "")); process.env.VSCODE_LOGS = logDir; @@ -173,6 +181,7 @@ export class Entry extends Command { builtInExtensionsDirectory: builtInExtensionsDir, dataDirectory: dataDir, workingDirectory: workingDir, + cacheDirectory: cacheHome, fork: (modulePath: string, args: string[], options: ForkOptions): ChildProcess => { if (options && options.env && options.env.AMD_ENTRYPOINT) { return forkModule(options.env.AMD_ENTRYPOINT, args, options, dataDir); @@ -188,11 +197,6 @@ export class Entry extends Command { } : undefined, }); - if (!fs.existsSync(workingDir)) { - logger.info("Creating working directory", field("working-dir", workingDir)); - fs.mkdirSync(workingDir); - } - logger.info("Starting webserver...", field("host", flags.host), field("port", flags.port)); app.server.listen(flags.port, flags.host); let clientId = 1; diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index b7b2a6ef..48873bbd 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -1,5 +1,5 @@ import { logger, field } from "@coder/logger"; -import { ReadWriteConnection } from "@coder/protocol"; +import { mkdirP, ReadWriteConnection } from "@coder/protocol"; import { Server, ServerOptions } from "@coder/protocol/src/node/server"; import * as express from "express"; //@ts-ignore @@ -20,7 +20,7 @@ import safeCompare = require("safe-compare"); import { TunnelCloseCode } from "@coder/tunnel/src/common"; import { handle as handleTunnel } from "@coder/tunnel/src/server"; import { createPortScanner } from "./portScanner"; -import { buildDir, isCli } from "./constants"; +import { buildDir } from "./constants"; interface CreateAppOptions { registerMiddleware?: (app: express.Application) => void; @@ -257,8 +257,9 @@ export const createApp = async (options: CreateAppOptions): Promise<{ req.on("data", (chunk) => { data.push(chunk); }); - req.on("end", () => { + req.on("end", async () => { const body = data.join(""); + await mkdirP(path.dirname(fullPath)); fs.writeFileSync(fullPath, body); logger.debug("Wrote resource", field("path", fullPath), field("content-length", body.length)); res.status(200); diff --git a/packages/vscode/src/client.ts b/packages/vscode/src/client.ts index 35e7d906..562ca37c 100644 --- a/packages/vscode/src/client.ts +++ b/packages/vscode/src/client.ts @@ -4,6 +4,7 @@ import Severity from "vs/base/common/severity"; import { INotificationService } from "vs/platform/notification/common/notification"; import { IStatusbarService, StatusbarAlignment } from "vs/platform/statusbar/common/statusbar"; import * as paths from "./fill/paths"; +import product from "./fill/product"; import "./vscode.scss"; import { MenuId, MenuRegistry } from "vs/platform/actions/common/actions"; import { CommandsRegistry } from "vs/platform/commands/common/commands"; @@ -14,6 +15,7 @@ class VSClient extends IdeClient { protected initialize(): Promise { return this.task("Start workbench", 1000, async (data, sharedData) => { paths._paths.initialize(data, sharedData); + product.initialize(data); process.env.SHELL = data.shell; // At this point everything should be filled, including `os`. `os` also // relies on `initData` but it listens first so it initialize before this diff --git a/packages/vscode/src/fill/environmentService.ts b/packages/vscode/src/fill/environmentService.ts index 615d5b06..c585de13 100644 --- a/packages/vscode/src/fill/environmentService.ts +++ b/packages/vscode/src/fill/environmentService.ts @@ -1,3 +1,4 @@ +import * as path from "path"; import * as paths from "./paths"; import * as environment from "vs/platform/environment/node/environmentService"; @@ -5,6 +6,10 @@ export class EnvironmentService extends environment.EnvironmentService { public get sharedIPCHandle(): string { return paths.getSocketPath() || super.sharedIPCHandle; } + + public get extensionsPath(): string { + return path.join(paths.getAppDataPath(), "extensions"); + } } const target = environment as typeof environment; diff --git a/packages/vscode/src/fill/product.ts b/packages/vscode/src/fill/product.ts index a861af78..9f5eb7de 100644 --- a/packages/vscode/src/fill/product.ts +++ b/packages/vscode/src/fill/product.ts @@ -1,26 +1,37 @@ +import { InitData } from "@coder/protocol"; import { IProductConfiguration } from "vs/platform/product/node/product"; -const product = { - nameShort: "code-server", - nameLong: "code-server", - dataFolderName: ".code-server", - extensionsGallery: { +class Product implements IProductConfiguration { + public nameShort = "code-server"; + public nameLong = "code-server"; + + private _dataFolderName: string | undefined; + public get dataFolderName(): string { + if (!this._dataFolderName) { + throw new Error("trying to access data folder name before it has been set"); + } + + return this._dataFolderName; + } + + public extensionsGallery = { serviceUrl: global && global.process && global.process.env.SERVICE_URL || process.env.SERVICE_URL || "https://v1.extapi.coder.com", - }, - extensionExecutionEnvironments: { + }; + + public 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'; + public fetchUrl = ""; + + public initialize(_data: InitData): void { + // Nothing at the moment; dataFolderName isn't used since we override the + // extension path. + } } -export default product; +export default new Product(); diff --git a/packages/vscode/src/fill/storageDatabase.ts b/packages/vscode/src/fill/storageDatabase.ts index b6ae95bc..de69d0ac 100644 --- a/packages/vscode/src/fill/storageDatabase.ts +++ b/packages/vscode/src/fill/storageDatabase.ts @@ -1,8 +1,9 @@ -import { readFile, writeFile, mkdir } from "fs"; +import { readFile, writeFile } from "fs"; import * as path from "path"; import { promisify } from "util"; import { IDisposable } from "@coder/disposable"; import { logger, field } from "@coder/logger"; +import { mkdirP } from "@coder/protocol"; import { Event } from "vs/base/common/event"; import * as workspaceStorage from "vs/base/node/storage"; import * as globalStorage from "vs/platform/storage/node/storageIpc"; @@ -77,9 +78,7 @@ class StorageDatabase implements workspaceStorage.IStorageDatabase { } private async save(): Promise { - try { - await promisify(mkdir)(path.dirname(this.path)); - } catch (ex) {} + await mkdirP(path.dirname(this.path)); return promisify(writeFile)(this.path, this.content); }