@aiot-nexus/plugin-sdk 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/src/cli/run.d.ts +1 -1
- package/dist/src/cli/run.js +94 -0
- package/dist/src/cli.js +10 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/nexus-dev-control.d.ts +42 -0
- package/dist/src/nexus-dev-control.js +401 -0
- package/dist/src/nexus-dev-deployment.d.ts +9 -0
- package/dist/src/nexus-dev-deployment.js +195 -0
- package/dist/src/nexus-dev.d.ts +21 -0
- package/dist/src/nexus-dev.js +569 -0
- package/dist/src/scaffold.js +1 -0
- package/package.json +2 -2
- package/templates/minimal/README.md.tmpl +3 -0
- package/templates/sqlite/README.md.tmpl +3 -0
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ nexus-plugin validate <plugin-package-directory>
|
|
|
22
22
|
nexus-plugin create <plugin-id> --template minimal|sqlite --output <parent-directory>
|
|
23
23
|
nexus-plugin dev [plugin-project-directory]
|
|
24
24
|
nexus-plugin dev:web [plugin-project-directory]
|
|
25
|
+
nexus-plugin dev:nexus [plugin-project-directory] --host <nexus.exe-or-directory>
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
`npm run pack:sdk` 将离线安装包写入 `package/`。该目录是本地构建产物,不进入 Git。`npm run publish:sdk` 发布到官方 npm registry;`prepack` 会强制运行测试、构建并核对实际 npm 文件清单,缺少任一公开入口时拒绝发布。
|
|
@@ -43,7 +44,7 @@ nexus-plugin create my-frame-plugin \
|
|
|
43
44
|
- 目标目录非空时命令以返回码 `2` 拒绝覆盖。生成或安装失败返回 `1`。
|
|
44
45
|
- 两套模板均读取宿主 `PluginRendererContext.language` 和 `themeMode`,通过 `onContextChanged` 在同一 iframe 内响应运行时语言与主题切换,并内置 `zh-CN` / `en-US` renderer 文案;manifest 使用相同文化资源本地化宿主入口。模板页面自身不提供语言或主题选择控件。
|
|
45
46
|
|
|
46
|
-
默认来源从 npm registry 安装。离线开发时先执行 `npm run pack:sdk`,再通过 `--sdk-source /path/to/aiot-nexus-plugin-sdk-1.0.
|
|
47
|
+
默认来源从 npm registry 安装。离线开发时先执行 `npm run pack:sdk`,再通过 `--sdk-source /path/to/aiot-nexus-plugin-sdk-1.1.0.tgz` 指定本地包;CLI 会把本地包一并放入生成工程。
|
|
47
48
|
|
|
48
49
|
## 独立调试
|
|
49
50
|
|
|
@@ -81,3 +82,21 @@ nexus-plugin dev [plugin-project-directory]
|
|
|
81
82
|
调试宿主工具栏中的语言和主题选择器用于模拟 Nexus 主框架 context,不属于插件页面。切换后调试宿主向同一 iframe 重发框架 `1.0.0` 已有的 `init` 消息;SDK 将后续 `init` 作为 context 更新并原地刷新语言和主题,同时仍接受可选的 `context-changed` 消息。调试宿主还复用正式的 `nexus-plugin://` 资源协议、iframe sandbox、CSP、消息结构、operation 校验以及当前 Host API 权限语义,但不包含 Nexus 的完整页面路由、Pinia 状态和应用级功能。交付前仍需把 `dist/<plugin-id>/` 放入正式 Nexus 完成集成验证。
|
|
82
83
|
|
|
83
84
|
浏览器 UI 命令支持 `--no-build`、`--no-open`、`--port <port>`,以及与 Electron 调试宿主相同的 `--language`、`--theme`、`--surface` 和 `--contribution-id` 参数。它使用普通 `http://127.0.0.1` iframe,不模拟 `nexus-plugin://` 协议或 Electron sandbox,因此只作为 UI 开发入口。Renderer-only 输出不会覆盖正式插件制品。
|
|
85
|
+
|
|
86
|
+
## 使用正式 Nexus 调试
|
|
87
|
+
|
|
88
|
+
已经安装并构建插件后,可直接把编译好的正式 Nexus 作为宿主:
|
|
89
|
+
|
|
90
|
+
```powershell
|
|
91
|
+
npm run dev:nexus -- --host D:\prj\tmp\aiot-nexus\dist\electron\nexus-win32-x64\nexus.exe
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`dev:nexus` 首次启动会完成正式构建,通过 staging 原子部署到 `<exeDir>/Plugins/<pluginId>/`,再由 CLI 以 `--plugin-dev`、随机控制管道和会话令牌启动 Nexus。开发通道只在这次由 CLI 管理的进程中启用;普通双击或不带 `--plugin-dev` 启动时不会创建控制管道,也不会打开 DevTools。
|
|
95
|
+
|
|
96
|
+
| 修改范围 | 行为 |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `src/renderer/**` | 只运行 `build:web` 并替换 renderer;宿主 PID 不变,iframe 重新完成 `ready/init`。 |
|
|
99
|
+
| `src/manifest.json` 的展示、贡献点等安全字段 | 构建 renderer、原子替换 manifest 后重扫插件目录;宿主 PID 不变。 |
|
|
100
|
+
| `src/main.ts`、依赖、原生模块或主进程相关 manifest 字段 | 完整构建成功后请求旧宿主退出,替换完整插件包并启动新宿主;宿主 PID 变化。 |
|
|
101
|
+
|
|
102
|
+
更新构建或 manifest 重扫失败时保留上一个可运行部署;新宿主启动失败时回滚插件包并恢复旧版本。`mainEntry` 不通过清理 `require.cache` 热替换,主进程变化始终使用完整重启。默认打开 Renderer DevTools;可传 `--no-devtools`。主进程调试支持 `--inspect[=<port>]` 和 `--inspect-brk[=<port>]`,贡献点选择支持 `--surface` 与 `--contribution-id`。按 `Ctrl+C` 停止 CLI 时,CLI 会关闭其启动的正式宿主。
|
package/dist/src/cli/run.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ export interface CliIo {
|
|
|
3
3
|
stdout(message: string): void;
|
|
4
4
|
stderr(message: string): void;
|
|
5
5
|
}
|
|
6
|
-
export declare function runCli(args: string[], io: CliIo): number
|
|
6
|
+
export declare function runCli(args: string[], io: CliIo): number | Promise<number>;
|
|
7
7
|
export declare function usage(): string;
|
package/dist/src/cli/run.js
CHANGED
|
@@ -29,6 +29,7 @@ const index_1 = require("../index");
|
|
|
29
29
|
const scaffold_1 = require("../scaffold");
|
|
30
30
|
const dev_1 = require("../dev");
|
|
31
31
|
const web_dev_1 = require("../web-dev");
|
|
32
|
+
const nexus_dev_1 = require("../nexus-dev");
|
|
32
33
|
const nodeEngine = ">=16.20.0 <17";
|
|
33
34
|
function runCli(args, io) {
|
|
34
35
|
const command = args[0];
|
|
@@ -70,6 +71,9 @@ function runCli(args, io) {
|
|
|
70
71
|
if (command === "dev:web") {
|
|
71
72
|
return runWebDev(args.slice(1), io);
|
|
72
73
|
}
|
|
74
|
+
if (command === "dev:nexus") {
|
|
75
|
+
return runNexusDev(args.slice(1), io);
|
|
76
|
+
}
|
|
73
77
|
return usageError(io, command ? "Unknown command: " + command : "A command is required.");
|
|
74
78
|
}
|
|
75
79
|
exports.runCli = runCli;
|
|
@@ -85,10 +89,100 @@ function usage() {
|
|
|
85
89
|
" [--contribution-id <id>] [--no-devtools]",
|
|
86
90
|
" nexus-plugin dev:web [plugin-project-directory] [--no-build] [--no-open] [--port <port>]",
|
|
87
91
|
" [--language <culture>] [--theme system|light|dark] [--surface business|tool|settings]",
|
|
92
|
+
" [--contribution-id <id>]",
|
|
93
|
+
" nexus-plugin dev:nexus [plugin-project-directory] --host <nexus.exe-or-directory>",
|
|
94
|
+
" [--inspect[=<port>] | --inspect-brk[=<port>]] [--no-devtools]",
|
|
95
|
+
" [--language <culture>] [--theme system|light|dark] [--surface business|tool|settings]",
|
|
88
96
|
" [--contribution-id <id>]"
|
|
89
97
|
].join("\n");
|
|
90
98
|
}
|
|
91
99
|
exports.usage = usage;
|
|
100
|
+
function runNexusDev(args, io) {
|
|
101
|
+
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
102
|
+
io.stdout(usage());
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
let projectDirectory = io.cwd();
|
|
106
|
+
let index = 0;
|
|
107
|
+
if (args[0] && !args[0].startsWith("--")) {
|
|
108
|
+
projectDirectory = path.resolve(io.cwd(), args[0]);
|
|
109
|
+
index = 1;
|
|
110
|
+
}
|
|
111
|
+
const options = {
|
|
112
|
+
projectDirectory,
|
|
113
|
+
openDevTools: true
|
|
114
|
+
};
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
for (; index < args.length; index += 1) {
|
|
117
|
+
const option = args[index];
|
|
118
|
+
if (option === "--no-devtools") {
|
|
119
|
+
if (seen.has(option)) {
|
|
120
|
+
return usageError(io, "Duplicate option: " + option);
|
|
121
|
+
}
|
|
122
|
+
seen.add(option);
|
|
123
|
+
options.openDevTools = false;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const inspector = parseInspectorOption(option);
|
|
127
|
+
if (inspector) {
|
|
128
|
+
if (options.inspectorMode) {
|
|
129
|
+
return usageError(io, "Only one main-process inspector option can be used.");
|
|
130
|
+
}
|
|
131
|
+
const key = "--" + inspector.mode;
|
|
132
|
+
if (seen.has(key)) {
|
|
133
|
+
return usageError(io, "Duplicate option: " + key);
|
|
134
|
+
}
|
|
135
|
+
seen.add(key);
|
|
136
|
+
options.inspectorMode = inspector.mode;
|
|
137
|
+
options.inspectorPort = inspector.port;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!["--host", "--language", "--theme", "--surface", "--contribution-id"].includes(option)) {
|
|
141
|
+
return usageError(io, "Unknown dev:nexus option: " + option);
|
|
142
|
+
}
|
|
143
|
+
if (seen.has(option)) {
|
|
144
|
+
return usageError(io, "Duplicate option: " + option);
|
|
145
|
+
}
|
|
146
|
+
seen.add(option);
|
|
147
|
+
const value = args[index + 1];
|
|
148
|
+
if (value === undefined || value.startsWith("--")) {
|
|
149
|
+
return usageError(io, option + " requires a value.");
|
|
150
|
+
}
|
|
151
|
+
index += 1;
|
|
152
|
+
if (option === "--host") {
|
|
153
|
+
options.hostPath = path.resolve(io.cwd(), value);
|
|
154
|
+
}
|
|
155
|
+
else if (option === "--language") {
|
|
156
|
+
options.language = value;
|
|
157
|
+
}
|
|
158
|
+
else if (option === "--theme") {
|
|
159
|
+
if (value !== "system" && value !== "light" && value !== "dark") {
|
|
160
|
+
return usageError(io, "theme must be system, light, or dark.");
|
|
161
|
+
}
|
|
162
|
+
options.themeMode = value;
|
|
163
|
+
}
|
|
164
|
+
else if (option === "--surface") {
|
|
165
|
+
if (value !== "business" && value !== "tool" && value !== "settings") {
|
|
166
|
+
return usageError(io, "surface must be business, tool, or settings.");
|
|
167
|
+
}
|
|
168
|
+
options.surface = value;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
options.contributionId = value;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!options.hostPath) {
|
|
175
|
+
return usageError(io, "dev:nexus requires --host <nexus.exe-or-directory>.");
|
|
176
|
+
}
|
|
177
|
+
return (0, nexus_dev_1.runPluginNexusDev)(options, io).catch((error) => {
|
|
178
|
+
if (error instanceof dev_1.PluginDevError) {
|
|
179
|
+
io.stderr(error.message);
|
|
180
|
+
return error.exitCode;
|
|
181
|
+
}
|
|
182
|
+
io.stderr("Packaged Nexus plugin development failed: " + (error instanceof Error ? error.message : String(error)));
|
|
183
|
+
return 1;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
92
186
|
function runWebDev(args, io) {
|
|
93
187
|
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
94
188
|
io.stdout(usage());
|
package/dist/src/cli.js
CHANGED
|
@@ -2,8 +2,17 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const run_1 = require("./cli/run");
|
|
5
|
-
|
|
5
|
+
const result = (0, run_1.runCli)(process.argv.slice(2), {
|
|
6
6
|
cwd: () => process.cwd(),
|
|
7
7
|
stdout: (message) => process.stdout.write(message + "\n"),
|
|
8
8
|
stderr: (message) => process.stderr.write(message + "\n")
|
|
9
9
|
});
|
|
10
|
+
if (typeof result === "number") {
|
|
11
|
+
process.exitCode = result;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
result.then((exitCode) => { process.exitCode = exitCode; }, (error) => {
|
|
15
|
+
process.stderr.write("nexus-plugin failed: " + (error instanceof Error ? error.message : String(error)) + "\n");
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
});
|
|
18
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pluginManifestSchema from "../.generated/plugin-manifest.schema.json";
|
|
2
|
-
export declare const PLUGIN_SDK_VERSION = "1.0
|
|
2
|
+
export declare const PLUGIN_SDK_VERSION = "1.1.0";
|
|
3
3
|
export { pluginManifestSchema };
|
|
4
4
|
export * from "../.generated/plugin-framework-version";
|
|
5
5
|
export * from "./types";
|
package/dist/src/index.js
CHANGED
|
@@ -20,7 +20,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
20
20
|
exports.pluginManifestSchema = exports.PLUGIN_SDK_VERSION = void 0;
|
|
21
21
|
const plugin_manifest_schema_json_1 = __importDefault(require("../.generated/plugin-manifest.schema.json"));
|
|
22
22
|
exports.pluginManifestSchema = plugin_manifest_schema_json_1.default;
|
|
23
|
-
exports.PLUGIN_SDK_VERSION = "1.0
|
|
23
|
+
exports.PLUGIN_SDK_VERSION = "1.1.0";
|
|
24
24
|
__exportStar(require("../.generated/plugin-framework-version"), exports);
|
|
25
25
|
__exportStar(require("./types"), exports);
|
|
26
26
|
__exportStar(require("./browser/frame-client"), exports);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type NexusDevUpdateType = "rendererUpdated" | "manifestUpdated";
|
|
2
|
+
export interface NexusDevUpdateResult {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
message: string;
|
|
5
|
+
requiresRestart?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare class NexusDevControlServer {
|
|
8
|
+
readonly token: string;
|
|
9
|
+
readonly pipePath: string;
|
|
10
|
+
private server;
|
|
11
|
+
private socket;
|
|
12
|
+
private receiveBuffer;
|
|
13
|
+
private helloAccepted;
|
|
14
|
+
private authenticated;
|
|
15
|
+
private authenticationResolve;
|
|
16
|
+
private authenticationReject;
|
|
17
|
+
private authenticationPromise;
|
|
18
|
+
private disconnectionPromise;
|
|
19
|
+
private disconnectionResolve;
|
|
20
|
+
private disconnectHandler;
|
|
21
|
+
private readonly pendingUpdates;
|
|
22
|
+
private readonly pendingShutdowns;
|
|
23
|
+
private nextRequestId;
|
|
24
|
+
start(): Promise<void>;
|
|
25
|
+
waitForAuthentication(timeoutMs?: number): Promise<void>;
|
|
26
|
+
sendUpdate(type: NexusDevUpdateType, pluginId: string): Promise<NexusDevUpdateResult>;
|
|
27
|
+
requestShutdown(): Promise<void>;
|
|
28
|
+
waitForDisconnection(timeoutMs?: number): Promise<void>;
|
|
29
|
+
setDisconnectHandler(handler: (error: Error) => void): () => void;
|
|
30
|
+
isConnected(): boolean;
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
private handleConnection;
|
|
33
|
+
private handleData;
|
|
34
|
+
private handleMessage;
|
|
35
|
+
private handleDisconnect;
|
|
36
|
+
private rejectPending;
|
|
37
|
+
private rejectPendingShutdowns;
|
|
38
|
+
private ensureAuthenticationPromise;
|
|
39
|
+
private ensureDisconnectionPromise;
|
|
40
|
+
private rejectAuthentication;
|
|
41
|
+
}
|
|
42
|
+
export declare function helloProofPayload(version: number, pid: number, nonce: string): string;
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.helloProofPayload = exports.NexusDevControlServer = void 0;
|
|
27
|
+
const crypto = __importStar(require("crypto"));
|
|
28
|
+
const fs = __importStar(require("fs"));
|
|
29
|
+
const net = __importStar(require("net"));
|
|
30
|
+
const os = __importStar(require("os"));
|
|
31
|
+
const path = __importStar(require("path"));
|
|
32
|
+
const dev_1 = require("./dev");
|
|
33
|
+
const protocolVersion = 1;
|
|
34
|
+
const maximumMessageBytes = 64 * 1024;
|
|
35
|
+
const requestTimeoutMs = 10000;
|
|
36
|
+
class NexusDevControlServer {
|
|
37
|
+
constructor() {
|
|
38
|
+
this.token = crypto.randomBytes(32).toString("hex");
|
|
39
|
+
this.pipePath = createPipePath();
|
|
40
|
+
this.receiveBuffer = Buffer.alloc(0);
|
|
41
|
+
this.helloAccepted = false;
|
|
42
|
+
this.authenticated = false;
|
|
43
|
+
this.pendingUpdates = new Map();
|
|
44
|
+
this.pendingShutdowns = new Map();
|
|
45
|
+
this.nextRequestId = 1;
|
|
46
|
+
}
|
|
47
|
+
async start() {
|
|
48
|
+
if (this.server) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
await removeUnixSocket(this.pipePath);
|
|
52
|
+
const server = net.createServer((socket) => this.handleConnection(socket));
|
|
53
|
+
this.server = server;
|
|
54
|
+
await new Promise((resolve, reject) => {
|
|
55
|
+
const handleError = (error) => {
|
|
56
|
+
server.removeListener("listening", handleListening);
|
|
57
|
+
reject(error);
|
|
58
|
+
};
|
|
59
|
+
const handleListening = () => {
|
|
60
|
+
server.removeListener("error", handleError);
|
|
61
|
+
resolve();
|
|
62
|
+
};
|
|
63
|
+
server.once("error", handleError);
|
|
64
|
+
server.once("listening", handleListening);
|
|
65
|
+
server.listen(this.pipePath);
|
|
66
|
+
}).catch((error) => {
|
|
67
|
+
this.server = undefined;
|
|
68
|
+
throw new dev_1.PluginDevError("Plugin development control pipe could not start: " + errorMessage(error));
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async waitForAuthentication(timeoutMs = 15000) {
|
|
72
|
+
if (this.authenticated) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const authenticationPromise = this.ensureAuthenticationPromise();
|
|
76
|
+
if (timeoutMs <= 0) {
|
|
77
|
+
await authenticationPromise;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let timer;
|
|
81
|
+
try {
|
|
82
|
+
await Promise.race([
|
|
83
|
+
authenticationPromise,
|
|
84
|
+
new Promise((_resolve, reject) => {
|
|
85
|
+
timer = setTimeout(() => reject(new dev_1.PluginDevError("Nexus did not authenticate the plugin development control connection.")), timeoutMs);
|
|
86
|
+
})
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
if (timer) {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
sendUpdate(type, pluginId) {
|
|
96
|
+
if (!this.authenticated || !this.socket || this.socket.destroyed) {
|
|
97
|
+
return Promise.reject(new dev_1.PluginDevError("Nexus plugin development control connection is not ready."));
|
|
98
|
+
}
|
|
99
|
+
const requestId = String(process.pid) + "-" + String(this.nextRequestId++);
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const timer = setTimeout(() => {
|
|
102
|
+
this.pendingUpdates.delete(requestId);
|
|
103
|
+
reject(new dev_1.PluginDevError("Nexus timed out while applying " + type + "."));
|
|
104
|
+
}, requestTimeoutMs);
|
|
105
|
+
this.pendingUpdates.set(requestId, { type, pluginId, resolve, reject, timer });
|
|
106
|
+
this.socket?.write(JSON.stringify({ type, pluginId, requestId }) + "\n");
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
requestShutdown() {
|
|
110
|
+
if (!this.authenticated || !this.socket || this.socket.destroyed) {
|
|
111
|
+
return Promise.reject(new dev_1.PluginDevError("Nexus plugin development control connection is not ready."));
|
|
112
|
+
}
|
|
113
|
+
const requestId = String(process.pid) + "-shutdown-" + String(this.nextRequestId++);
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
this.pendingShutdowns.delete(requestId);
|
|
117
|
+
reject(new dev_1.PluginDevError("Nexus timed out while accepting the plugin development restart."));
|
|
118
|
+
}, requestTimeoutMs);
|
|
119
|
+
this.pendingShutdowns.set(requestId, { resolve, reject, timer });
|
|
120
|
+
this.socket?.write(JSON.stringify({ type: "shutdown", requestId }) + "\n");
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
waitForDisconnection(timeoutMs = 10000) {
|
|
124
|
+
if (!this.socket) {
|
|
125
|
+
return Promise.resolve();
|
|
126
|
+
}
|
|
127
|
+
const disconnectionPromise = this.ensureDisconnectionPromise();
|
|
128
|
+
return withTimeout(disconnectionPromise, timeoutMs, "Nexus did not close the plugin development control connection.");
|
|
129
|
+
}
|
|
130
|
+
setDisconnectHandler(handler) {
|
|
131
|
+
this.disconnectHandler = handler;
|
|
132
|
+
return () => {
|
|
133
|
+
if (this.disconnectHandler === handler) {
|
|
134
|
+
this.disconnectHandler = undefined;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
isConnected() {
|
|
139
|
+
return Boolean(this.authenticated && this.socket && !this.socket.destroyed);
|
|
140
|
+
}
|
|
141
|
+
async close() {
|
|
142
|
+
this.rejectPending(new dev_1.PluginDevError("Plugin development control connection closed."));
|
|
143
|
+
this.rejectPendingShutdowns(new dev_1.PluginDevError("Plugin development control connection closed."));
|
|
144
|
+
this.rejectAuthentication(new dev_1.PluginDevError("Plugin development control connection closed."));
|
|
145
|
+
if (this.socket) {
|
|
146
|
+
this.socket.removeAllListeners();
|
|
147
|
+
this.socket.destroy();
|
|
148
|
+
this.socket = undefined;
|
|
149
|
+
}
|
|
150
|
+
this.disconnectionResolve?.();
|
|
151
|
+
this.disconnectionPromise = undefined;
|
|
152
|
+
this.disconnectionResolve = undefined;
|
|
153
|
+
const server = this.server;
|
|
154
|
+
this.server = undefined;
|
|
155
|
+
if (server) {
|
|
156
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
157
|
+
}
|
|
158
|
+
await removeUnixSocket(this.pipePath);
|
|
159
|
+
}
|
|
160
|
+
handleConnection(socket) {
|
|
161
|
+
if (this.socket) {
|
|
162
|
+
socket.destroy();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.socket = socket;
|
|
166
|
+
this.ensureAuthenticationPromise();
|
|
167
|
+
this.ensureDisconnectionPromise();
|
|
168
|
+
socket.on("data", (chunk) => this.handleData(socket, chunk));
|
|
169
|
+
socket.on("error", (error) => this.handleDisconnect(socket, error));
|
|
170
|
+
socket.on("close", () => this.handleDisconnect(socket, new dev_1.PluginDevError("Nexus plugin development control connection disconnected.")));
|
|
171
|
+
}
|
|
172
|
+
handleData(socket, chunk) {
|
|
173
|
+
if (socket !== this.socket) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
|
|
177
|
+
while (true) {
|
|
178
|
+
const newlineIndex = this.receiveBuffer.indexOf(0x0a);
|
|
179
|
+
if (newlineIndex < 0) {
|
|
180
|
+
if (this.receiveBuffer.length > maximumMessageBytes) {
|
|
181
|
+
this.handleDisconnect(socket, new dev_1.PluginDevError("Nexus plugin development control message exceeded the size limit."));
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (newlineIndex > maximumMessageBytes) {
|
|
186
|
+
this.handleDisconnect(socket, new dev_1.PluginDevError("Nexus plugin development control message exceeded the size limit."));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const line = this.receiveBuffer.subarray(0, newlineIndex);
|
|
190
|
+
this.receiveBuffer = this.receiveBuffer.subarray(newlineIndex + 1);
|
|
191
|
+
if (line.length > 0 && !this.handleMessage(socket, line)) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
handleMessage(socket, line) {
|
|
197
|
+
let value;
|
|
198
|
+
try {
|
|
199
|
+
value = JSON.parse(line.toString("utf8"));
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
this.handleDisconnect(socket, new dev_1.PluginDevError("Nexus plugin development control message is not valid JSON."));
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
if (!this.helloAccepted) {
|
|
206
|
+
if (!isHelloMessage(value) || !verifyHelloProof(value, this.token)) {
|
|
207
|
+
this.handleDisconnect(socket, new dev_1.PluginDevError("Nexus plugin development control proof is invalid."));
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
this.helloAccepted = true;
|
|
211
|
+
this.socket?.write(JSON.stringify({ type: "authenticate", token: this.token }) + "\n");
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (!this.authenticated && isAuthenticatedMessage(value)) {
|
|
215
|
+
this.authenticated = true;
|
|
216
|
+
this.authenticationResolve?.();
|
|
217
|
+
this.authenticationResolve = undefined;
|
|
218
|
+
this.authenticationReject = undefined;
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
if (!this.authenticated) {
|
|
222
|
+
this.handleDisconnect(socket, new dev_1.PluginDevError("Nexus plugin development control authentication response is invalid."));
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (isUpdateResult(value)) {
|
|
226
|
+
const pending = this.pendingUpdates.get(value.requestId);
|
|
227
|
+
if (!pending) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
if (value.updateType !== pending.type || value.pluginId !== pending.pluginId) {
|
|
231
|
+
const error = new dev_1.PluginDevError("Nexus plugin development update result did not match its request.");
|
|
232
|
+
this.pendingUpdates.delete(value.requestId);
|
|
233
|
+
clearTimeout(pending.timer);
|
|
234
|
+
pending.reject(error);
|
|
235
|
+
this.handleDisconnect(socket, error);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
this.pendingUpdates.delete(value.requestId);
|
|
239
|
+
clearTimeout(pending.timer);
|
|
240
|
+
pending.resolve({
|
|
241
|
+
ok: value.ok,
|
|
242
|
+
message: value.message,
|
|
243
|
+
...(value.requiresRestart ? { requiresRestart: true } : {})
|
|
244
|
+
});
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
if (isShutdownResult(value)) {
|
|
248
|
+
const pending = this.pendingShutdowns.get(value.requestId);
|
|
249
|
+
if (!pending) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
this.pendingShutdowns.delete(value.requestId);
|
|
253
|
+
clearTimeout(pending.timer);
|
|
254
|
+
if (value.ok) {
|
|
255
|
+
pending.resolve();
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
pending.reject(new dev_1.PluginDevError(value.message));
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
this.handleDisconnect(socket, new dev_1.PluginDevError("Nexus plugin development control message is invalid."));
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
handleDisconnect(socket, error) {
|
|
266
|
+
if (socket !== this.socket) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const wasAuthenticated = this.authenticated;
|
|
270
|
+
this.socket = undefined;
|
|
271
|
+
this.helloAccepted = false;
|
|
272
|
+
this.authenticated = false;
|
|
273
|
+
this.receiveBuffer = Buffer.alloc(0);
|
|
274
|
+
if (socket && !socket.destroyed) {
|
|
275
|
+
socket.destroy();
|
|
276
|
+
}
|
|
277
|
+
this.rejectAuthentication(error);
|
|
278
|
+
this.rejectPending(error);
|
|
279
|
+
this.rejectPendingShutdowns(error);
|
|
280
|
+
this.disconnectionResolve?.();
|
|
281
|
+
this.disconnectionPromise = undefined;
|
|
282
|
+
this.disconnectionResolve = undefined;
|
|
283
|
+
if (wasAuthenticated) {
|
|
284
|
+
this.disconnectHandler?.(error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
rejectPending(error) {
|
|
288
|
+
for (const pending of this.pendingUpdates.values()) {
|
|
289
|
+
clearTimeout(pending.timer);
|
|
290
|
+
pending.reject(error);
|
|
291
|
+
}
|
|
292
|
+
this.pendingUpdates.clear();
|
|
293
|
+
}
|
|
294
|
+
rejectPendingShutdowns(error) {
|
|
295
|
+
for (const pending of this.pendingShutdowns.values()) {
|
|
296
|
+
clearTimeout(pending.timer);
|
|
297
|
+
pending.reject(error);
|
|
298
|
+
}
|
|
299
|
+
this.pendingShutdowns.clear();
|
|
300
|
+
}
|
|
301
|
+
ensureAuthenticationPromise() {
|
|
302
|
+
if (!this.authenticationPromise) {
|
|
303
|
+
this.authenticationPromise = new Promise((resolve, reject) => {
|
|
304
|
+
this.authenticationResolve = resolve;
|
|
305
|
+
this.authenticationReject = reject;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return this.authenticationPromise;
|
|
309
|
+
}
|
|
310
|
+
ensureDisconnectionPromise() {
|
|
311
|
+
if (!this.disconnectionPromise) {
|
|
312
|
+
this.disconnectionPromise = new Promise((resolve) => {
|
|
313
|
+
this.disconnectionResolve = resolve;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return this.disconnectionPromise;
|
|
317
|
+
}
|
|
318
|
+
rejectAuthentication(error) {
|
|
319
|
+
this.authenticationReject?.(error);
|
|
320
|
+
this.authenticationPromise = undefined;
|
|
321
|
+
this.authenticationReject = undefined;
|
|
322
|
+
this.authenticationResolve = undefined;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
exports.NexusDevControlServer = NexusDevControlServer;
|
|
326
|
+
function withTimeout(promise, timeoutMs, message) {
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
const timer = setTimeout(() => reject(new dev_1.PluginDevError(message)), timeoutMs);
|
|
329
|
+
promise.then((value) => {
|
|
330
|
+
clearTimeout(timer);
|
|
331
|
+
resolve(value);
|
|
332
|
+
}, (error) => {
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
reject(error);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function createPipePath() {
|
|
339
|
+
const suffix = String(process.pid) + "-" + crypto.randomBytes(12).toString("hex");
|
|
340
|
+
return process.platform === "win32"
|
|
341
|
+
? "\\\\.\\pipe\\nexus-plugin-dev-" + suffix
|
|
342
|
+
: path.join(os.tmpdir(), "nexus-plugin-dev-" + suffix + ".sock");
|
|
343
|
+
}
|
|
344
|
+
function verifyHelloProof(value, token) {
|
|
345
|
+
if (value.proof === undefined) {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
const expected = crypto.createHmac("sha256", token)
|
|
349
|
+
.update(helloProofPayload(value.protocolVersion, value.pid, value.nonce), "utf8")
|
|
350
|
+
.digest("hex");
|
|
351
|
+
return value.proof.length === expected.length
|
|
352
|
+
&& crypto.timingSafeEqual(Buffer.from(value.proof, "utf8"), Buffer.from(expected, "utf8"));
|
|
353
|
+
}
|
|
354
|
+
function helloProofPayload(version, pid, nonce) {
|
|
355
|
+
return "nexus-plugin-dev:" + String(version) + ":" + String(pid) + ":" + nonce;
|
|
356
|
+
}
|
|
357
|
+
exports.helloProofPayload = helloProofPayload;
|
|
358
|
+
function isHelloMessage(value) {
|
|
359
|
+
if (!isRecord(value)) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
return value.type === "hello"
|
|
363
|
+
&& value.protocolVersion === protocolVersion
|
|
364
|
+
&& typeof value.pid === "number" && Number.isSafeInteger(value.pid) && value.pid > 0
|
|
365
|
+
&& typeof value.nonce === "string" && /^[a-f0-9]{32}$/i.test(value.nonce)
|
|
366
|
+
&& (value.proof === undefined || (typeof value.proof === "string" && /^[a-f0-9]{64}$/i.test(value.proof)));
|
|
367
|
+
}
|
|
368
|
+
function isAuthenticatedMessage(value) {
|
|
369
|
+
return isRecord(value)
|
|
370
|
+
&& value.type === "authenticated"
|
|
371
|
+
&& value.protocolVersion === protocolVersion
|
|
372
|
+
&& typeof value.pid === "number" && Number.isSafeInteger(value.pid) && value.pid > 0;
|
|
373
|
+
}
|
|
374
|
+
function isUpdateResult(value) {
|
|
375
|
+
return isRecord(value)
|
|
376
|
+
&& value.type === "updateResult"
|
|
377
|
+
&& typeof value.requestId === "string"
|
|
378
|
+
&& (value.updateType === "rendererUpdated" || value.updateType === "manifestUpdated")
|
|
379
|
+
&& typeof value.pluginId === "string"
|
|
380
|
+
&& typeof value.ok === "boolean"
|
|
381
|
+
&& typeof value.message === "string"
|
|
382
|
+
&& (value.requiresRestart === undefined || typeof value.requiresRestart === "boolean");
|
|
383
|
+
}
|
|
384
|
+
function isShutdownResult(value) {
|
|
385
|
+
return isRecord(value)
|
|
386
|
+
&& value.type === "shutdownResult"
|
|
387
|
+
&& typeof value.requestId === "string"
|
|
388
|
+
&& typeof value.ok === "boolean"
|
|
389
|
+
&& typeof value.message === "string";
|
|
390
|
+
}
|
|
391
|
+
function isRecord(value) {
|
|
392
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
393
|
+
}
|
|
394
|
+
async function removeUnixSocket(pipePath) {
|
|
395
|
+
if (process.platform !== "win32") {
|
|
396
|
+
await fs.promises.rm(pipePath, { force: true });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function errorMessage(error) {
|
|
400
|
+
return error instanceof Error ? error.message : String(error);
|
|
401
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface PluginDeploymentTransaction {
|
|
2
|
+
readonly targetPath: string;
|
|
3
|
+
commit(): void;
|
|
4
|
+
rollback(): void;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveNexusExecutable(hostPath: string): string;
|
|
7
|
+
export declare function beginPluginPackageDeployment(packageDirectory: string, nexusExecutable: string, pluginId: string): PluginDeploymentTransaction;
|
|
8
|
+
export declare function beginRendererDeployment(rendererDirectory: string, deployedPluginDirectory: string): PluginDeploymentTransaction;
|
|
9
|
+
export declare function beginManifestAndRendererDeployment(manifestPath: string, rendererDirectory: string, deployedPluginDirectory: string): PluginDeploymentTransaction;
|