Skip to content

Commit

Permalink
[filesystem] run watching in the separate process
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosiakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov committed Dec 2, 2017
1 parent ec299e5 commit 6540675
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 55 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ examples/*/webpack.config.js
.browser_modules
**/docs/api
package-backup.json
.history
.history
.Trash-*
2 changes: 2 additions & 0 deletions packages/core/src/browser/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ export class Tree implements ITree {
if (ICompositeTreeNode.is(parent)) {
this.resolveChildren(parent).then(children => this.setChildren(parent, children));
}
// FIXME: it shoud not be here
// if the idea was to support refreshing of all kind of nodes, then API should be adapted
this.fireChanged();
}

Expand Down
61 changes: 61 additions & 0 deletions packages/core/src/common/messaging/connection-error-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/

import { Message } from "vscode-jsonrpc";

export interface ResolvedConnectionErrorHandlerOptions {
readonly serverName: string
/**
* The maximum amout of errors allowed before stopping the server.
*/
readonly maxErrors: number
/**
* The maimum amount of restarts allowed in the restart interval.
*/
readonly maxRestarts: number
/**
* In minutes.
*/
readonly restartInterval: number
}

export type ConnectionErrorHandlerOptions = Partial<ResolvedConnectionErrorHandlerOptions> & {
readonly serverName: string
};

export class ConnectionErrorHandler {

protected readonly options: ResolvedConnectionErrorHandlerOptions;
constructor(options: ConnectionErrorHandlerOptions) {
this.options = {
maxErrors: 3,
maxRestarts: 5,
restartInterval: 3,
...options
};
}

shouldStop(error: Error, message?: Message, count?: number): boolean {
return !count || count > this.options.maxErrors;
}

protected readonly restarts: number[] = [];
shouldRestart(): string | undefined {
this.restarts.push(Date.now());
if (this.restarts.length < this.options.maxRestarts) {
return undefined;
}
const diff = this.restarts[this.restarts.length - 1] - this.restarts[0];
if (diff <= this.options.restartInterval * 60 * 1000) {
return `The ${this.options.serverName} server crashed ${this.options.maxRestarts} times in the last ${this.options.restartInterval} minutes.
The server will not be restarted.`;
}
this.restarts.shift();
return undefined;
}

}
1 change: 1 addition & 0 deletions packages/core/src/common/messaging/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

export * from './handler';
export * from './proxy-factory';
export * from './connection-error-handler';
2 changes: 0 additions & 2 deletions packages/filesystem/src/common/filesystem-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,10 @@ export function createFileSystemPreferences(preferences: PreferenceService): Fil
}

export function bindFileSystemPreferences(bind: interfaces.Bind): void {

bind(FileSystemPreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get(PreferenceService);
return createFileSystemPreferences(preferences);
});

bind(PreferenceContribution).toConstantValue({ schema: filesystemPreferenceSchema });

}
7 changes: 5 additions & 2 deletions packages/filesystem/src/common/filesystem-watcher-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ export class ReconnectingFileSystemWatcherServer implements FileSystemWatcherSer
constructor(
@inject(FileSystemWatcherServerProxy) protected readonly proxy: FileSystemWatcherServerProxy
) {
this.proxy.onDidOpenConnection(() => this.reconnect());
const onInitialized = this.proxy.onDidOpenConnection(() => {
onInitialized.dispose();
this.proxy.onDidOpenConnection(() => this.reconnect());
});
}

protected reconnect(): void {
Expand Down Expand Up @@ -104,7 +107,7 @@ export class ReconnectingFileSystemWatcherServer implements FileSystemWatcherSer
return Promise.resolve();
}

