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.
This commit is contained in:
Asher 2019-03-12 11:12:50 -05:00
parent 0a9f5d8eee
commit e597d49912
No known key found for this signature in database
GPG Key ID: 7BB4BA9C783D2BBC
9 changed files with 86 additions and 57 deletions

View File

@ -97,3 +97,22 @@ export const parse = (arg: string): any => { // tslint:disable-line no-any
return arg ? convert(JSON.parse(arg)) : arg; return arg ? convert(JSON.parse(arg)) : arg;
}; };
export const mkdirP = async (path: string): Promise<void> => {
// 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;
}
}
}
};

View File

@ -1,16 +1,15 @@
import * as os from "os"; 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 { logger, field } from "@coder/logger";
import { Pong, ClientMessage, WorkingInitMessage, ServerMessage } from "../proto"; import { Pong, ClientMessage, WorkingInitMessage, ServerMessage } from "../proto";
import { evaluate, ActiveEvaluation } from "./evaluate"; import { evaluate, ActiveEvaluation } from "./evaluate";
import { ForkProvider } from "../common/helpers"; import { ForkProvider } from "../common/helpers";
import { ReadWriteConnection } from "../common/connection"; import { ReadWriteConnection } from "../common/connection";
import { mkdirP } from "../common/util";
export interface ServerOptions { export interface ServerOptions {
readonly workingDirectory: string; readonly workingDirectory: string;
readonly dataDirectory: string; readonly dataDirectory: string;
readonly cacheDirectory: string;
readonly builtInExtensionsDirectory: string; readonly builtInExtensionsDirectory: string;
readonly fork?: ForkProvider; readonly fork?: ForkProvider;
} }
@ -42,24 +41,11 @@ export class Server {
return; return;
} }
// Ensure the data directory exists. Promise.all([
const mkdirP = async (path: string): Promise<void> => { mkdirP(this.options.cacheDirectory),
const split = path.replace(/^\/*|\/*$/g, "").split("/"); mkdirP(this.options.dataDirectory),
let dir = ""; mkdirP(this.options.workingDirectory),
while (split.length > 0) { ]).catch((error) => {
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) => {
logger.error(error.message, field("error", error)); logger.error(error.message, field("error", error));
}); });

View File

@ -4,10 +4,12 @@ describe("Server", () => {
const dataDirectory = "/tmp/example"; const dataDirectory = "/tmp/example";
const workingDirectory = "/working/dir"; const workingDirectory = "/working/dir";
const builtInExtensionsDirectory = "/tmp/example"; const builtInExtensionsDirectory = "/tmp/example";
const cacheDirectory = "/tmp/cache";
const client = createClient({ const client = createClient({
builtInExtensionsDirectory,
cacheDirectory,
dataDirectory, dataDirectory,
workingDirectory, workingDirectory,
builtInExtensionsDirectory,
}); });
it("should get init msg", (done) => { it("should get init msg", (done) => {

View File

@ -1,9 +1,11 @@
import { field, logger } from "@coder/logger"; import { field, logger } from "@coder/logger";
import { mkdirP } from "@coder/protocol";
import { ServerMessage, SharedProcessActiveMessage } from "@coder/protocol/src/proto"; import { ServerMessage, SharedProcessActiveMessage } from "@coder/protocol/src/proto";
import { Command, flags } from "@oclif/command"; import { Command, flags } from "@oclif/command";
import { fork, ForkOptions, ChildProcess } from "child_process"; import { fork, ForkOptions, ChildProcess } from "child_process";
import { randomFillSync } from "crypto"; import { randomFillSync } from "crypto";
import * as fs from "fs"; import * as fs from "fs";
import * as os from "os";
import * as path from "path"; import * as path from "path";
import * as WebSocket from "ws"; import * as WebSocket from "ws";
import { createApp } from "./server"; 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 dataDir = path.resolve(flags["data-dir"] || path.join(dataHome, "code-server"));
const workingDir = path.resolve(args["workdir"]); 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); setupNativeModules(dataDir);
const builtInExtensionsDir = path.resolve(buildDir || path.join(__dirname, ".."), "build/extensions"); const builtInExtensionsDir = path.resolve(buildDir || path.join(__dirname, ".."), "build/extensions");
if (flags["bootstrap-fork"]) { if (flags["bootstrap-fork"]) {
@ -74,14 +90,6 @@ export class Entry extends Command {
return requireFork(modulePath, JSON.parse(flags.args!), builtInExtensionsDir); 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, "")); const logDir = path.join(cacheHome, "code-server/logs", new Date().toISOString().replace(/[-:.TZ]/g, ""));
process.env.VSCODE_LOGS = logDir; process.env.VSCODE_LOGS = logDir;
@ -173,6 +181,7 @@ export class Entry extends Command {
builtInExtensionsDirectory: builtInExtensionsDir, builtInExtensionsDirectory: builtInExtensionsDir,
dataDirectory: dataDir, dataDirectory: dataDir,
workingDirectory: workingDir, workingDirectory: workingDir,
cacheDirectory: cacheHome,
fork: (modulePath: string, args: string[], options: ForkOptions): ChildProcess => { fork: (modulePath: string, args: string[], options: ForkOptions): ChildProcess => {
if (options && options.env && options.env.AMD_ENTRYPOINT) { if (options && options.env && options.env.AMD_ENTRYPOINT) {
return forkModule(options.env.AMD_ENTRYPOINT, args, options, dataDir); return forkModule(options.env.AMD_ENTRYPOINT, args, options, dataDir);
@ -188,11 +197,6 @@ export class Entry extends Command {
} : undefined, } : 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)); logger.info("Starting webserver...", field("host", flags.host), field("port", flags.port));
app.server.listen(flags.port, flags.host); app.server.listen(flags.port, flags.host);
let clientId = 1; let clientId = 1;

View File

@ -1,5 +1,5 @@
import { logger, field } from "@coder/logger"; 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 { Server, ServerOptions } from "@coder/protocol/src/node/server";
import * as express from "express"; import * as express from "express";
//@ts-ignore //@ts-ignore
@ -20,7 +20,7 @@ import safeCompare = require("safe-compare");
import { TunnelCloseCode } from "@coder/tunnel/src/common"; import { TunnelCloseCode } from "@coder/tunnel/src/common";
import { handle as handleTunnel } from "@coder/tunnel/src/server"; import { handle as handleTunnel } from "@coder/tunnel/src/server";
import { createPortScanner } from "./portScanner"; import { createPortScanner } from "./portScanner";
import { buildDir, isCli } from "./constants"; import { buildDir } from "./constants";
interface CreateAppOptions { interface CreateAppOptions {
registerMiddleware?: (app: express.Application) => void; registerMiddleware?: (app: express.Application) => void;
@ -257,8 +257,9 @@ export const createApp = async (options: CreateAppOptions): Promise<{
req.on("data", (chunk) => { req.on("data", (chunk) => {
data.push(chunk); data.push(chunk);
}); });
req.on("end", () => { req.on("end", async () => {
const body = data.join(""); const body = data.join("");
await mkdirP(path.dirname(fullPath));
fs.writeFileSync(fullPath, body); fs.writeFileSync(fullPath, body);
logger.debug("Wrote resource", field("path", fullPath), field("content-length", body.length)); logger.debug("Wrote resource", field("path", fullPath), field("content-length", body.length));
res.status(200); res.status(200);

View File

@ -4,6 +4,7 @@ import Severity from "vs/base/common/severity";
import { INotificationService } from "vs/platform/notification/common/notification"; import { INotificationService } from "vs/platform/notification/common/notification";
import { IStatusbarService, StatusbarAlignment } from "vs/platform/statusbar/common/statusbar"; import { IStatusbarService, StatusbarAlignment } from "vs/platform/statusbar/common/statusbar";
import * as paths from "./fill/paths"; import * as paths from "./fill/paths";
import product from "./fill/product";
import "./vscode.scss"; import "./vscode.scss";
import { MenuId, MenuRegistry } from "vs/platform/actions/common/actions"; import { MenuId, MenuRegistry } from "vs/platform/actions/common/actions";
import { CommandsRegistry } from "vs/platform/commands/common/commands"; import { CommandsRegistry } from "vs/platform/commands/common/commands";
@ -14,6 +15,7 @@ class VSClient extends IdeClient {
protected initialize(): Promise<void> { protected initialize(): Promise<void> {
return this.task("Start workbench", 1000, async (data, sharedData) => { return this.task("Start workbench", 1000, async (data, sharedData) => {
paths._paths.initialize(data, sharedData); paths._paths.initialize(data, sharedData);
product.initialize(data);
process.env.SHELL = data.shell; process.env.SHELL = data.shell;
// At this point everything should be filled, including `os`. `os` also // At this point everything should be filled, including `os`. `os` also
// relies on `initData` but it listens first so it initialize before this // relies on `initData` but it listens first so it initialize before this

View File

@ -1,3 +1,4 @@
import * as path from "path";
import * as paths from "./paths"; import * as paths from "./paths";
import * as environment from "vs/platform/environment/node/environmentService"; import * as environment from "vs/platform/environment/node/environmentService";
@ -5,6 +6,10 @@ export class EnvironmentService extends environment.EnvironmentService {
public get sharedIPCHandle(): string { public get sharedIPCHandle(): string {
return paths.getSocketPath() || super.sharedIPCHandle; return paths.getSocketPath() || super.sharedIPCHandle;
} }
public get extensionsPath(): string {
return path.join(paths.getAppDataPath(), "extensions");
}
} }
const target = environment as typeof environment; const target = environment as typeof environment;

View File

@ -1,26 +1,37 @@
import { InitData } from "@coder/protocol";
import { IProductConfiguration } from "vs/platform/product/node/product"; import { IProductConfiguration } from "vs/platform/product/node/product";
const product = { class Product implements IProductConfiguration {
nameShort: "code-server", public nameShort = "code-server";
nameLong: "code-server", public nameLong = "code-server";
dataFolderName: ".code-server",
extensionsGallery: { 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 serviceUrl: global && global.process && global.process.env.SERVICE_URL
|| process.env.SERVICE_URL || process.env.SERVICE_URL
|| "https://v1.extapi.coder.com", || "https://v1.extapi.coder.com",
}, };
extensionExecutionEnvironments: {
public extensionExecutionEnvironments = {
"wayou.vscode-todo-highlight": "worker", "wayou.vscode-todo-highlight": "worker",
"vscodevim.vim": "worker", "vscodevim.vim": "worker",
"coenraads.bracket-pair-colorizer": "worker", "coenraads.bracket-pair-colorizer": "worker",
}, };
fetchUrl: "",
} as IProductConfiguration;
if (process.env['VSCODE_DEV']) { public fetchUrl = "";
product.nameShort += ' Dev';
product.nameLong += ' Dev'; public initialize(_data: InitData): void {
product.dataFolderName += '-dev'; // Nothing at the moment; dataFolderName isn't used since we override the
// extension path.
}
} }
export default product; export default new Product();

View File

@ -1,8 +1,9 @@
import { readFile, writeFile, mkdir } from "fs"; import { readFile, writeFile } from "fs";
import * as path from "path"; import * as path from "path";
import { promisify } from "util"; import { promisify } from "util";
import { IDisposable } from "@coder/disposable"; import { IDisposable } from "@coder/disposable";
import { logger, field } from "@coder/logger"; import { logger, field } from "@coder/logger";
import { mkdirP } from "@coder/protocol";
import { Event } from "vs/base/common/event"; import { Event } from "vs/base/common/event";
import * as workspaceStorage from "vs/base/node/storage"; import * as workspaceStorage from "vs/base/node/storage";
import * as globalStorage from "vs/platform/storage/node/storageIpc"; import * as globalStorage from "vs/platform/storage/node/storageIpc";
@ -77,9 +78,7 @@ class StorageDatabase implements workspaceStorage.IStorageDatabase {
} }
private async save(): Promise<void> { private async save(): Promise<void> {
try { await mkdirP(path.dirname(this.path));
await promisify(mkdir)(path.dirname(this.path));
} catch (ex) {}
return promisify(writeFile)(this.path, this.content); return promisify(writeFile)(this.path, this.content);
} }