mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
214 lines
7.6 KiB
TypeScript
214 lines
7.6 KiB
TypeScript
|
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);
|
||
|
};
|
||
|
}
|