2020-07-21 13:20:51 +00:00
|
|
|
import { ActionRouter } from 'app/common/ActionRouter';
|
|
|
|
import { LocalPlugin } from 'app/common/plugin';
|
|
|
|
import { BaseComponent, createRpcLogger, warnIfNotReady } from 'app/common/PluginInstance';
|
|
|
|
import { GristAPI, RPC_GRISTAPI_INTERFACE } from 'app/plugin/GristAPI';
|
2022-07-04 14:14:55 +00:00
|
|
|
import log from 'app/server/lib/log';
|
2020-07-21 13:20:51 +00:00
|
|
|
import { getAppPathTo } from 'app/server/lib/places';
|
|
|
|
import { makeLinePrefixer } from 'app/server/lib/sandboxUtil';
|
|
|
|
import { exitPromise, timeoutReached } from 'app/server/lib/serverUtils';
|
|
|
|
import { ChildProcess, fork, ForkOptions } from 'child_process';
|
|
|
|
import * as fse from 'fs-extra';
|
|
|
|
import { IMessage, IMsgCustom, IMsgRpcCall, Rpc } from 'grain-rpc';
|
|
|
|
import * as path from 'path';
|
|
|
|
|
|
|
|
// Error for not yet implemented api.
|
|
|
|
class NotImplemented extends Error {
|
|
|
|
constructor(name: string) {
|
|
|
|
super(`calling ${name} from UnsafeNode is not yet implemented`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The unsafeNode component used by a PluginInstance.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
export class UnsafeNodeComponent extends BaseComponent {
|
|
|
|
private _child?: ChildProcess; /* plugin node code will run as separate process */
|
|
|
|
private _exited: Promise<void>; /* fulfulled when process has completed */
|
|
|
|
private _rpc: Rpc;
|
|
|
|
private _pluginPath: string;
|
|
|
|
private _pluginId: string;
|
|
|
|
private _actionRouter: ActionRouter;
|
|
|
|
|
|
|
|
private _gristAPI: GristAPI = {
|
|
|
|
render() { throw new NotImplemented('render'); },
|
|
|
|
dispose() { throw new NotImplemented('dispose'); },
|
|
|
|
subscribe: (tableId: string) => this._actionRouter.subscribeTable(tableId),
|
|
|
|
unsubscribe: (tableId: string) => this._actionRouter.unsubscribeTable(tableId),
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @arg parent: the plugin instance this component is part of
|
|
|
|
* @arg _mainPath: main script file to run
|
|
|
|
* @arg appRoot: root path for application (important for setting a good NODE_PATH)
|
|
|
|
* @arg _gristDocPath: path to the current Grist doc (to which this plugin applies).
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
constructor(plugin: LocalPlugin, pluginRpc: Rpc, private _mainPath: string, public appRoot: string,
|
|
|
|
private _gristDocPath: string,
|
|
|
|
rpcLogger = createRpcLogger(log, `PLUGIN ${plugin.id}/${_mainPath} UnsafeNode:`)) {
|
|
|
|
super(plugin.manifest, rpcLogger);
|
|
|
|
this._pluginPath = plugin.path;
|
|
|
|
this._pluginId = plugin.id;
|
|
|
|
this._rpc = new Rpc({
|
|
|
|
sendMessage: (msg) => this.sendMessage(msg),
|
|
|
|
logger: rpcLogger,
|
|
|
|
});
|
|
|
|
this._rpc.registerForwarder('*', pluginRpc);
|
|
|
|
this._rpc.registerImpl<GristAPI>(RPC_GRISTAPI_INTERFACE, this._gristAPI);
|
|
|
|
this._actionRouter = new ActionRouter(this._rpc);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async sendMessage(data: IMessage): Promise<void> {
|
|
|
|
if (!this._child) {
|
|
|
|
await this.activateImplementation();
|
|
|
|
}
|
|
|
|
this._child!.send(data);
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
public receiveAction(action: any[]) {
|
|
|
|
this._actionRouter.process(action)
|
|
|
|
.catch((err: any) => log.warn('unsafeNode[%s] receiveAction failed with %s',
|
|
|
|
this._child ? this._child.pid : "NULL", err));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* Create the child node process needed for this component.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
protected async activateImplementation(): Promise<void> {
|
|
|
|
log.info(`unsafeNode operating in ${this._pluginPath}`);
|
|
|
|
const base = this._pluginPath;
|
|
|
|
const script = path.resolve(base, this._mainPath);
|
|
|
|
await fse.access(script, fse.constants.R_OK);
|
|
|
|
// Time to set up the node search path the client will see.
|
|
|
|
// We take our own, via Module.globalPaths, a poorly documented
|
|
|
|
// method listing the search path for the active node program
|
|
|
|
// https://github.com/nodejs/node/blob/master/test/parallel/test-module-globalpaths-nodepath.js
|
|
|
|
const paths = require('module').globalPaths.slice().concat([
|
|
|
|
// add the path to the plugin itself
|
|
|
|
path.resolve(base),
|
|
|
|
// add the path to grist's public api
|
|
|
|
getAppPathTo(this.appRoot, 'public-api'),
|
|
|
|
// add the path to the node_modules packaged with grist, in electron form
|
|
|
|
getAppPathTo(this.appRoot, 'node_modules')
|
|
|
|
]);
|
|
|
|
const env = Object.assign({}, process.env, {
|
|
|
|
NODE_PATH: paths.join(path.delimiter),
|
|
|
|
GRIST_PLUGIN_PATH: `${this._pluginId}/${this._mainPath}`,
|
|
|
|
GRIST_DOC_PATH: this._gristDocPath,
|
|
|
|
});
|
|
|
|
const electronVersion: string = (process.versions as any).electron;
|
|
|
|
if (electronVersion) {
|
|
|
|
// Pass along the fact that we are running under an electron-ified node, for the purposes of
|
|
|
|
// finding binaries (sqlite3 in particular).
|
|
|
|
env.ELECTRON_VERSION = electronVersion;
|
|
|
|
}
|
|
|
|
const child = this._child = fork(script, [], {
|
|
|
|
env,
|
|
|
|
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
|
|
} as ForkOptions); // Explicit cast only because node-6 typings mistakenly omit stdio property
|
|
|
|
|
|
|
|
log.info("unsafeNode[%s] started %s", child.pid, script);
|
|
|
|
|
|
|
|
// Important to use exitPromise() before events from child may be received, so don't call
|
|
|
|
// yield or await between fork and here.
|
|
|
|
this._exited = exitPromise(child)
|
|
|
|
.then(code => log.info("unsafeNode[%s] exited with %s", child.pid, code))
|
|
|
|
.catch(err => log.warn("unsafeNode[%s] failed with %s", child.pid, err))
|
|
|
|
.then(() => { this._child = undefined; });
|
|
|
|
|
(core) Speed up and upgrade build.
Summary:
- Upgrades to build-related packages:
- Upgrade typescript, related libraries and typings.
- Upgrade webpack, eslint; add tsc-watch, node-dev, eslint_d.
- Build organization changes:
- Build webpack from original typescript, transpiling only; with errors still
reported by a background tsc watching process.
- Typescript-related changes:
- Reduce imports of AWS dependencies (very noticeable speedup)
- Avoid auto-loading global @types
- Client code is now built with isolatedModules flag (for safe transpilation)
- Use allowJs to avoid copying JS files manually.
- Linting changes
- Enhance Arcanist ESLintLinter to run before/after commands, and set up to use eslint_d
- Update eslint config, and include .eslintignore to avoid linting generated files.
- Include a bunch of eslint-prompted and eslint-generated fixes
- Add no-unused-expression rule to eslint, and fix a few warnings about it
- Other items:
- Refactor cssInput to avoid circular dependency
- Remove a bit of unused code, libraries, dependencies
Test Plan: No behavior changes, all existing tests pass. There are 30 tests fewer reported because `test_gpath.py` was removed (it's been unused for years)
Reviewers: paulfitz
Reviewed By: paulfitz
Subscribers: paulfitz
Differential Revision: https://phab.getgrist.com/D3498
2022-06-27 20:09:41 +00:00
|
|
|
child.stdout!.on('data', makeLinePrefixer('PLUGIN stdout: '));
|
|
|
|
child.stderr!.on('data', makeLinePrefixer('PLUGIN stderr: '));
|
2020-07-21 13:20:51 +00:00
|
|
|
|
|
|
|
warnIfNotReady(this._rpc, 3000, "Plugin isn't ready; be sure to call grist.ready() from plugin");
|
|
|
|
child.on('message', this._rpc.receiveMessage.bind(this._rpc));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* Remove the child node process needed for this component.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
protected async deactivateImplementation(): Promise<void> {
|
|
|
|
if (!this._child) {
|
|
|
|
log.info('unsafeNode deactivating: no child process');
|
|
|
|
} else {
|
|
|
|
log.info('unsafeNode[%s] deactivate: disconnecting child', this._child.pid);
|
|
|
|
this._child.disconnect();
|
|
|
|
if (await timeoutReached(2000, this._exited)) {
|
|
|
|
log.info("unsafeNode[%s] deactivate: sending SIGTERM", this._child.pid);
|
|
|
|
this._child.kill('SIGTERM');
|
|
|
|
}
|
|
|
|
if (await timeoutReached(5000, this._exited)) {
|
|
|
|
log.warn("unsafeNode[%s] deactivate: child still has not exited", this._child.pid);
|
|
|
|
} else {
|
|
|
|
log.info("unsafeNode deactivate: child exited");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected doForwardCall(c: IMsgRpcCall): Promise<any> {
|
|
|
|
return this._rpc.forwardCall({...c, mdest: ''});
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async doForwardMessage(c: IMsgCustom): Promise<any> {
|
|
|
|
return this._rpc.forwardMessage({...c, mdest: ''});
|
|
|
|
}
|
|
|
|
}
|