setClient(client: FileSystemWatcherClient): void {
setClient(client: FileSystemWatcherClient | undefined): void {
this.proxy.setClient(client);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,30 @@
*/

import { watch } from "chokidar";
import { injectable, inject } from "inversify";
import URI from "@theia/core/lib/common/uri";
import { Disposable, DisposableCollection, ILogger } from '@theia/core/lib/common';
import { FileUri } from "@theia/core/lib/node";
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { FileUri } from "@theia/core/lib/node/file-uri";
import {
FileChange,
FileChangeType,
FileSystemWatcherClient,
FileSystemWatcherServer,
WatchOptions
} from '../common/filesystem-watcher-protocol';
} from '../../common/filesystem-watcher-protocol';

// tslint:disable:no-console
// tslint:disable:no-any

export interface WatchError {
readonly code: string;
readonly filename: string;
}
export namespace WatchError {
export function is(error: any): error is WatchError {
return ('code' in error) && ('filename' in error) && error.code !== undefined && error.filename !== undefined;
}
}

@injectable()
export class ChokidarFileSystemWatcherServer implements FileSystemWatcherServer {

protected client: FileSystemWatcherClient | undefined;
Expand All @@ -38,40 +44,42 @@ export class ChokidarFileSystemWatcherServer implements FileSystemWatcherServer
protected readonly toDisposeOnFileChange = new DisposableCollection();

/* Did we print the message about exhausted inotify watches yet? */
protected printedENOSPCError: boolean = false;
protected printedENOSPCError = false;

constructor(
@inject(ILogger) protected readonly logger: ILogger
) { }
constructor(protected readonly options: {
verbose: boolean
} = { verbose: false }) { }

dispose(): void {
this.toDispose.dispose();
}

watchFileChanges(uri: string, options: WatchOptions = { ignored: [] }): Promise<number> {
watchFileChanges(uri: string, rawOptions?: WatchOptions): Promise<number> {
const options: WatchOptions = {
ignored: [],
...rawOptions
};
const watcherId = this.watcherSequence++;
const paths = this.toPaths(uri);
this.logger.info(`Starting watching:`, paths);
this.debug('Starting watching:', paths);
return new Promise<number>(resolve => {
if (options.ignored.length > 0) {
this.logger.debug(log =>
log('Files ignored for watching', options.ignored)
);
this.debug('Files ignored for watching', options.ignored);
}
const watcher = watch(paths, {
ignoreInitial: true,
ignored: options.ignored
});
watcher.once('ready', () => {
this.logger.info(`Started watching:`, paths);
console.info('Started watching:', paths);
resolve(watcherId);
});

watcher.on('error', error => {
if (this.isWatchError(error)) {
if (WatchError.is(error)) {
this.handleWatchError(error);
} else {
this.logger.error('Unknown file watch error:', error);
console.error('Unknown file watch error:', error);
}
});

Expand All @@ -82,9 +90,9 @@ export class ChokidarFileSystemWatcherServer implements FileSystemWatcherServer
watcher.on('unlinkDir', path => this.pushDeleted(watcherId, path));
const disposable = Disposable.create(() => {
this.watchers.delete(watcherId);
this.logger.info(`Stopping watching:`, paths);
this.debug('Stopping watching:', paths);
watcher.close();
this.logger.info(`Stopped watching.`);
console.info('Stopped watching.');
});
this.watchers.set(watcherId, disposable);
this.toDispose.push(disposable);
Expand All @@ -109,23 +117,17 @@ export class ChokidarFileSystemWatcherServer implements FileSystemWatcherServer
}

protected pushAdded(watcherId: number, path: string): void {
this.logger.debug(log =>
log(`Added:`, path)
);
this.debug('Added:', path);
this.pushFileChange(watcherId, path, FileChangeType.ADDED);
}

protected pushUpdated(watcherId: number, path: string): void {
this.logger.debug(log =>
log(`Updated:`, path)
);
this.debug('Updated:', path);
this.pushFileChange(watcherId, path, FileChangeType.UPDATED);
}

protected pushDeleted(watcherId: number, path: string): void {
this.logger.debug(log =>
log(`Deleted:`, path)
);
this.debug('Deleted:', path);
this.pushFileChange(watcherId, path, FileChangeType.DELETED);
}

Expand All @@ -147,10 +149,6 @@ export class ChokidarFileSystemWatcherServer implements FileSystemWatcherServer
}
}

protected isWatchError(error: any): error is WatchError {
return ('code' in error) && ('filename' in error) && error.code !== undefined && error.filename !== undefined;
}

/**
* Given a watch error object, print a user-friendly error message that
* explains what failed, and what can be done about it.
Expand Down Expand Up @@ -186,7 +184,13 @@ message will appear only once.";
break;
}

this.logger.error(`Error watching ${error.filename}: ${msg}`);
console.error(`Error watching ${error.filename}: ${msg}`);
}

protected debug(...params: any[]): void {
if (this.options.verbose) {
console.log(...params);
}
}

}
54 changes: 54 additions & 0 deletions packages/filesystem/src/node/chokidar-watcher/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/

import 'reflect-metadata';
import * as yargs from 'yargs';
import { ConsoleLogger } from 'vscode-ws-jsonrpc/lib/logger';
import { createMessageConnection, IPCMessageReader, IPCMessageWriter, Trace } from 'vscode-jsonrpc';
import { JsonRpcProxyFactory } from '@theia/core';
import { FileSystemWatcherClient } from '../../common/filesystem-watcher-protocol';
import { ChokidarFileSystemWatcherServer } from './chokidar-filesystem-watcher';

// tslint:disable:no-console
// tslint:disable:no-any

const options: {
verbose: boolean
} = yargs.option('vebose', {
default: false,
alias: 'v',
type: 'boolean'
}).argv as any;

const reader = new IPCMessageReader(process);
const writer = new IPCMessageWriter(process);
const logger = new ConsoleLogger();
const connection = createMessageConnection(reader, writer, logger);
connection.trace(Trace.Off, {
log: (message, data) => console.log(`${message} ${data}`)
});

const server = new ChokidarFileSystemWatcherServer(options);
const factory = new JsonRpcProxyFactory<FileSystemWatcherClient>(server);
server.setClient(factory.createProxy());
factory.listen(connection);

// FIXME extract the utility function to fork Theia process
if (process.env['THEIA_PARENT_PID']) {
const parentPid = Number(process.env['THEIA_PARENT_PID']);

if (typeof parentPid === 'number' && !isNaN(parentPid)) {
setInterval(function () {
try {
// throws an exception if the main process doesn't exist anymore.
process.kill(parentPid, 0);
} catch (e) {
process.exit();
}
}, 5000);
}
}
6 changes: 3 additions & 3 deletions packages/filesystem/src/node/filesystem-backend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ import { ConnectionHandler, JsonRpcConnectionHandler } from "@theia/core/lib/com
import { FileSystemNode } from './node-filesystem';
import { FileSystemWatcher, FileSystem, FileSystemClient, fileSystemPath, bindFileSystemPreferences } from "../common";
import { FileSystemWatcherServer, FileSystemWatcherClient, fileSystemWatcherPath } from '../common/filesystem-watcher-protocol';
import { ChokidarFileSystemWatcherServer } from './chokidar-filesystem-watcher';
import { HostFileSystemWatcherServer } from './host-filesystem-watcher';

export function bindFileSystem(bind: interfaces.Bind): void {
bind(FileSystemNode).toSelf().inSingletonScope();
bind(FileSystem).toDynamicValue(ctx => ctx.container.get(FileSystemNode)).inSingletonScope();
}

export function bindFileSystemWatcherServer(bind: interfaces.Bind): void {
bind(ChokidarFileSystemWatcherServer).toSelf();
bind(HostFileSystemWatcherServer).toSelf();
bind(FileSystemWatcherServer).toDynamicValue(ctx =>
ctx.container.get(ChokidarFileSystemWatcherServer)
ctx.container.get(HostFileSystemWatcherServer)
);
}

Expand Down
Loading

0 comments on commit 6540675

Please sign in to comment.