extension-host: implement mobile support

use interfaces to create external components and launchers
fixed typo in readme
This commit is contained in:
DH
2025-09-07 01:26:10 +03:00
parent dd34213f4c
commit dbf29d1b1a
31 changed files with 777 additions and 557 deletions

View File

@@ -15,7 +15,7 @@
- `npm install`
- `npm run build:web`
- `npm run dev:ui`
- `npm run dev:wev:server`
- `npm run dev:web:server`
## Build Guide for Android

10
package-lock.json generated
View File

@@ -42,6 +42,7 @@
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.6",
"react-native-device-info": "^14.0.4",
"react-native-gesture-handler": "~2.24.0",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "^5.4.0",
@@ -14155,6 +14156,15 @@
}
}
},
"node_modules/react-native-device-info": {
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-14.0.4.tgz",
"integrity": "sha512-NX0wMAknSDBeFnEnSFQ8kkAcQrFHrG4Cl0mVjoD+0++iaKrOupiGpBXqs8xR0SeJyPC5zpdPl4h/SaBGly6UxA==",
"license": "MIT",
"peerDependencies": {
"react-native": "*"
}
},
"node_modules/react-native-edge-to-edge": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz",

View File

@@ -43,6 +43,7 @@
"expo-constants": "~17.1.7",
"expo-dev-client": "^5.2.4",
"expo-document-picker": "^13.1.6",
"expo-file-system": "~18.1.11",
"expo-font": "~13.3.2",
"expo-haptics": "~14.1.4",
"expo-image": "~2.4.0",
@@ -60,13 +61,13 @@
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.6",
"react-native-device-info": "^14.0.4",
"react-native-gesture-handler": "~2.24.0",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "^5.4.0",
"react-native-screens": "~4.11.1",
"react-native-web": "^0.20.0",
"react-native-webview": "13.13.5",
"expo-file-system": "~18.1.11"
"react-native-webview": "13.13.5"
},
"devDependencies": {
"@expo/metro-config": "~0.20.0",

View File

@@ -711,17 +711,17 @@ type ${uLabel}Interface = {
${"methods" in iface ? iface.methods && Object.keys(iface.methods).map(method => {
const methodTypeLabel = generateComponentLabelName(component, `${name}-${method}`, true);
if ("params" in (iface.methods as any)[method]) {
return ` ${generateLabelName(method, false)}(caller: Component, request: ${methodTypeLabel}Request): ${methodTypeLabel}Response | Promise<${methodTypeLabel}Response>`;
return ` ${generateLabelName(method, false)}(caller: ComponentRef, request: ${methodTypeLabel}Request): ${methodTypeLabel}Response | Promise<${methodTypeLabel}Response>`;
} else {
return ` ${generateLabelName(method, false)}(caller: Component): ${methodTypeLabel}Response | Promise<${methodTypeLabel}Response>`;
return ` ${generateLabelName(method, false)}(caller: ComponentRef): ${methodTypeLabel}Response | Promise<${methodTypeLabel}Response>`;
}
}).join("\n") : ""}
${"notifications" in iface ? iface.notifications && Object.keys(iface.notifications).map(notification => {
const methodTypeLabel = generateComponentLabelName(component, `${name}-${notification}`, true);
if ("params" in (iface.notifications as any)[notification]) {
return ` ${generateLabelName(notification, false)}(caller: Component, request: ${methodTypeLabel}Request): void | Promise<void>;`
return ` ${generateLabelName(notification, false)}(caller: ComponentRef, request: ${methodTypeLabel}Request): void | Promise<void>;`
} else {
return ` ${generateLabelName(notification, false)}(caller: Component): void | Promise<void>;`;
return ` ${generateLabelName(notification, false)}(caller: ComponentRef): void | Promise<void>;`;
}
}).join("\n") : ""}
};`
@@ -849,34 +849,34 @@ export class ${uLabel}ComponentObject implements ComponentObject {
constructor(public impl: ${uLabel}Interface, public name: string) {}
call(caller: Component, method: string, params: Json | undefined): Promise<Json | void> | Json | void {
call(caller: ComponentRef, method: string, params: Json | undefined): Promise<Json | void> | Json | void {
void caller, params;
switch (method) {
${"methods" in iface ? iface.methods && Object.keys(iface.methods).map(method => {
if ("params" in (iface.methods as any)[method]) {
return ` case "${method}": return this.impl.${generateLabelName(method, false)}(caller, params as any);\n`
} else {
return ` case "${method}": return this.impl.${generateLabelName(method, false)}(caller);\n`
}
}).join("\n") : ""}
if ("params" in (iface.methods as any)[method]) {
return ` case "${method}": return this.impl.${generateLabelName(method, false)}(caller, params as any);\n`
} else {
return ` case "${method}": return this.impl.${generateLabelName(method, false)}(caller);\n`
}
}).join("\n") : ""}
default:
throw createError(ErrorCode.MethodNotFound);
}
}
notify(caller: Component, method: string, params: Json | undefined): void | Promise<void> {
notify(caller: ComponentRef, method: string, params: Json | undefined): void | Promise<void> {
void caller, params;
switch (method) {
${"notifications" in iface ? iface.notifications && Object.keys(iface.notifications).map(notification => {
if ("params" in (iface.notifications as any)[notification]) {
return ` case "${notification}": return this.impl.${generateLabelName(notification, false)}(caller, params as any);\n`
} else {
return ` case "${notification}": return this.impl.${generateLabelName(notification, false)}(caller);\n`
}
}).join("\n") : ""}
if ("params" in (iface.notifications as any)[notification]) {
return ` case "${notification}": return this.impl.${generateLabelName(notification, false)}(caller, params as any);\n`
} else {
return ` case "${notification}": return this.impl.${generateLabelName(notification, false)}(caller);\n`
}
}).join("\n") : ""}
default:
throw createError(ErrorCode.MethodNotFound);
@@ -897,12 +897,12 @@ ${"methods" in iface ? iface.methods && Object.keys(iface.methods).map(method =>
const methodTypeLabel = generateComponentLabelName(component, `${name}-${method}`, true);
if ("params" in (iface.methods as any)[method]) {
return ` async ${generateLabelName(method, false)}(request: ${methodTypeLabel}Request): Promise<${methodTypeLabel}Response> {
return (await ${component == "core" ? "" : "core."}objectCall({ object: this.id, method: "${method}", params: request})).result as any;
return await ${component == "core" ? "" : "core."}objectCall({ object: this.id, method: "${method}", params: request}) as any;
}
`
} else {
return ` async ${generateLabelName(method, false)}(): Promise<${methodTypeLabel}Response> {
return (await ${component == "core" ? "" : "core."}objectCall({ object: this.id, method: "${method}", params: {}})).result as any;
return await ${component == "core" ? "" : "core."}objectCall({ object: this.id, method: "${method}", params: {}}) as any;
}
`
}
@@ -1019,7 +1019,7 @@ export function onAny${uLabel}Created(handler: () => Promise<void> | void) {
`;
}
generateView(component: string, _path: string, name: string) {
generateView(_component: string, _path: string, name: string) {
this.viewBody += `
export function push${name}View(target: Window, params: ${name}Props) {
return target.pushView("${name}", params);
@@ -1045,8 +1045,8 @@ type ComponentObject = {
typeId: string;
name: string;
impl: object;
call(caller: Component, method: string, params: Json | undefined): Promise<Json | void> | Json | void;
notify(caller: Component, method: string, params: Json | undefined): void | Promise<void>;
call(caller: ComponentRef, method: string, params: Json | undefined): Promise<Json | void> | Json | void;
notify(caller: ComponentRef, method: string, params: Json | undefined): void | Promise<void>;
dispose(): void | Promise<void>;
};
@@ -1074,24 +1074,28 @@ class ServerPrivateApiGenerator implements ContributionGenerator {
generateEvent(component: string, event: object, name: string) {
const label = generateComponentLabelName(component, name, true);
if (Object.keys(event).length == 0) {
this.body += `
export function send${label}Event(receiver: Component) {
this.body += `export function on${label}(handler: () => Promise<void> | void) {
return thisComponent().onEvent(thisComponent(), "${name}", handler as any);
}
export function send${label}Event(receiver: ComponentRef) {
return receiver.sendEvent("${name}");
}
export function emit${label}Event() {
return thisComponent().emitEvent("${name}");
}
\n`;
return;
}
this.body += `
export function send${label}Event(receiver: Component, params: ${label}Event) {
`;
} else {
this.body += `export function on${label}(handler: (event: ${label}Event) => Promise<void> | void) {
return thisComponent().onEvent(thisComponent(), "${name}", handler as any);
}
export function send${label}Event(receiver: ComponentRef, params: ${label}Event) {
return receiver.sendEvent("${name}", params);
}
export function emit${label}Event(params: ${label}Event) {
return thisComponent().emitEvent("${name}", params);
}\n`;
}
`;
}
}
generateMethod(component: string, method: object, name: string) {
@@ -1111,7 +1115,7 @@ export function emit${label}Event(params: ${label}Event) {
const label = generateComponentLabelName(component, name, false);
const uLabel = generateComponentLabelName(component, name, true);
this.body += `
export async function call${uLabel}(caller: Component, params: ${uLabel}Request): Promise<${uLabel}Response> {
export async function call${uLabel}(caller: ComponentRef, params: ${uLabel}Request): Promise<${uLabel}Response> {
return impl.${method.handler}(caller, params);
}
@@ -1140,7 +1144,7 @@ export async function ${label}(params: ${uLabel}Request): Promise<${uLabel}Respo
const label = generateComponentLabelName(component, name, false);
const uLabel = generateComponentLabelName(component, name, true);
this.body += `
export async function notify${uLabel}(caller: Component, params: ${uLabel}Request) {
export async function notify${uLabel}(caller: ComponentRef, params: ${uLabel}Request) {
impl.${notification.handler}(caller, params);
}
export async function ${label}(params: ${uLabel}Request) {
@@ -1184,25 +1188,21 @@ export async function create${uLabel}Object<Impl extends ${uLabel}Interface, Par
objects[id] = new ${uLabel}ComponentObject(impl, name);
return impl;
}
`;
if (component != 'core') {
this.interfaceBody += `
export function on${uLabel}Created(handler: (object: ${uLabel}) => Promise<void> | void) {
return core.onObjectCreated((params) => {
return ${component == "core" ? "" : "core."}onObjectCreated((params) => {
if (params.interface == "${component}/${name}") {
handler(new ${uLabel}(params.object));
}
});
}
export function onAny${uLabel}Created(handler: () => Promise<void> | void) {
return core.onObjectCreated((params) => {
return ${component == "core" ? "" : "core."}onObjectCreated((params) => {
if (params.interface == "${component}/${name}") {
handler();
}
});
}
`;
}
}
@@ -1212,6 +1212,7 @@ ${this.callBody.length > 0 || this.notifyBody.length > 0 ? 'import * as impl fro
import { createError } from "$core/Error";
import { thisComponent } from "$/component-info";
import * as core from "$core";
import { isJsonObject } from "$core/Json";
${this.viewBody && "import { Window } from '$core/Window';"}
export { thisComponent } from "$/component-info";
@@ -1221,8 +1222,8 @@ type ComponentObject = {
typeId: string;
name: string;
impl: object;
call(caller: Component, method: string, params: Json | undefined): Promise<Json | void> | Json | void;
notify(caller: Component, method: string, params: Json | undefined): void | Promise<void>;
call(caller: ComponentRef, method: string, params: Json | undefined): Promise<Json | void> | Json | void;
notify(caller: ComponentRef, method: string, params: Json | undefined): void | Promise<void>;
dispose(): void | Promise<void>;
};
@@ -1244,13 +1245,13 @@ export function ownObjects() {
return Object.values(objects);
}
export async function call(caller: Component, method: string, params: Json | undefined): Promise<Json | void> {
export async function call(caller: ComponentRef, method: string, params: Json | undefined): Promise<Json | void> {
void caller, params;
switch (method) {
${this.callBody}
case "$/object/call":
if (params && typeof params.method == "string" && typeof params.object == 'number') {
if (params && isJsonObject(params) && typeof params.method == "string" && typeof params.object == 'number') {
if (params.object in objects) {
return objects[params.object].call(caller, params.method,
"params" in params ? params.params as any : undefined
@@ -1267,13 +1268,13 @@ ${this.callBody}
}
}
export async function notify(caller: Component, method: string, params: Json | undefined) {
export async function notify(caller: ComponentRef, method: string, params: Json | undefined) {
void caller, params;
switch (method) {
${this.notifyBody}
case "$/object/notify":
if (params && typeof params.notification == "string" && typeof params.object == 'number') {
if (params && isJsonObject(params) && typeof params.notification == "string" && typeof params.object == 'number') {
if (params.object in objects) {
return objects[params.object].notify(caller, params.notification,
"params" in params ? params.params as any : undefined
@@ -1801,11 +1802,11 @@ class ${componentLabel}ComponentImpl implements IComponentImpl {
}
}
async call(caller: Component, method: string, params?: Json) {
async call(caller: ComponentRef, method: string, params?: Json) {
return await api.call(caller, method, params);
}
async notify(caller: Component, notification: string, params?: Json) {
async notify(caller: ComponentRef, notification: string, params?: Json) {
await api.notify(caller, notification, params);
}
};

View File

@@ -308,43 +308,6 @@
}
}
},
"extension/load": {
"handler": "loadExtension",
"params": {
"id": {
"type": "string"
}
}
},
"extension/unload": {
"handler": "unloadExtension",
"params": {
"id": {
"type": "string"
}
}
},
"extension/install": {
"handler": "installExtension",
"params": {
"path": {
"type": "string"
}
},
"returns": {
"id": {
"type": "string"
}
}
},
"extension/remove": {
"handler": "removeExtension",
"params": {
"id": {
"type": "string"
}
}
},
"settings/set": {
"handler": "handleSettingsSet",
"params": {
@@ -452,11 +415,22 @@
"type": "json"
}
},
"returns": {
"result": {
"returns": "json"
},
"component-call": {
"handler": "handleCall",
"params": {
"caller": {
"type": "string"
},
"method": {
"type": "string"
},
"params": {
"type": "json"
}
}
},
"returns": "json"
}
},
"notifications": {
@@ -481,6 +455,20 @@
"type": "json"
}
}
},
"component-notify": {
"handler": "handleNotify",
"params": {
"caller": {
"type": "string"
},
"notification": {
"type": "string"
},
"params": {
"type": "json"
}
}
}
},
"events": {
@@ -504,14 +492,101 @@
"interfaces": {
"external-component": {
"methods": {
"call": {
"params": "json",
"object-call": {
"params": {
"object": {
"type": "number"
},
"method": {
"type": "string"
},
"params": {
"type": "json"
}
},
"returns": "json"
},
"call": {
"params": {
"method": {
"type": "string"
},
"params": {
"type": "json"
}
},
"returns": "json"
},
"initialize": {
},
"activate": {
"params": {
"settings": {
"type": "json"
}
}
},
"deactivate": {},
"dispose": {},
"get-pid": {
"returns": "number"
}
},
"notifications": {
"notify": {
"params": "json"
"params": {
"method": {
"type": "string"
},
"params": {
"type": "json"
}
}
},
"object-notify": {
"params": {
"object": {
"type": "number"
},
"method": {
"type": "string"
},
"params": {
"type": "json"
}
}
},
"object-destroy": {
"params": {
"object": {
"type": "number"
},
"interface-name": {
"type": "string"
}
}
}
}
},
"launcher": {
"methods": {
"launch": {
"params": {
"path": {
"type": "string"
},
"args": {
"type": "array",
"item-type": "string"
},
"manifest": {
"type": "extension-info"
},
"launcher-params": {
"type": "json-object"
}
},
"returns": "number"
}
}
}

View File

@@ -17,7 +17,7 @@ declare global {
export type ComponentId = string;
export type Component = {
export type ComponentRef = {
getId(): ComponentId;
onClose(listener: () => void | Promise<void>): IDisposable;
sendEvent(event: string, params?: any): void;

View File

@@ -0,0 +1,11 @@
import { Target } from "./Target";
import { Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';
export function getDeviceArchitecture() {
const abis = DeviceInfo.supportedAbisSync();
const abi = (abis && abis.length > 0) ? abis[0] : 'unknown';
return abi.includes("arm64") || abi.includes("aarch64") ? "aarch64" : "x64";
}
export const NativeTarget = new Target("elf", getDeviceArchitecture(), Platform.OS);

View File

@@ -0,0 +1,3 @@
import { Target } from "./Target";
export const NativeTarget = new Target(process.platform === "win32" ? "pe" : "elf", process.arch, process.platform);

View File

@@ -14,13 +14,8 @@ export class Target {
return new Target(parts[0], parts[1], parts[2]);
}
static native() {
return nativeTarget;
}
format(): string {
return this.fileFormat + "-" + this.arch + "-" + this.platform;
}
}
const nativeTarget = new Target("none", "none", "none");

View File

@@ -8,7 +8,7 @@ export function onComponentActivation(component: ComponentInstance) {
const impl = component.getImpl();
const createRendererComponent = (webContents: Electron.WebContents) => {
const rendererComponent: Component = {
const rendererComponent: ComponentRef = {
getId: () => ":renderer",
onClose: (listener) => {
const wrapped = async () => {

View File

@@ -20,8 +20,11 @@ export type IComponentImpl = IDisposable & {
initialize(eventEmitter: (event: string, params: Json) => void): void | Promise<void>;
activate(context: ComponentContext, settings: Json, signal?: AbortSignal): void | Promise<void>;
deactivate(context: ComponentContext): void | Promise<void>;
call?(caller: Component, method: string, params: Json | undefined): Promise<Json | void>;
notify?(caller: Component, notification: string, params: Json | undefined): Promise<void>;
call?(caller: ComponentRef, method: string, params: Json | undefined): Promise<Json | void>;
notify?(caller: ComponentRef, notification: string, params: Json | undefined): Promise<void>;
objectCall?(caller: ComponentRef, object: number, method: string, params: Json | undefined): Promise<Json | void>;
objectNotify?(caller: ComponentRef, object: number, notification: string, params: Json | undefined): Promise<void>;
objectDestroy?(caller: ComponentRef, object: number, interfaceName: string): Promise<void>;
getPid?(): number;
}
@@ -50,7 +53,7 @@ export class ComponentInstance implements ComponentContext {
this.externalEventEmitter[`${sender.getId()}/${event}`]?.emit(params);
}
private createCallerView(caller: ComponentInstance): Component {
private createCallerView(caller: ComponentInstance): ComponentRef {
return {
getId: () => caller.getId(),
onClose: (listener) => caller.onEvent(this, deactivateEvent, listener),
@@ -219,6 +222,10 @@ export class ComponentInstance implements ComponentContext {
throw createError(ErrorCode.InvalidParams, `${caller.getId()}: component ${this.getName()} has no interface method support`);
}
if (this.impl.objectCall) {
return this.impl.objectCall(this.createCallerView(caller), objectId, method, params);
}
return await this.impl.call(this.createCallerView(caller), `$/object/call`, {
object: objectId,
method,
@@ -235,6 +242,10 @@ export class ComponentInstance implements ComponentContext {
throw createError(ErrorCode.InvalidParams, `${caller.getId()}: component ${this.getName()} has no interface support`);
}
if (this.impl.objectNotify) {
return this.impl.objectNotify(this.createCallerView(caller), objectId, notification, params);
}
return await this.impl.notify(this.createCallerView(caller), `$/object/notify`, {
object: objectId,
notification,
@@ -251,6 +262,10 @@ export class ComponentInstance implements ComponentContext {
throw createError(ErrorCode.InvalidParams, `${caller.getId()}: component ${this.getName()} has no interface support`);
}
if (this.impl.objectDestroy) {
return this.impl.objectDestroy(this.createCallerView(caller), objectId, interfaceName);
}
return await this.impl.notify(this.createCallerView(caller), `$/object/destroy`, {
objectId,
interface: interfaceName

View File

@@ -1,52 +0,0 @@
// FIXME: remove this import
import type { Readable, Writable } from 'stream';
import { Target } from "./Target";
export type Process = {
stdin: Writable;
stdout: Readable;
stderr: Readable;
kill: (signal: number | NodeJS.Signals) => void;
on: (event: 'close' | 'exit', listener: (...args: any[]) => void) => void;
once: (event: 'close' | 'exit', listener: (...args: any[]) => void) => void;
off: (event: 'close' | 'exit', listener: (...args: any[]) => void) => void;
getPid(): number;
}
export type LaunchParams = {
launcherRequirements: object;
signal?: AbortSignal;
}
export type Launcher = {
launch: (path: string, args: string[], params: LaunchParams) => Promise<Process> | Process;
}
const launcherStorage: {
[key: string]: Launcher
} = {};
export function addLauncher(target: Target, launcher: Launcher) {
launcherStorage[target.format()] = launcher;
}
export function deleteLauncher(target: Target) {
delete launcherStorage[target.format()];
}
export function getLauncher(target: Target | string) {
if (target instanceof Target) {
target = target.format();
}
if (target in launcherStorage) {
return launcherStorage[target];
}
return undefined;
}
export function getLauncherList() {
return Object.keys(launcherStorage);
}

View File

@@ -1,5 +1,5 @@
import { createError } from "lib/Error";
import { ComponentInstance, findComponentById } from "./ComponentInstance";
import { ComponentInstance, findComponentById, registerComponent, unregisterComponent } from "./ComponentInstance";
import * as self from '$';
let nextObjectId = 0;
@@ -22,19 +22,21 @@ export function unregisterInterface(component: ComponentInstance, interfaceName:
iface.forEach(async objectId => {
const instance = objects[objectId];
delete objects[objectId];
if (instance) {
delete objects[objectId];
const component = findComponentById(instance.owner);
const component = findComponentById(instance.owner);
try {
await component?.objectDestroy(component, instance.interfaceName, objectId);
} catch {}
try {
await component?.objectDestroy(component, instance.interfaceName, objectId);
} catch { }
}
});
delete interfaceObjects[id];
}
export function createObject(caller: Component, objectName: string, interfaceName: string) {
export async function createObject(caller: ComponentRef, objectName: string, interfaceName: string) {
if (!(interfaceName in interfaceObjects)) {
throw createError(ErrorCode.InvalidParams, `Unknown interface ${interfaceName}`);
}
@@ -49,10 +51,46 @@ export function createObject(caller: Component, objectName: string, interfaceNam
interfaceObjects[interfaceName].add(objectId);
caller.onClose(() => destroyObject(caller, objectId));
self.emitObjectCreatedEvent({ interface: interfaceName, object: objectId });
if (interfaceName == "core/external-component") {
const externalComponent = self.toExternalComponent(objectId);
const pid = 0; // FIXME: await externalComponent.getPid();
registerComponent({ name: objectName, version: "0.0.1" }, {
initialize: (): void | Promise<void> => {
return externalComponent.initialize();
},
activate: (_context: ComponentContext, settings: Json, _signal?: AbortSignal): void | Promise<void> => {
return externalComponent.activate({ settings });
},
deactivate: (_context: ComponentContext) => {
return externalComponent.deactivate();
},
call: (_caller: ComponentRef, method: string, params: Json | undefined) => {
return externalComponent.call({ method, params: params ?? null });
},
notify: async (_caller: ComponentRef, notification: string, params: Json | undefined) => {
await externalComponent.notify({ method: notification, params: params ?? null });
},
objectCall: (_caller: ComponentRef, object: number, method: string, params: Json | undefined) => {
return externalComponent.objectCall({ object, method, params: params ?? null });
},
objectNotify: async (_caller: ComponentRef, object: number, notification: string, params: Json | undefined) => {
await externalComponent.objectNotify({ object, method: notification, params: params ?? null });
},
objectDestroy: async (_caller: ComponentRef, object: number, interfaceName: string) => {
await externalComponent.objectDestroy({ object, interfaceName });
},
getPid: () => pid,
dispose: async () => {
await externalComponent.dispose();
},
});
}
return objectId;
}
export function destroyObject(caller: Component, objectId: number) {
export async function destroyObject(caller: ComponentRef, objectId: number) {
const instance = objects[objectId];
if (!instance) {
@@ -63,6 +101,12 @@ export function destroyObject(caller: Component, objectId: number) {
throw createError(ErrorCode.InvalidRequest, `${caller.getId()}: Cannot destroy object '${instance.objectName}' created by component '${instance.owner}'`);
}
if (instance.interfaceName == "core/external-component") {
try {
await unregisterComponent(instance.objectName);
} catch {}
}
delete objects[objectId];
interfaceObjects[instance.interfaceName]?.delete(objectId);
}
@@ -95,9 +139,13 @@ export function getName(objectId: number) {
return objectInstance.objectName;
}
export function call(caller: Component, objectId: number, method: string, params: Json) {
export function call(caller: ComponentRef, objectId: number, method: string, params: Json) {
const instance = objects[objectId];
if (!instance) {
throw createError(ErrorCode.InvalidRequest, `Failed to find instance ${objectId}`);
}
const callerComponent = findComponentById(caller.getId());
if (!callerComponent) {
throw createError(ErrorCode.InvalidRequest, "Cannot find caller component");
@@ -111,9 +159,13 @@ export function call(caller: Component, objectId: number, method: string, params
return component.objectCall(callerComponent, objectId, method, params);
}
export function notify(caller: Component, objectId: number, notification: string, params: Json) {
export function notify(caller: ComponentRef, objectId: number, notification: string, params: Json) {
const instance = objects[objectId];
if (!instance) {
throw createError(ErrorCode.InvalidRequest, `Failed to find instance ${objectId}`);
}
const callerComponent = findComponentById(caller.getId());
if (!callerComponent) {
throw createError(ErrorCode.InvalidRequest, "Cannot find caller component");

View File

@@ -1,26 +0,0 @@
export class Target {
constructor(
public fileFormat: string,
public arch: string,
public platform: string,
) { }
static parse(triple: string) {
const parts = triple.split('-');
if (parts.length != 3) {
return undefined;
}
return new Target(parts[0], parts[1], parts[2]);
}
static native() {
return nativeTarget;
}
format(): string {
return this.fileFormat + "-" + this.arch + "-" + this.platform;
}
}
const nativeTarget = new Target(process.platform === "win32" ? "pe" : "elf", process.arch, process.platform);

View File

@@ -1,37 +0,0 @@
import { createError } from '$/Error';
import { NativeModule, requireNativeModule } from 'expo';
declare class ExtensionLoaderModule extends NativeModule {
loadExtension(path: string): Promise<number>;
unloadExtension(id: number): Promise<void>;
call(extension: number, method: string, params: string): Promise<string>;
notify(extension: number, notification: string, params: string): Promise<void>;
sendResponse(methodId: number, body: string): Promise<void>;
}
const nativeLoader = requireNativeModule<ExtensionLoaderModule>('ExtensionLoader');
const loadedExtensions: Record<string, number> = {};
export async function loadExtension(request: ExtensionLoadRequest): Promise<ExtensionLoadResponse> {
const nativeId = await nativeLoader.loadExtension(request.id);
loadedExtensions[request.id] = nativeId;
}
export async function unloadExtension(request: ExtensionUnloadRequest): Promise<ExtensionUnloadResponse> {
const nativeId = loadedExtensions[request.id];
if (nativeId === undefined) {
throw createError(ErrorCode.InvalidRequest, `extension ${request.id} is not loaded`);
}
await nativeLoader.unloadExtension(nativeId);
}
export async function installExtension(_request: ExtensionInstallRequest): Promise<ExtensionInstallResponse> {
throw createError(ErrorCode.InvalidRequest);
}
export async function removeExtension(_request: ExtensionRemoveRequest): Promise<ExtensionRemoveResponse> {
throw createError(ErrorCode.InvalidRequest);
}

View File

@@ -4,14 +4,11 @@ import { ComponentInstance, findComponentById, getActivatedComponentList, getCom
import * as instance from './ComponentInstance';
import * as settings from './Settings';
import { Schema, SchemaError, SchemaObject, validateObject } from 'lib/Schema';
import * as extensionApi from './extension-api';
import { registerBuiltinLaunchers } from './registerBuiltinLaunchers';
import { initialize } from './initialize';
import * as objects from './Objects';
import { initializeRenderer } from './initialize-renderer';
initialize();
registerBuiltinLaunchers();
export async function activate() {
try {
@@ -73,7 +70,7 @@ export async function deactivate() {
}
}
export async function activateComponent(_caller: Component, request: ComponentActivateRequest): Promise<ComponentActivateResponse> {
export async function activateComponent(_caller: ComponentRef, request: ComponentActivateRequest): Promise<ComponentActivateResponse> {
const component = findComponentById(request.id);
if (!component) {
throw createError(ErrorCode.InvalidParams, `component ${request.id} not found`);
@@ -82,7 +79,7 @@ export async function activateComponent(_caller: Component, request: ComponentAc
await component.activate();
}
export async function deactivateComponent(_caller: Component, request: ComponentDeactivateRequest): Promise<ComponentDeactivateResponse> {
export async function deactivateComponent(_caller: ComponentRef, request: ComponentDeactivateRequest): Promise<ComponentDeactivateResponse> {
const component = findComponentById(request.id);
if (!component) {
throw createError(ErrorCode.InvalidParams, `component ${request.id} not found`);
@@ -91,22 +88,6 @@ export async function deactivateComponent(_caller: Component, request: Component
await component.deactivate();
}
export async function loadExtension(_caller: Component, request: ExtensionLoadRequest): Promise<ExtensionLoadResponse> {
return extensionApi.loadExtension(request);
}
export async function unloadExtension(_caller: Component, request: ExtensionUnloadRequest): Promise<ExtensionUnloadResponse> {
return extensionApi.unloadExtension(request);
}
export async function installExtension(_caller: Component, request: ExtensionInstallRequest): Promise<ExtensionInstallResponse> {
return extensionApi.installExtension(request);
}
export async function removeExtension(_caller: Component, request: ExtensionRemoveRequest): Promise<ExtensionRemoveResponse> {
return extensionApi.removeExtension(request);
}
function getComponentInstanceSettings(instance: ComponentInstance) {
const schema = instance.getContribution("settings");
if (!schema) {
@@ -123,7 +104,7 @@ function getComponentInstanceSettings(instance: ComponentInstance) {
};
}
function getComponentSettings(component: Component) {
function getComponentSettings(component: ComponentRef) {
if (component.getId() == ":renderer") {
// for renderer collect settings for all activated components
@@ -178,7 +159,7 @@ function getObjectMember(object: any, path: string[]) {
}
export async function handleSettingsSet(caller: Component, request: SettingsSetRequest): Promise<SettingsSetResponse> {
export async function handleSettingsSet(caller: ComponentRef, request: SettingsSetRequest): Promise<SettingsSetResponse> {
const path = request.path.split("/");
const name = path.pop();
@@ -212,53 +193,82 @@ export async function handleSettingsSet(caller: Component, request: SettingsSetR
settings.save().catch(e => console.error("failed to save settings", e));
}
export async function handleSettingsGet(caller: Component, request: SettingsGetRequest): Promise<SettingsGetResponse> {
export async function handleSettingsGet(caller: ComponentRef, request: SettingsGetRequest): Promise<SettingsGetResponse> {
const { settings, schema } = getComponentSettings(caller);
const path = request.path.split("/");
return { value: getObjectMember(settings, path), schema: getObjectMember(schema, path) };
}
export async function shutdown(caller: Component, _request: ShutdownRequest): Promise<ShutdownResponse> {
export async function shutdown(caller: ComponentRef, _request: ShutdownRequest): Promise<ShutdownResponse> {
console.warn(`shutdown invoked by ${caller.getId()}`);
await instance.uninitializeComponent(self.thisComponent().getManifest());
}
export async function handleObjectCreate(caller: Component, request: ObjectCreateRequest): Promise<ObjectCreateResponse> {
export async function handleObjectCreate(caller: ComponentRef, request: ObjectCreateRequest): Promise<ObjectCreateResponse> {
return {
object: objects.createObject(caller, request.name, request.interface)
object: await objects.createObject(caller, request.name, request.interface)
};
}
export async function handleObjectDestroy(caller: Component, request: ObjectDestroyRequest): Promise<ObjectDestroyResponse> {
export async function handleObjectDestroy(caller: ComponentRef, request: ObjectDestroyRequest): Promise<ObjectDestroyResponse> {
return objects.destroyObject(caller, request.object)
}
export async function handleFindObject(_caller: Component, request: ObjectFindRequest): Promise<ObjectFindResponse> {
export async function handleFindObject(_caller: ComponentRef, request: ObjectFindRequest): Promise<ObjectFindResponse> {
return {
object: objects.findObject(request.interfaceName, request.objectName)
};
}
export async function handleObjectGetName(_caller: Component, request: ObjectGetNameRequest): Promise<ObjectGetNameResponse> {
export async function handleObjectGetName(_caller: ComponentRef, request: ObjectGetNameRequest): Promise<ObjectGetNameResponse> {
return {
name: objects.getName(request.object)
};
}
export async function handleObjectGetList(_caller: Component, request: ObjectGetListRequest): Promise<ObjectGetListResponse> {
export async function handleObjectGetList(_caller: ComponentRef, request: ObjectGetListRequest): Promise<ObjectGetListResponse> {
return {
objects: objects.getObjectList(request.interface)
};
}
export async function handleObjectCall(caller: Component, request: ObjectCallRequest): Promise<ObjectCallResponse> {
return {
result: await objects.call(caller, request.object, request.method, request.params) ?? {}
};
export async function handleObjectCall(caller: ComponentRef, request: ObjectCallRequest): Promise<ObjectCallResponse> {
return await objects.call(caller, request.object, request.method, request.params) ?? {}
}
export async function handleObjectNotify(caller: Component, request: ObjectNotifyRequest) {
export async function handleObjectNotify(caller: ComponentRef, request: ObjectNotifyRequest) {
return objects.notify(caller, request.object, request.notification, request.params);
}
export async function handleCall(_caller: ComponentRef, request: ComponentCallRequest): Promise<ComponentCallResponse> {
const callerComponent = findComponentById(request.caller);
if (!callerComponent) {
throw createError(ErrorCode.InvalidRequest, `Cannot find caller component ${request.caller}`);
}
const [componentName, ...method] = request.method.split("/");
const component = findComponentById(componentName);
if (!component) {
throw createError(ErrorCode.InvalidRequest, `Cannot find component ${componentName}`);
}
return await component.call(callerComponent, method.join("/"), request.params) ?? {};
}
export async function handleNotify(_caller: ComponentRef, request: ComponentNotifyRequest) {
const callerComponent = findComponentById(request.caller);
if (!callerComponent) {
throw createError(ErrorCode.InvalidRequest, `Cannot find caller component ${request.caller}`);
}
const [componentName, ...notification] = request.notification.split("/");
const component = findComponentById(componentName);
if (!component) {
throw createError(ErrorCode.InvalidRequest, `Cannot find component ${componentName}`);
}
return component.notify(callerComponent, notification.join("/"), request.params);
}

View File

@@ -1,3 +0,0 @@
export function registerBuiltinLaunchers() {}

View File

@@ -1,132 +0,0 @@
import { dirname } from "path";
import { addLauncher, Launcher, LaunchParams, Process } from "./Launcher";
import { Target } from "./Target";
import { fork, spawn } from "child_process";
import { Duplex } from "stream";
import { EventEmitter } from "events";
import { fileURLToPath } from "url";
const nativeLauncher: Launcher = {
launch: async (path: string, args: string[], params: LaunchParams) => {
path = fileURLToPath(path);
const newProcess = spawn(path, args, {
argv0: path,
cwd: dirname(process.execPath),
signal: params.signal,
stdio: 'pipe'
});
newProcess.stdout.setEncoding('utf8');
newProcess.stderr.setEncoding('utf8');
const pid = newProcess.pid ?? 0;
const result: Process = {
stdin: newProcess.stdin,
stdout: newProcess.stdout,
stderr: newProcess.stderr,
kill: (signal: number | NodeJS.Signals) => {
newProcess.kill(signal);
},
on: (event: string, handler: (...args: any[]) => void) => {
newProcess.on(event, handler);
},
once: (event: string, handler: (...args: any[]) => void) => {
newProcess.once(event, handler);
},
off: (event: string, handler: (...args: any[]) => void) => {
newProcess.off(event, handler);
},
getPid: () => pid
};
return result;
}
};
const nodeLauncher: Launcher = {
launch: async (path: string, args: string[], params: LaunchParams) => {
path = fileURLToPath(path);
const newProcess = fork(path, args, {
signal: params.signal,
stdio: 'pipe',
cwd: dirname(path),
});
newProcess.stdout!.setEncoding('utf8');
newProcess.stderr!.setEncoding('utf8');
const pid = newProcess.pid ?? 0;
const result: Process = {
stdin: newProcess.stdin!,
stdout: newProcess.stdout!,
stderr: newProcess.stderr!,
kill: (signal: number | NodeJS.Signals) => {
newProcess.kill(signal);
},
on: (event: string, handler: (...args: any[]) => void) => {
newProcess.on(event, handler);
},
once: (event: string, handler: (...args: any[]) => void) => {
newProcess.once(event, handler);
},
off: (event: string, handler: (...args: any[]) => void) => {
newProcess.off(event, handler);
},
getPid: () => pid
};
return result;
}
};
const inlineLauncher: Launcher = {
launch: async (path: string, args: string[], params: LaunchParams) => {
path = fileURLToPath(path);
const imported = await import(path);
if (!("activate" in imported) || typeof imported.activate != 'function') {
throw new Error(`${path}: invalid inline module`);
}
const eventEmitter = new EventEmitter();
const stdin = new Duplex();
const stdout = new Duplex();
const stderr = new Duplex();
imported.activate(eventEmitter, args, params.launcherRequirements, stdin, stdout, stderr);
const result: Process = {
stdin: stdin,
stdout: stdout,
stderr: stderr,
kill: (_signal: number | NodeJS.Signals) => {
if (("deactivate" in imported) && typeof imported.deactivate == 'function') {
imported.deactivate();
}
},
on: (event: string, handler: (...args: any[]) => void) => {
eventEmitter.on(event, handler);
},
once: (event: string, handler: (...args: any[]) => void) => {
eventEmitter.once(event, handler);
},
off: (event: string, handler: (...args: any[]) => void) => {
eventEmitter.off(event, handler);
},
getPid() {
return 0;
},
};
return result;
}
};
export function registerBuiltinLaunchers() {
addLauncher(Target.native(), nativeLauncher);
addLauncher(new Target("js", "any", "node"), nodeLauncher);
addLauncher(new Target("js", "any", "inline"), inlineLauncher);
}

View File

@@ -8,7 +8,7 @@ import * as core from "$core";
export class ExplorerComponent implements IDisposable {
items: ExplorerItem[] = [];
progressToItem: Record<number, ExplorerItem> = {};
subscriptions: Record<number, Component> = {};
subscriptions: Record<number, ComponentRef> = {};
refreshAbortController = new AbortController();
refreshImmediate: NodeJS.Immediate | undefined = undefined;
describedLocations = new Set<string>();
@@ -218,7 +218,7 @@ export class ExplorerComponent implements IDisposable {
});
}
async get(caller: Component, params: ExplorerGetRequest): Promise<ExplorerGetResponse> {
async get(caller: ComponentRef, params: ExplorerGetRequest): Promise<ExplorerGetResponse> {
// this.refresh();
const progressChannel = params.channel ?? (await progress.progressCreate({

View File

@@ -14,7 +14,7 @@ export function deactivate() {
component = undefined;
}
export async function handleAdd(_caller: Component, params: ExplorerAddRequest) {
export async function handleAdd(_caller: ComponentRef, params: ExplorerAddRequest) {
if (!component) {
throw createError(ErrorCode.InvalidRequest);
}
@@ -22,7 +22,7 @@ export async function handleAdd(_caller: Component, params: ExplorerAddRequest)
return component.add(params);
}
export function handleRemove(_caller: Component, params: ExplorerRemoveRequest) {
export function handleRemove(_caller: ComponentRef, params: ExplorerRemoveRequest) {
if (!component) {
throw createError(ErrorCode.InvalidRequest);
}
@@ -30,7 +30,7 @@ export function handleRemove(_caller: Component, params: ExplorerRemoveRequest)
return component.remove(params);
}
export async function handleGet(caller: Component, params: ExplorerGetRequest): Promise<ExplorerGetResponse> {
export async function handleGet(caller: ComponentRef, params: ExplorerGetRequest): Promise<ExplorerGetResponse> {
if (!component) {
throw createError(ErrorCode.InvalidRequest);
}

View File

@@ -11,5 +11,46 @@
{
"name": "fs"
}
]
],
"contributions": {
"methods": {
"load": {
"handler": "loadExtension",
"params": {
"id": {
"type": "string"
}
}
},
"unload": {
"handler": "unloadExtension",
"params": {
"id": {
"type": "string"
}
}
},
"install": {
"handler": "installExtension",
"params": {
"path": {
"type": "string"
}
},
"returns": {
"id": {
"type": "string"
}
}
},
"remove": {
"handler": "removeExtension",
"params": {
"id": {
"type": "string"
}
}
}
}
}
}

View File

@@ -1,14 +1,16 @@
import * as path from '$/path';
import * as path from '$core/path';
import * as fs from '$fs';
import { findComponent, findComponentById, unregisterComponent } from './ComponentInstance';
import { createError } from 'lib/Error';
import { getLauncher } from './Launcher';
import { Extension } from './Extension';
import * as core from '$core';
import { createError } from '$core/Error';
export async function loadExtension(request: ExtensionLoadRequest): Promise<ExtensionLoadResponse> {
if (findComponentById(request.id)) {
return;
}
export async function loadExtension(request: ExtensionHostLoadRequest): Promise<ExtensionHostLoadResponse> {
try {
const componentObject = await core.findExternalComponentObject(request.id);
if (componentObject) {
return;
}
} catch { }
const localExtensionsPath = path.join(await fs.fsGetBuiltinResourcesLocation(undefined), "extensions");
const extensionManifestLocation = path.join(localExtensionsPath, request.id, "extension.json");
@@ -29,29 +31,29 @@ export async function loadExtension(request: ExtensionLoadRequest): Promise<Exte
}
})();
const launcher = getLauncher(manifest.launcher.type);
const launcher = await core.findLauncherObject(manifest.launcher.type);
if (launcher == null) {
throw createError(ErrorCode.InternalError, `launcher ${manifest.launcher.type} not found`);
}
const process = await (async () => {
try {
return launcher.launch(path.join(localExtensionsPath, request.id, manifest.executable), manifest.args ?? [], {
launcherRequirements: manifest.launcher.requirements ?? {},
});
} catch {
throw createError(ErrorCode.InternalError, `${request.id}: failed to spawn extension process`);
}
})();
new Extension(manifest, process);
try {
await launcher.launch({
path: path.join(localExtensionsPath, request.id, manifest.executable),
args: manifest.args ?? [],
manifest,
launcherParams: manifest.launcher.requirements ?? {}
});
} catch (e) {
throw createError(ErrorCode.InternalError, `${request.id}: failed to spawn extension process: ${e}`);
}
}
export async function unloadExtension(request: ExtensionUnloadRequest): Promise<ExtensionUnloadResponse> {
await unregisterComponent(request.id);
export async function unloadExtension(request: ExtensionHostUnloadRequest): Promise<ExtensionHostUnloadResponse> {
const componentObject = await core.findExternalComponentObject(request.id);
await componentObject.destroy();
}
export async function installExtension(request: ExtensionInstallRequest): Promise<ExtensionInstallResponse> {
export async function installExtension(request: ExtensionHostInstallRequest): Promise<ExtensionHostInstallResponse> {
// FIXME: unpack package
const extensionManifestLocation = path.join(request.path, "extension.json");
@@ -71,15 +73,21 @@ export async function installExtension(request: ExtensionInstallRequest): Promis
}
})();
if (findComponent(manifest.name[0].text, manifest.version)) {
throw createError(ErrorCode.InvalidRequest, `extension ${request.path} already installed`);
}
void manifest;
throw createError(ErrorCode.InternalError, "not implemented");
}
export async function removeExtension(request: ExtensionRemoveRequest): Promise<ExtensionRemoveResponse> {
if (findComponentById(request.id)) {
export async function removeExtension(request: ExtensionHostRemoveRequest): Promise<ExtensionHostRemoveResponse> {
const component = await (async () => {
try {
return await core.findExternalComponentObject(request.id);
} catch {
return undefined;
}
})();
if (component) {
throw createError(ErrorCode.InvalidRequest, `extension ${request.id} in use`);
}

View File

@@ -1,6 +1,7 @@
import * as core from "$core";
import * as fs from '$fs';
import * as path from '$core/path';
import * as extensionApi from './extension-api';
export async function activateLocalExtensions(list: Set<string>) {
try {
@@ -17,7 +18,7 @@ export async function activateLocalExtensions(list: Set<string>) {
}
try {
await core.extensionLoad({ id: entry.name });
await extensionApi.loadExtension({ id: entry.name });
} catch (e) {
console.error(`failed to load local extension ${entry.name}`, e);
continue;
@@ -28,7 +29,7 @@ export async function activateLocalExtensions(list: Set<string>) {
} catch (e) {
console.error(`failed to activate extension ${entry.name}`, e);
try {
await core.extensionUnload({ id: entry.name });
await extensionApi.unloadExtension({ id: entry.name });
} catch (e) {
console.error(`failed to unload extension ${entry.name}`, e);
}

View File

@@ -1,9 +1,12 @@
import { activateLocalExtensions } from './extension-host';
import * as core from "$core";
import * as extensionApi from './extension-api';
import { registerBuiltinLaunchers } from './registerBuiltinLaunchers';
const activatedExtensions = new Set<string>();
export function activate() {
export async function activate() {
await registerBuiltinLaunchers();
return activateLocalExtensions(activatedExtensions);
}
@@ -19,3 +22,18 @@ export async function deactivate() {
}
}
export async function loadExtension(_caller: ComponentRef, request: ExtensionHostLoadRequest): Promise<ExtensionHostLoadResponse> {
return extensionApi.loadExtension(request);
}
export async function unloadExtension(_caller: ComponentRef, request: ExtensionHostUnloadRequest): Promise<ExtensionHostUnloadResponse> {
return extensionApi.unloadExtension(request);
}
export async function installExtension(_caller: ComponentRef, request: ExtensionHostInstallRequest): Promise<ExtensionHostInstallResponse> {
return extensionApi.installExtension(request);
}
export async function removeExtension(_caller: ComponentRef, request: ExtensionHostRemoveRequest): Promise<ExtensionHostRemoveResponse> {
return extensionApi.removeExtension(request);
}

View File

@@ -0,0 +1,86 @@
import { NativeModule, requireNativeModule } from 'expo';
import { NativeTarget } from '$core/NativeTarget';
import * as core from "$core";
import * as self from "$";
declare class ExtensionLoaderModule extends NativeModule {
loadExtension(path: string): Promise<number>;
unloadExtension(id: number): Promise<void>;
call(extension: number, method: string, params: string): Promise<string>;
notify(extension: number, notification: string, params: string): Promise<void>;
sendResponse(methodId: number, body: string): Promise<void>;
}
const nativeLoader = requireNativeModule<ExtensionLoaderModule>('ExtensionLoader');
class NativeProtocol implements ExternalComponentInterface {
constructor(
private objectId: number,
private extensionId: number,
public manifest: ExtensionInfo) {
}
async activate(_caller: ComponentRef, request: ExternalComponentActivateRequest) {
return JSON.parse(await nativeLoader.call(this.extensionId, "$/activate", JSON.stringify(request)));
}
deactivate(_caller: ComponentRef): ExternalComponentDeactivateResponse | Promise<ExternalComponentDeactivateResponse> {
return nativeLoader.notify(this.extensionId, "$/deactivate", "{}");
}
async call(_caller: ComponentRef, request: ExternalComponentCallRequest) {
return JSON.parse(await nativeLoader.call(this.extensionId, request.method, JSON.stringify(request.params)));
}
dispose() {
nativeLoader.unloadExtension(this.extensionId);
}
getPid() {
return 0;
}
async initialize() {
return JSON.parse(await nativeLoader.call(this.extensionId, "$/initialize", "{}"));
}
notify(_caller: ComponentRef, request: ExternalComponentNotifyRequest): void | Promise<void> {
return nativeLoader.notify(this.extensionId, request.method, JSON.stringify(request.params));
}
async objectCall(_caller: ComponentRef, request: ExternalComponentObjectCallRequest) {
return JSON.parse(await nativeLoader.call(this.extensionId, "$/object/activate", JSON.stringify(request)));
}
objectDestroy(caller: ComponentRef, request: ExternalComponentObjectDestroyRequest): void | Promise<void> {
return nativeLoader.notify(this.extensionId, "$/object/destroy", JSON.stringify(request));
}
objectNotify(caller: ComponentRef, request: ExternalComponentObjectNotifyRequest): void | Promise<void> {
return nativeLoader.notify(this.extensionId, "$/object/notify", JSON.stringify(request));
}
getObjectId() {
return this.objectId;
}
}
class InlineLauncher implements LauncherInterface {
async launch(_caller: ComponentRef, request: LauncherLaunchRequest): Promise<LauncherLaunchResponse> {
const extensionId = await nativeLoader.loadExtension(request.path);
const protocol = await core.createExternalComponentObject(request.manifest.name[0].text, NativeProtocol, extensionId, request.manifest);
try {
await protocol.initialize();
return protocol.getObjectId();
} catch (e) {
self.destroyObject(protocol.getObjectId());
nativeLoader.unloadExtension(extensionId);
throw e;
}
}
}
export async function registerBuiltinLaunchers() {
await core.createLauncherObject(NativeTarget.format(), InlineLauncher);
}

View File

@@ -1,8 +1,24 @@
import { Process } from './Launcher';
import { dirname } from "path";
import { Target } from "$core/Target";
import { NativeTarget } from "$core/NativeTarget";
import { fork, spawn } from "child_process";
import { Duplex, Readable, Writable } from "stream";
import { EventEmitter } from "events";
import { fileURLToPath } from "url";
import * as self from "$";
import * as core from "$core";
import packageJson from '../../../../package.json' with { type: "json" };
import { findComponent, getComponentId, registerComponent, uninitializeComponent, IComponentImpl } from './ComponentInstance';
import { createError } from 'lib/Error';
type Process = {
stdin: Writable;
stdout: Readable;
stderr: Readable;
kill: (signal: number | NodeJS.Signals) => void;
on: (event: 'close' | 'exit', listener: (...args: any[]) => void) => void;
once: (event: 'close' | 'exit', listener: (...args: any[]) => void) => void;
off: (event: 'close' | 'exit', listener: (...args: any[]) => void) => void;
getPid(): number;
}
type ResponseValue = void | object | null | number | string | boolean | [];
type ResponseError = {
@@ -13,14 +29,13 @@ type ResponseError = {
type Response = ResponseValue | ResponseError | void;
type ErrorHandler = (error: ResponseError) => void;
const clientInfo: ClientInfo = Object.freeze({
name: packageJson.name,
version: packageJson.version,
capabilities: {}
});
export class Extension implements IComponentImpl {
class JsonRpcProtocol implements ExternalComponentInterface {
private alive = true;
private expectedResponses: {
[key: number]: {
@@ -39,23 +54,25 @@ export class Extension implements IComponentImpl {
private componentManifest: ComponentManifest;
constructor(
public readonly manifest: ExtensionInfo,
public readonly extensionProcess: Process) {
private objectId: number,
public readonly extensionProcess: Process,
public manifest: ExtensionInfo) {
this.componentManifest = {
name: manifest.name[0].text,
version: manifest.version,
};
extensionProcess.stdout.on('data', (message: string) => {
this.receive(message);
});
extensionProcess.stderr.on('data', (message: string) => {
this.debugLog(message);
});
this.debugLog("Starting");
this.componentManifest = {
...this.manifest,
name: this.manifest.name[0].text
};
extensionProcess.on('exit', () => {
this.debugLog("Exit");
this.alive = false;
@@ -65,10 +82,8 @@ export class Extension implements IComponentImpl {
clearTimeout(this.responseWatchdog);
}
uninitializeComponent(this.componentManifest);
// uninitializeComponent(this.componentManifest);
});
registerComponent(this.componentManifest, this);
}
getPid() {
@@ -81,7 +96,7 @@ export class Extension implements IComponentImpl {
if (line.length == 0) {
continue;
}
process.stderr.write(`${date} [${this.manifest.name[0].text}-v${this.manifest.version}] ${line}\n`);
process.stderr.write(`${date} [${this.componentManifest.name}-v${this.componentManifest.version}] ${line}\n`);
}
}
@@ -92,31 +107,14 @@ export class Extension implements IComponentImpl {
const response = await this.callMethod<InitializeResponse>("$/initialize", request);
let isInvalid = false;
if (response.extension.name[0].text != this.manifest.name[0].text) {
this.debugLog(`executable sends unexpected name ${response.extension.name}`);
isInvalid = true;
}
if (response.extension.version != this.manifest.version) {
this.debugLog(`executable sends unexpected version ${response.extension.version}`);
isInvalid = true;
}
// FIXME: register contributions
if (isInvalid) {
throw createError(ErrorCode.InvalidRequest, "Extension initialize request returns invalid name/version");
}
this.componentManifest = {
...response.extension,
name: response.extension.name[0].text,
};
}
async activate(_context: ComponentContext, settings: JsonObject, signal?: AbortSignal | undefined) {
const request: ActivateRequest = {
settings
};
return await this.callMethod<ActivateResponse>("$/activate", request, signal);
async activate(_caller: ComponentRef, request: ExternalComponentActivateRequest) {
return await this.callMethod<ActivateResponse>("$/activate", request);
}
async deactivate() {
@@ -184,6 +182,18 @@ export class Extension implements IComponentImpl {
}
}
objectCall(_caller: ComponentRef, request: ExternalComponentObjectCallRequest): ExternalComponentObjectCallResponse | Promise<ExternalComponentObjectCallResponse> {
return this.callMethod("$/object/call", request);
}
objectDestroy(_caller: ComponentRef, request: ExternalComponentObjectDestroyRequest): void | Promise<void> {
return this.sendNotify("$/object/destroy", request);
}
objectNotify(_caller: ComponentRef, request: ExternalComponentObjectNotifyRequest): void | Promise<void> {
return this.sendNotify("$/object/notify", request);
}
async callMethod<R extends Response = Response>(method: string, params: object | [] | string | number | boolean | null = null, signal?: AbortSignal) {
const id = this.nextMessageId++;
const abortHandler = () => this.cancel({ id });
@@ -221,16 +231,16 @@ export class Extension implements IComponentImpl {
this.send({ jsonrpc: "2.0", notification, params });
}
async call(_caller: Component, method: string, params?: Json): Promise<Json | void> {
return this.callMethod(method, params);
call(_caller: ComponentRef, params: ExternalComponentCallRequest): Promise<ExternalComponentCallResponse> {
return this.callMethod(params.method, params.params);
}
async notify(_caller: Component, notification: string, params: Json | undefined) {
this.sendNotify(notification, params);
notify(_caller: ComponentRef, params: ExternalComponentNotifyRequest) {
return this.sendNotify(params.method, params.params);
}
async cancel(request: CancelRequest) {
await this.sendNotify("$/cancel", request);
cancel(request: CancelRequest) {
return this.sendNotify("$/cancel", request);
}
onError(handler: ErrorHandler) {
@@ -344,36 +354,20 @@ export class Extension implements IComponentImpl {
}
if ("method" in message) {
const componentMethod = message["method"] as string;
const params = "params" in message ? message["params"] as JsonObject : undefined;
const [componentName, ...method] = componentMethod.split("/");
const component = findComponent(componentName);
const self = findComponent(this.componentManifest.name);
if (!component || !self) {
this.send({
jsonrpc: "2.0", id, error: {
code: ErrorCode.MethodNotFound,
message: componentMethod
}
});
return;
}
const method = message["method"] as string;
const params = "params" in message ? message["params"] as JsonObject : null;
if (id !== null) {
try {
const result = await component.call(self, method.join("/"), params);
const result = await core.componentCall({ caller: this.manifest.name[0].text, method, params });
this.send({ jsonrpc: "2.0", id, result });
} catch (error) {
this.send({ jsonrpc: "2.0", id, error });
}
} else {
try {
component.notify(self, method.join("/"), params);
} catch (error) {
core.componentNotify({ caller: this.manifest.name[0].text, notification: method, params }).catch(error => {
this.send({ jsonrpc: "2.0", error });
}
});
}
return;
@@ -400,7 +394,7 @@ export class Extension implements IComponentImpl {
if (requestDeadline <= now) {
delete this.expectedResponses[request];
this.debugLog(`wait for response timed out (request ${request})`);
this.cancel({ id: parseInt(request) });
this.cancel({ id: parseInt(request) }).catch(e => console.warn(`cancellation of request ${request} failed`, e));
response.reject({ code: ErrorCode.TimedOut });
} else if (nextDeadline < 0 || nextDeadline > requestDeadline) {
nextDeadline = requestDeadline;
@@ -419,8 +413,157 @@ export class Extension implements IComponentImpl {
}
}
getId() {
return getComponentId(this.componentManifest);
getObjectId() {
return this.objectId;
}
}
class NativeLauncher implements LauncherInterface {
async launch(_caller: ComponentRef, request: LauncherLaunchRequest): Promise<LauncherLaunchResponse> {
const path = fileURLToPath(request.path);
const newProcess = spawn(path, request.args, {
argv0: path,
cwd: dirname(process.execPath),
stdio: 'pipe'
});
newProcess.stdout.setEncoding('utf8');
newProcess.stderr.setEncoding('utf8');
const pid = newProcess.pid ?? 0;
const wrappedNewProcess: Process = {
stdin: newProcess.stdin,
stdout: newProcess.stdout,
stderr: newProcess.stderr,
kill: (signal: number | NodeJS.Signals) => {
newProcess.kill(signal);
},
on: (event: string, handler: (...args: any[]) => void) => {
newProcess.on(event, handler);
},
once: (event: string, handler: (...args: any[]) => void) => {
newProcess.once(event, handler);
},
off: (event: string, handler: (...args: any[]) => void) => {
newProcess.off(event, handler);
},
getPid: () => pid
};
const protocol = await core.createExternalComponentObject(request.manifest.name[0].text, JsonRpcProtocol, wrappedNewProcess, request.manifest);
try {
await protocol.initialize();
return protocol.getObjectId();
} catch (e) {
self.destroyObject(protocol.getObjectId());
newProcess.kill("SIGKILL");
throw e;
}
}
};
class NodeLauncher implements LauncherInterface {
async launch(_caller: ComponentRef, request: LauncherLaunchRequest): Promise<LauncherLaunchResponse> {
const path = fileURLToPath(request.path);
const newProcess = fork(path, request.args, {
stdio: 'pipe',
cwd: dirname(path),
});
newProcess.stdout!.setEncoding('utf8');
newProcess.stderr!.setEncoding('utf8');
const pid = newProcess.pid ?? 0;
const wrappedNewProcess: Process = {
stdin: newProcess.stdin!,
stdout: newProcess.stdout!,
stderr: newProcess.stderr!,
kill: (signal: number | NodeJS.Signals) => {
newProcess.kill(signal);
},
on: (event: string, handler: (...args: any[]) => void) => {
newProcess.on(event, handler);
},
once: (event: string, handler: (...args: any[]) => void) => {
newProcess.once(event, handler);
},
off: (event: string, handler: (...args: any[]) => void) => {
newProcess.off(event, handler);
},
getPid: () => pid
};
const protocol = await core.createExternalComponentObject(request.manifest.name[0].text, JsonRpcProtocol, wrappedNewProcess, request.manifest);
try {
await protocol.initialize();
return protocol.getObjectId();
} catch (e) {
self.destroyObject(protocol.getObjectId());
newProcess.kill("SIGKILL");
throw e;
}
}
};
class InlineLauncher implements LauncherInterface {
async launch(_caller: ComponentRef, request: LauncherLaunchRequest): Promise<LauncherLaunchResponse> {
const path = fileURLToPath(request.path);
const imported = await import(path);
if (!("activate" in imported) || typeof imported.activate != 'function') {
throw new Error(`${path}: invalid inline module`);
}
const eventEmitter = new EventEmitter();
const stdin = new Duplex();
const stdout = new Duplex();
const stderr = new Duplex();
imported.activate(eventEmitter, request.args, request.launcherParams, stdin, stdout, stderr);
const wrappedProcess: Process = {
stdin: stdin,
stdout: stdout,
stderr: stderr,
kill: (_signal: number | NodeJS.Signals) => {
if (("deactivate" in imported) && typeof imported.deactivate == 'function') {
imported.deactivate();
}
},
on: (event: string, handler: (...args: any[]) => void) => {
eventEmitter.on(event, handler);
},
once: (event: string, handler: (...args: any[]) => void) => {
eventEmitter.once(event, handler);
},
off: (event: string, handler: (...args: any[]) => void) => {
eventEmitter.off(event, handler);
},
getPid() {
return 0;
},
};
const protocol = await core.createExternalComponentObject(request.manifest.name[0].text, JsonRpcProtocol, wrappedProcess, request.manifest);
try {
await protocol.initialize();
return protocol.getObjectId();
} catch (e) {
self.destroyObject(protocol.getObjectId());
throw e;
}
}
};
export async function registerBuiltinLaunchers() {
await core.createLauncherObject(NativeTarget.format(), NativeLauncher);
await core.createLauncherObject(new Target("js", "any", "node").format(), NodeLauncher);
await core.createLauncherObject(new Target("js", "any", "inline").format(), InlineLauncher);
}

View File

@@ -8,14 +8,14 @@ import * as core from '$core';
class NativeFile implements FileInterface {
constructor(private id: number, private handle: FileHandle) { }
close(_caller: Component) {
close(_caller: ComponentRef) {
core.objectDestroy({
object: this.id
});
this.handle.close();
}
read(_caller: Component, request: FsFileReadRequest): FsFileReadResponse {
read(_caller: ComponentRef, request: FsFileReadRequest): FsFileReadResponse {
this.handle.offset = request.offset;
const result = this.handle.readBytes(request.size);
@@ -24,7 +24,7 @@ class NativeFile implements FileInterface {
};
}
write(_caller: Component, request: FsFileWriteRequest): FsFileWriteResponse {
write(_caller: ComponentRef, request: FsFileWriteRequest): FsFileWriteResponse {
this.handle.offset = request.offset;
this.handle.writeBytes(Uint8Array.from(request.data));
return request.data.length;
@@ -36,7 +36,7 @@ class NativeFile implements FileInterface {
}
class NativeFileSystem implements FileSystemInterface {
async open(_caller: Component, request: FsFileSystemOpenRequest): Promise<FsFileSystemOpenResponse> {
async open(_caller: ComponentRef, request: FsFileSystemOpenRequest): Promise<FsFileSystemOpenResponse> {
try {
const descriptor = new File(request.uri).open();
const object = await self.createFileObject(request.uri, NativeFile, descriptor);
@@ -46,7 +46,7 @@ class NativeFileSystem implements FileSystemInterface {
}
}
async readToString(_caller: Component, request: FsFileSystemReadToStringRequest): Promise<FsFileSystemReadToStringResponse> {
async readToString(_caller: ComponentRef, request: FsFileSystemReadToStringRequest): Promise<FsFileSystemReadToStringResponse> {
try {
return new File(request.uri).text();
} catch (e) {
@@ -54,7 +54,7 @@ class NativeFileSystem implements FileSystemInterface {
}
}
async writeString(_caller: Component, request: FsFileSystemWriteStringRequest) {
async writeString(_caller: ComponentRef, request: FsFileSystemWriteStringRequest) {
try {
return new File(request.uri).write(request.string);
} catch (e) {
@@ -62,7 +62,7 @@ class NativeFileSystem implements FileSystemInterface {
}
}
async readDir(_caller: Component, request: FsFileSystemReadDirRequest): Promise<FsFileSystemReadDirResponse> {
async readDir(_caller: ComponentRef, request: FsFileSystemReadDirRequest): Promise<FsFileSystemReadDirResponse> {
try {
const result = new Directory(request).list();
@@ -81,7 +81,7 @@ class NativeFileSystem implements FileSystemInterface {
}
}
async stat(_caller: Component, request: FsFileSystemStatRequest): Promise<FsFileSystemStatResponse> {
async stat(_caller: ComponentRef, request: FsFileSystemStatRequest): Promise<FsFileSystemStatResponse> {
try {
const result = new File(request);
@@ -103,50 +103,50 @@ export async function uninitialize() {
await Promise.all(self.ownObjects().map(object => object.dispose()));
}
export async function open(_caller: Component, request: FsOpenRequest): Promise<FsOpenResponse> {
export async function open(_caller: ComponentRef, request: FsOpenRequest): Promise<FsOpenResponse> {
const protocol = new URL(request.uri).protocol || "file:";
const object = await self.findFileSystemObject(protocol);
return await object.open(request);
}
export async function readToString(_caller: Component, request: FsReadToStringRequest): Promise<FsReadToStringResponse> {
export async function readToString(_caller: ComponentRef, request: FsReadToStringRequest): Promise<FsReadToStringResponse> {
const protocol = new URL(request.uri).protocol || "file:";
const object = await self.findFileSystemObject(protocol);
return await object.readToString(request);
}
export async function writeString(_caller: Component, request: FsWriteStringRequest): Promise<FsWriteStringResponse> {
export async function writeString(_caller: ComponentRef, request: FsWriteStringRequest): Promise<FsWriteStringResponse> {
const protocol = new URL(request.uri).protocol || "file:";
const object = await self.findFileSystemObject(protocol);
return await object.writeString(request);
}
export async function readDir(_caller: Component, request: FsReadDirRequest): Promise<FsReadDirResponse> {
export async function readDir(_caller: ComponentRef, request: FsReadDirRequest): Promise<FsReadDirResponse> {
const protocol = new URL(request).protocol || "file:";
const object = await self.findFileSystemObject(protocol);
return await object.readDir(request);
}
export async function stat(_caller: Component, request: FsStatRequest): Promise<FsStatResponse> {
export async function stat(_caller: ComponentRef, request: FsStatRequest): Promise<FsStatResponse> {
const protocol = new URL(request).protocol || "file:";
const object = await self.findFileSystemObject(protocol);
return await object.stat(request);
}
export function getBuiltinResourcesLocation(_caller: Component, _request: FsGetBuiltinResourcesLocationRequest): FsGetBuiltinResourcesLocationResponse {
export function getBuiltinResourcesLocation(_caller: ComponentRef, _request: FsGetBuiltinResourcesLocationRequest): FsGetBuiltinResourcesLocationResponse {
return Paths.document.uri;
}
export function getConfigLocation(_caller: Component, _request: FsGetConfigLocationRequest): FsGetConfigLocationResponse {
export function getConfigLocation(_caller: ComponentRef, _request: FsGetConfigLocationRequest): FsGetConfigLocationResponse {
return Paths.document.uri;
}
export async function openDirectorySelector(caller: Component, request: FsOpenDirectorySelectorRequest): Promise<FsOpenDirectorySelectorResponse> {
export async function openDirectorySelector(caller: ComponentRef, request: FsOpenDirectorySelectorRequest): Promise<FsOpenDirectorySelectorResponse> {
try {
return (await pickDirectory({
requestLongTermAccess: true

View File

@@ -15,14 +15,14 @@ function parseUri(uri: string) {
class NativeFile implements FileInterface {
constructor(private id: number, private handle: nodeFs.FileHandle) { }
async close(_caller: Component) {
async close(_caller: ComponentRef) {
core.objectDestroy({
object: this.id
});
await this.handle.close();
}
async read(_caller: Component, request: FsFileReadRequest): Promise<FsFileReadResponse> {
async read(_caller: ComponentRef, request: FsFileReadRequest): Promise<FsFileReadResponse> {
const buffer = new Uint8Array(request.size);
const result = await this.handle.read(buffer, 0, request.size, request.offset);
@@ -31,7 +31,7 @@ class NativeFile implements FileInterface {
};
}
async write(_caller: Component, request: FsFileWriteRequest): Promise<FsFileWriteResponse> {
async write(_caller: ComponentRef, request: FsFileWriteRequest): Promise<FsFileWriteResponse> {
const result = await this.handle.write(Uint8Array.from(request.data), {
position: request.offset
});
@@ -82,7 +82,7 @@ function toFileType(fileType: WithFileType) {
}
class NativeFileSystem implements FileSystemInterface {
async open(_caller: Component, request: FsFileSystemOpenRequest): Promise<FsFileSystemOpenResponse> {
async open(_caller: ComponentRef, request: FsFileSystemOpenRequest): Promise<FsFileSystemOpenResponse> {
const filePath = parseUri(request.uri).pathname;
try {
@@ -94,7 +94,7 @@ class NativeFileSystem implements FileSystemInterface {
}
}
async readToString(_caller: Component, request: FsFileSystemReadToStringRequest): Promise<FsFileSystemReadToStringResponse> {
async readToString(_caller: ComponentRef, request: FsFileSystemReadToStringRequest): Promise<FsFileSystemReadToStringResponse> {
const filePath = parseUri(request.uri).pathname;
try {
@@ -104,7 +104,7 @@ class NativeFileSystem implements FileSystemInterface {
}
}
async writeString(_caller: Component, request: FsFileSystemWriteStringRequest) {
async writeString(_caller: ComponentRef, request: FsFileSystemWriteStringRequest) {
const filePath = parseUri(request.uri).pathname;
try {
@@ -114,7 +114,7 @@ class NativeFileSystem implements FileSystemInterface {
}
}
async readDir(_caller: Component, request: FsFileSystemReadDirRequest): Promise<FsFileSystemReadDirResponse> {
async readDir(_caller: ComponentRef, request: FsFileSystemReadDirRequest): Promise<FsFileSystemReadDirResponse> {
const path = parseUri(request).pathname;
try {
const result = await nodeFs.readdir(path, { withFileTypes: true });
@@ -134,7 +134,7 @@ class NativeFileSystem implements FileSystemInterface {
}
}
async stat(_caller: Component, request: FsFileSystemStatRequest): Promise<FsFileSystemStatResponse> {
async stat(_caller: ComponentRef, request: FsFileSystemStatRequest): Promise<FsFileSystemStatResponse> {
const path = parseUri(request).pathname;
try {
@@ -158,42 +158,42 @@ export async function uninitialize() {
await Promise.all(self.ownObjects().map(object => object.dispose()));
}
export async function open(_caller: Component, request: FsOpenRequest): Promise<FsOpenResponse> {
export async function open(_caller: ComponentRef, request: FsOpenRequest): Promise<FsOpenResponse> {
const protocol = parseUri(request.uri).protocol || "file:";
const object = await self.findFileSystemObject(protocol);
return await object.open(request);
}
export async function readToString(_caller: Component, request: FsReadToStringRequest): Promise<FsReadToStringResponse> {
export async function readToString(_caller: ComponentRef, request: FsReadToStringRequest): Promise<FsReadToStringResponse> {
const protocol = parseUri(request.uri).protocol || "file:";
const object = await self.findFileSystemObject(protocol);
return await object.readToString(request);
}
export async function writeString(_caller: Component, request: FsWriteStringRequest): Promise<FsWriteStringResponse> {
export async function writeString(_caller: ComponentRef, request: FsWriteStringRequest): Promise<FsWriteStringResponse> {
const protocol = parseUri(request.uri).protocol || "file:";
const object = await self.findFileSystemObject(protocol);
return await object.writeString(request);
}
export async function readDir(_caller: Component, request: FsReadDirRequest): Promise<FsReadDirResponse> {
export async function readDir(_caller: ComponentRef, request: FsReadDirRequest): Promise<FsReadDirResponse> {
const protocol = parseUri(request).protocol || "file:";
const object = await self.findFileSystemObject(protocol);
return await object.readDir(request);
}
export async function stat(_caller: Component, request: FsStatRequest): Promise<FsStatResponse> {
export async function stat(_caller: ComponentRef, request: FsStatRequest): Promise<FsStatResponse> {
const protocol = parseUri(request).protocol || "file:";
const object = await self.findFileSystemObject(protocol);
return await object.stat(request);
}
export function getBuiltinResourcesLocation(_caller: Component, _request: FsGetBuiltinResourcesLocationRequest): FsGetBuiltinResourcesLocationResponse {
export function getBuiltinResourcesLocation(_caller: ComponentRef, _request: FsGetBuiltinResourcesLocationRequest): FsGetBuiltinResourcesLocationResponse {
if (app.isPackaged && "resourcesPath" in process && typeof process.resourcesPath == "string") {
return pathToFileURL(process.resourcesPath).toString();
}
@@ -201,11 +201,11 @@ export function getBuiltinResourcesLocation(_caller: Component, _request: FsGetB
return encodeURI(path.toURI(nodePath.resolve(import.meta.dirname, "..")));
}
export function getConfigLocation(_caller: Component, _request: FsGetConfigLocationRequest): FsGetConfigLocationResponse {
export function getConfigLocation(_caller: ComponentRef, _request: FsGetConfigLocationRequest): FsGetConfigLocationResponse {
return encodeURI(path.toURI(nodePath.dirname(process.execPath)));
}
export async function openDirectorySelector(caller: Component, request: FsOpenDirectorySelectorRequest): Promise<FsOpenDirectorySelectorResponse> {
export async function openDirectorySelector(caller: ComponentRef, request: FsOpenDirectorySelectorRequest): Promise<FsOpenDirectorySelectorResponse> {
const result = await dialog.showOpenDialog({
properties: [
'openDirectory',

View File

@@ -7,33 +7,33 @@ export async function deactivate() {
await fs.uninitialize();
}
export async function handleOpen(caller: Component, request: FsOpenRequest) {
export async function handleOpen(caller: ComponentRef, request: FsOpenRequest) {
return fs.open(caller, request);
}
export async function handleReadToString(caller: Component, request: FsReadToStringRequest) {
export async function handleReadToString(caller: ComponentRef, request: FsReadToStringRequest) {
return fs.readToString(caller, request);
}
export async function handleWriteString(caller: Component, request: FsWriteStringRequest) {
export async function handleWriteString(caller: ComponentRef, request: FsWriteStringRequest) {
return fs.writeString(caller, request);
}
export async function handleReadDir(caller: Component, request: FsReadDirRequest) {
export async function handleReadDir(caller: ComponentRef, request: FsReadDirRequest) {
return fs.readDir(caller, request);
}
export async function handleStat(caller: Component, request: FsStatRequest) {
export async function handleStat(caller: ComponentRef, request: FsStatRequest) {
return fs.stat(caller, request);
}
export function handleGetBuiltinResourcesLocation(caller: Component, request: FsGetBuiltinResourcesLocationRequest) {
export function handleGetBuiltinResourcesLocation(caller: ComponentRef, request: FsGetBuiltinResourcesLocationRequest) {
return fs.getBuiltinResourcesLocation(caller, request);
}
export function handleGetConfigLocation(caller: Component, request: FsGetConfigLocationRequest) {
export function handleGetConfigLocation(caller: ComponentRef, request: FsGetConfigLocationRequest) {
return fs.getConfigLocation(caller, request);
}
export function handleOpenDirectorySelector(caller: Component, request: FsOpenDirectorySelectorRequest) {
export function handleOpenDirectorySelector(caller: ComponentRef, request: FsOpenDirectorySelectorRequest) {
return fs.openDirectorySelector(caller, request);
}

View File

@@ -1,9 +1,9 @@
import * as github from './github';
export async function handleReleasesLatest(_caller: Component, params: GithubReleasesLatestRequest): Promise<GithubReleasesLatestResponse> {
export async function handleReleasesLatest(_caller: ComponentRef, params: GithubReleasesLatestRequest): Promise<GithubReleasesLatestResponse> {
return github.fetchReleasesLatest(params);
}
export async function handleReleases(_caller: Component, params: GithubReleasesRequest): Promise<GithubReleasesResponse> {
export async function handleReleases(_caller: ComponentRef, params: GithubReleasesRequest): Promise<GithubReleasesResponse> {
return github.fetchReleases(params);
}

View File

@@ -4,13 +4,13 @@ import * as api from '$';
import { Disposable, IDisposable } from '$core/Disposable';
type ProgressInstance = Omit<ProgressValue, 'channel'> & {
creator: Component;
creator: ComponentRef;
disposable?: IDisposable;
};
let nextProgressChannel = 0;
let channels: Record<number, ProgressInstance> = {};
let subscriptions: Record<number, Set<Component>> = {};
let subscriptions: Record<number, Set<ComponentRef>> = {};
export function deactivate() {
nextProgressChannel = 0;
@@ -30,7 +30,7 @@ export function deactivate() {
subscriptions = {};
}
export function progressCreate(source: Component, params: ProgressCreateRequest): ProgressCreateResponse {
export function progressCreate(source: ComponentRef, params: ProgressCreateRequest): ProgressCreateResponse {
const channel = nextProgressChannel++;
channels[channel] = {
@@ -58,7 +58,7 @@ export function progressCreate(source: Component, params: ProgressCreateRequest)
return { channel };
}
export async function progressUpdate(caller: Component, params: ProgressUpdateRequest) {
export async function progressUpdate(caller: ComponentRef, params: ProgressUpdateRequest) {
const info = channels[params.channel];
if (!info) {
@@ -108,7 +108,7 @@ export async function progressUpdate(caller: Component, params: ProgressUpdateRe
}
}
export function progressSubscribe(caller: Component, params: ProgressSubscribeRequest) {
export function progressSubscribe(caller: ComponentRef, params: ProgressSubscribeRequest) {
if (!(params.channel in channels)) {
throw createError(ErrorCode.InvalidParams);
}
@@ -120,7 +120,7 @@ export function progressSubscribe(caller: Component, params: ProgressSubscribeRe
api.sendProgressUpdateEvent(caller, { value: { channel: params.channel, ...info } });
}
export function progressUnsubscribe(caller: Component, params: ProgressUnsubscribeRequest) {
export function progressUnsubscribe(caller: ComponentRef, params: ProgressUnsubscribeRequest) {
const channelSubscriptions = subscriptions[params.channel];
if (!channelSubscriptions) {
return;