mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) move home server into core
Summary: This moves enough server material into core to run a home server. The data engine is not yet incorporated (though in manual testing it works when ported). Test Plan: existing tests pass Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2552
This commit is contained in:
213
app/common/PluginInstance.ts
Normal file
213
app/common/PluginInstance.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import {IForwarderDest, IMessage, IMsgCustom, IMsgRpcCall, IRpcLogger, MsgType, Rpc} from 'grain-rpc';
|
||||
import {Checker} from "ts-interface-checker";
|
||||
|
||||
import {InactivityTimer} from 'app/common/InactivityTimer';
|
||||
import {LocalPlugin} from 'app/common/plugin';
|
||||
import {BarePlugin} from 'app/plugin/PluginManifest';
|
||||
|
||||
import {Implementation} from 'app/plugin/PluginManifest';
|
||||
import {RenderOptions, RenderTarget} from 'app/plugin/RenderOptions';
|
||||
|
||||
|
||||
export type ComponentKind = "safeBrowser" | "safePython" | "unsafeNode";
|
||||
|
||||
// Describes a function that appends some html content to `containerElement` given some
|
||||
// options. Usefull for provided by a plugin.
|
||||
export type TargetRenderFunc = (containerElement: HTMLElement, options?: RenderOptions) => void;
|
||||
|
||||
/**
|
||||
* The `BaseComponent` is the base implementation for a plugins' component. It exposes methods
|
||||
* related to its activation. It provides basic features including the inactivity timer, activated
|
||||
* state for the component. A custom component must override the `deactivateImplementation`,
|
||||
* `activeImplementation` and `useRemoteAPI` methods.
|
||||
*/
|
||||
export abstract class BaseComponent implements IForwarderDest {
|
||||
|
||||
public inactivityTimer: InactivityTimer;
|
||||
private _activated: boolean = false;
|
||||
|
||||
constructor(plugin: BarePlugin, private _logger: IRpcLogger) {
|
||||
const deactivate = plugin.components.deactivate;
|
||||
const delay = (deactivate && deactivate.inactivitySec) ? deactivate.inactivitySec : 300;
|
||||
this.inactivityTimer = new InactivityTimer(() => this.deactivate(), delay * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wether the Component component have been activated.
|
||||
*/
|
||||
public activated(): boolean {
|
||||
return this._activated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the component.
|
||||
*/
|
||||
public async activate(): Promise<void> {
|
||||
if (this._logger.info) { this._logger.info("Activating plugin component"); }
|
||||
await this.activateImplementation();
|
||||
this._activated = true;
|
||||
this.inactivityTimer.enable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Force deactivate the component.
|
||||
*/
|
||||
public async deactivate(): Promise<void> {
|
||||
if (this._activated) {
|
||||
if (this._logger.info) { this._logger.info("Deactivating plugin component"); }
|
||||
this._activated = false;
|
||||
// Cancel the timer to ensure we don't have an unnecessary hanging timeout (in tests it will
|
||||
// prevent node from exiting, but also it's just wasteful).
|
||||
this.inactivityTimer.disable();
|
||||
try {
|
||||
await this.deactivateImplementation();
|
||||
} catch (e) {
|
||||
// If it fails, we warn and swallow the exception (or it would be an unhandled rejection).
|
||||
if (this._logger.warn) { this._logger.warn(`Deactivate failed: ${e.message}`); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async forwardCall(c: IMsgRpcCall): Promise<any> {
|
||||
if (!this._activated) { await this.activate(); }
|
||||
return await this.inactivityTimer.disableUntilFinish(this.doForwardCall(c));
|
||||
}
|
||||
|
||||
public async forwardMessage(msg: IMsgCustom): Promise<any> {
|
||||
if (!this._activated) { await this.activate(); }
|
||||
this.inactivityTimer.ping();
|
||||
this.doForwardMessage(msg); // tslint:disable-line:no-floating-promises TODO
|
||||
}
|
||||
|
||||
protected abstract doForwardCall(c: IMsgRpcCall): Promise<any>;
|
||||
|
||||
protected abstract doForwardMessage(msg: IMsgCustom): Promise<any>;
|
||||
|
||||
protected abstract deactivateImplementation(): Promise<void>;
|
||||
|
||||
protected abstract activateImplementation(): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Node Implementation for the PluginElement interface. A PluginInstance take care of activation of
|
||||
* the the plugins's components (activating, timing and deactivating), and create the api's for each contributions.
|
||||
*
|
||||
* Do not try to instanciate yourself, PluginManager does it for you. Instead use the
|
||||
* PluginManager.getPlugin(id) method that get instances for you.
|
||||
*
|
||||
*/
|
||||
export class PluginInstance {
|
||||
|
||||
public rpc: Rpc;
|
||||
public safeBrowser?: BaseComponent;
|
||||
public unsafeNode?: BaseComponent;
|
||||
public safePython?: BaseComponent;
|
||||
|
||||
private _renderTargets: Map<RenderTarget, TargetRenderFunc> = new Map();
|
||||
|
||||
private _nextRenderTargetId = 0;
|
||||
|
||||
constructor(public definition: LocalPlugin, rpcLogger: IRpcLogger) {
|
||||
|
||||
const rpc = this.rpc = new Rpc({logger: rpcLogger});
|
||||
rpc.setSendMessage((mssg: any) => rpc.receiveMessage(mssg));
|
||||
|
||||
this._renderTargets.set("fullscreen", renderFullScreen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance for the implementation, this implementation is specific to node environment.
|
||||
*/
|
||||
public getStub<Iface>(implementation: Implementation, checker: Checker): Iface {
|
||||
const components: any = this.definition.manifest.components;
|
||||
// the component forwarder was registered under the same relative path that was used to declare
|
||||
// it in the manifest
|
||||
const forwardName = components[implementation.component];
|
||||
return this.rpc.getStubForward<Iface>(forwardName, implementation.name, checker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop and clean up all components of this plugin.
|
||||
*/
|
||||
public async shutdown(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.safeBrowser && this.safeBrowser.deactivate(),
|
||||
this.safePython && this.safePython.deactivate(),
|
||||
this.unsafeNode && this.unsafeNode.deactivate(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a render target and return its identifier. When a plugin calls `render` with `inline`
|
||||
* mode and this identifier, it will append the safe browser process to `element`.
|
||||
*/
|
||||
public addRenderTarget(renderPluginContent: TargetRenderFunc): number {
|
||||
const id = this._nextRenderTargetId++;
|
||||
this._renderTargets.set(id, renderPluginContent);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the function that render an HTML element based on RenderTarget and RenderOptions.
|
||||
*/
|
||||
public getRenderTarget(target: RenderTarget, options?: RenderOptions): TargetRenderFunc {
|
||||
const targetRenderPluginContent = this._renderTargets.get(target);
|
||||
if (!targetRenderPluginContent) {
|
||||
throw new Error(`Unknown render target ${target}`);
|
||||
}
|
||||
return (containerElement, opts) => targetRenderPluginContent(containerElement, opts || options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the render target.
|
||||
*/
|
||||
public removeRenderTarget(target: RenderTarget): boolean {
|
||||
return this._renderTargets.delete(target);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders safe browser plugin in fullscreen.
|
||||
*/
|
||||
function renderFullScreen(element: Element) {
|
||||
element.classList.add("plugin_instance_fullscreen");
|
||||
document.body.appendChild(element);
|
||||
}
|
||||
|
||||
// Basically the union of relevant interfaces of console and server log.
|
||||
export interface BaseLogger {
|
||||
log?(message: string, ...args: any[]): void;
|
||||
debug?(message: string, ...args: any[]): void;
|
||||
warn?(message: string, ...args: any[]): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create IRpcLogger which logs to console or server log with the given prefix. Specifically will
|
||||
* warn using baseLog.warn, and log info using baseLog.debug or baseLog.log, as available.
|
||||
*/
|
||||
export function createRpcLogger(baseLog: BaseLogger, prefix: string): IRpcLogger {
|
||||
const info = baseLog.debug || baseLog.log;
|
||||
const warn = baseLog.warn;
|
||||
return {
|
||||
warn: warn && ((msg: string) => warn("%s %s", prefix, msg)),
|
||||
info: info && ((msg: string) => info("%s %s", prefix, msg)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If msec milliseconds pass without receiving a Ready message, print the given message as a
|
||||
* warning.
|
||||
* TODO: I propose making it a method of rpc itself, as rpc.warnIfNotReady(msec, message). Until
|
||||
* we have that, this implements it via an ugly hack.
|
||||
*/
|
||||
export function warnIfNotReady(rpc: Rpc, msec: number, message: string): void {
|
||||
if (!(rpc as any)._logger.warn) { return; }
|
||||
const timer = setTimeout(() => (rpc as any)._logger.warn(message), msec);
|
||||
const origDispatch = (rpc as any)._dispatch;
|
||||
(rpc as any)._dispatch = (msg: IMessage) => {
|
||||
if (msg.mtype === MsgType.Ready) { clearTimeout(timer); }
|
||||
origDispatch.call(rpc, msg);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user