@devscholar/node-ps1-dotnet 0.0.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/EnsureBom.ps1 +27 -0
- package/LICENSE.md +22 -0
- package/README.md +65 -0
- package/examples/console/await-delay/await-delay.ts +14 -0
- package/examples/console/console-input/console-input.ts +18 -0
- package/examples/winforms/counter/counter.ts +47 -0
- package/examples/winforms/drag-box/drag-box.ts +62 -0
- package/examples/wpf/counter/counter.ts +55 -0
- package/examples/wpf/drag-box/drag-box.ts +84 -0
- package/examples/wpf/webview2-browser/WebView2Libs/Microsoft.Web.WebView2.Core.dll +0 -0
- package/examples/wpf/webview2-browser/WebView2Libs/Microsoft.Web.WebView2.Core.dll.backup +0 -0
- package/examples/wpf/webview2-browser/WebView2Libs/Microsoft.Web.WebView2.Wpf.dll +0 -0
- package/examples/wpf/webview2-browser/WebView2Libs/Microsoft.Web.WebView2.Wpf.dll.backup +0 -0
- package/examples/wpf/webview2-browser/WebView2Libs/WebView2License.txt +27 -0
- package/examples/wpf/webview2-browser/counter.html +31 -0
- package/examples/wpf/webview2-browser/webview2-browser.ts +90 -0
- package/package.json +19 -0
- package/scripts/PsBridge/BridgeState.cs +39 -0
- package/scripts/PsBridge/Protocol.cs +222 -0
- package/scripts/PsBridge/PsBridge.psd1 +10 -0
- package/scripts/PsBridge/PsBridge.psm1 +22 -0
- package/scripts/PsBridge/PsHost.cs +393 -0
- package/scripts/PsBridge/PsHostEntry.cs +23 -0
- package/scripts/PsBridge/Reflection.cs +830 -0
- package/scripts/PsHost.ps1 +11 -0
- package/src/index.ts +510 -0
- package/src/ipc.ts +182 -0
- package/src/types.ts +21 -0
- package/src/utils.ts +13 -0
- package/start.js +67 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# scripts/PsHost.ps1
|
|
2
|
+
param($PipeName)
|
|
3
|
+
|
|
4
|
+
$ScriptDir = Split-Path $MyInvocation.MyCommand.Path
|
|
5
|
+
|
|
6
|
+
Import-Module "$ScriptDir\PsBridge" -Force
|
|
7
|
+
|
|
8
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
9
|
+
$ErrorActionPreference = "Stop"
|
|
10
|
+
|
|
11
|
+
[PsHostEntry]::Run($PipeName)
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as cp from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { getPowerShellPath } from './utils.ts';
|
|
7
|
+
import { IpcSync } from './ipc.ts';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
const gcRegistry = new FinalizationRegistry((id: string) => {
|
|
13
|
+
try { node_ps1_dotnet._release(id); } catch {}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const callbackRegistry = new Map<string, Function>();
|
|
17
|
+
const typeMetadataCache = new Map<string, Map<string, string>>();
|
|
18
|
+
const globalTypeCache = new Map<string, Map<string, string>>();
|
|
19
|
+
const pendingInspectRequests = new Map<string, string[]>();
|
|
20
|
+
const pendingInspectTimeout = new Map<string, NodeJS.Timeout>();
|
|
21
|
+
|
|
22
|
+
let ipc: IpcSync | null = null;
|
|
23
|
+
let proc: cp.ChildProcess | null = null;
|
|
24
|
+
let initialized = false;
|
|
25
|
+
let cachedRuntimeInfo: { frameworkMoniker: string; runtimeVersion: string } | null = null;
|
|
26
|
+
|
|
27
|
+
function cleanup() {
|
|
28
|
+
if (!initialized) return;
|
|
29
|
+
initialized = false;
|
|
30
|
+
|
|
31
|
+
if (ipc) {
|
|
32
|
+
try {
|
|
33
|
+
ipc.close();
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (proc && !proc.killed) {
|
|
38
|
+
try {
|
|
39
|
+
// Windows 下强制杀死进程,防止标准流卡死 Node.js 的事件循环
|
|
40
|
+
proc.kill('SIGKILL');
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
proc = null;
|
|
45
|
+
ipc = null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function initialize() {
|
|
49
|
+
if (initialized) return;
|
|
50
|
+
|
|
51
|
+
const pipeName = `PsNode_${process.pid}_${Math.floor(Math.random() * 10000)}`;
|
|
52
|
+
const scriptPath = path.join(__dirname, '..', 'scripts', 'PsHost.ps1');
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(scriptPath)) {
|
|
55
|
+
throw new Error(`Cannot find PsHost.ps1: ${scriptPath}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const powerShellPath = getPowerShellPath();
|
|
59
|
+
proc = cp.spawn(powerShellPath, [
|
|
60
|
+
'-NoProfile', '-ExecutionPolicy', 'Bypass',
|
|
61
|
+
'-Command', `& '${scriptPath}' -PipeName '${pipeName}'`
|
|
62
|
+
], { stdio: 'inherit', windowsHide: false });
|
|
63
|
+
|
|
64
|
+
// 让子进程尽量不阻止 Node.js 退出
|
|
65
|
+
proc.unref();
|
|
66
|
+
|
|
67
|
+
proc.on('exit', (code) => {
|
|
68
|
+
process.exit(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// 修复脚本跑完后无法停机卡死的 Bug:
|
|
72
|
+
// 当 V8 执行到底,事件循环清空时,触发 beforeExit -> 强杀子进程并立刻退出
|
|
73
|
+
process.on('beforeExit', () => {
|
|
74
|
+
cleanup();
|
|
75
|
+
process.exit(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
process.on('exit', () => {
|
|
79
|
+
cleanup();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
process.on('SIGINT', () => {
|
|
83
|
+
cleanup();
|
|
84
|
+
process.exit(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
process.on('SIGTERM', () => {
|
|
88
|
+
cleanup();
|
|
89
|
+
process.exit(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
process.on('uncaughtException', (err) => {
|
|
93
|
+
console.error('Uncaught Exception:', err);
|
|
94
|
+
cleanup();
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
ipc = new IpcSync(pipeName, (res: any) => {
|
|
99
|
+
const cb = callbackRegistry.get(res.callbackId!);
|
|
100
|
+
if (cb) {
|
|
101
|
+
const wrappedArgs = (res.args || []).map((arg: any) => {
|
|
102
|
+
if (arg && arg.type === 'ref' && arg.props) {
|
|
103
|
+
return createProxyWithInlineProps(arg);
|
|
104
|
+
}
|
|
105
|
+
return createProxy(arg);
|
|
106
|
+
});
|
|
107
|
+
return cb(...wrappedArgs);
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
ipc.connect();
|
|
113
|
+
initialized = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const typeNameCache = new Map<string, string>();
|
|
117
|
+
|
|
118
|
+
function getObjectTypeName(id: string): string | null {
|
|
119
|
+
if (typeNameCache.has(id)) {
|
|
120
|
+
return typeNameCache.get(id)!;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const res = ipc!.send({ action: 'GetTypeName', targetId: id });
|
|
124
|
+
if (res && res.typeName) {
|
|
125
|
+
typeNameCache.set(id, res.typeName);
|
|
126
|
+
return res.typeName;
|
|
127
|
+
}
|
|
128
|
+
} catch {}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getTypeMembers(typeName: string, memberNames: string[]): Map<string, string> | null {
|
|
133
|
+
if (globalTypeCache.has(typeName)) {
|
|
134
|
+
const cached = globalTypeCache.get(typeName)!;
|
|
135
|
+
const result = new Map<string, string>();
|
|
136
|
+
let allFound = true;
|
|
137
|
+
for (const name of memberNames) {
|
|
138
|
+
if (cached.has(name)) {
|
|
139
|
+
result.set(name, cached.get(name)!);
|
|
140
|
+
} else {
|
|
141
|
+
allFound = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (allFound) return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const res = ipc!.send({ action: 'InspectType', typeName, memberNames });
|
|
149
|
+
if (res && res.members) {
|
|
150
|
+
if (!globalTypeCache.has(typeName)) {
|
|
151
|
+
globalTypeCache.set(typeName, new Map());
|
|
152
|
+
}
|
|
153
|
+
const cache = globalTypeCache.get(typeName)!;
|
|
154
|
+
for (const [name, type] of Object.entries(res.members as Record<string, string>)) {
|
|
155
|
+
cache.set(name, type);
|
|
156
|
+
}
|
|
157
|
+
return new Map(Object.entries(res.members as Record<string, string>));
|
|
158
|
+
}
|
|
159
|
+
} catch {}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function ensureMemberTypeCached(id: string, memberName: string, memberCache: Map<string, string>) {
|
|
164
|
+
if (memberCache.has(memberName)) return;
|
|
165
|
+
|
|
166
|
+
const typeName = getObjectTypeName(id);
|
|
167
|
+
if (typeName) {
|
|
168
|
+
const members = getTypeMembers(typeName, [memberName]);
|
|
169
|
+
if (members && members.has(memberName)) {
|
|
170
|
+
memberCache.set(memberName, members.get(memberName)!);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const inspectRes = ipc!.send({ action: 'Inspect', targetId: id, memberName });
|
|
177
|
+
memberCache.set(memberName, inspectRes.memberType);
|
|
178
|
+
} catch {
|
|
179
|
+
memberCache.set(memberName, 'method');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function createProxyWithInlineProps(meta: any): any {
|
|
184
|
+
if (meta.type !== 'ref') return createProxy(meta);
|
|
185
|
+
|
|
186
|
+
const id = meta.id!;
|
|
187
|
+
const inlineProps = meta.props || {};
|
|
188
|
+
|
|
189
|
+
if (!typeMetadataCache.has(id)) {
|
|
190
|
+
typeMetadataCache.set(id, new Map());
|
|
191
|
+
}
|
|
192
|
+
const memberCache = typeMetadataCache.get(id)!;
|
|
193
|
+
|
|
194
|
+
class Stub {}
|
|
195
|
+
|
|
196
|
+
const proxy = new Proxy(Stub, {
|
|
197
|
+
get: (target: any, prop: string) => {
|
|
198
|
+
if (prop === '__ref') return id;
|
|
199
|
+
if (prop === '__inlineProps') return inlineProps;
|
|
200
|
+
if (typeof prop !== 'string') return undefined;
|
|
201
|
+
|
|
202
|
+
if (inlineProps.hasOwnProperty(prop)) {
|
|
203
|
+
memberCache.set(prop, 'property');
|
|
204
|
+
return inlineProps[prop];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (prop.startsWith('add_')) {
|
|
208
|
+
const eventName = prop.substring(4);
|
|
209
|
+
return (callback: Function) => {
|
|
210
|
+
const cbId = `cb_${Date.now()}_${Math.random()}`;
|
|
211
|
+
callbackRegistry.set(cbId, callback);
|
|
212
|
+
ipc!.send({ action: 'AddEvent', targetId: id, eventName, callbackId: cbId });
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let memType = memberCache.get(prop);
|
|
217
|
+
|
|
218
|
+
if (!memType) {
|
|
219
|
+
ensureMemberTypeCached(id, prop, memberCache);
|
|
220
|
+
memType = memberCache.get(prop);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (memType === 'property') {
|
|
224
|
+
const res = ipc!.send({ action: 'Invoke', targetId: id, methodName: prop, args: [] });
|
|
225
|
+
return createProxy(res);
|
|
226
|
+
} else {
|
|
227
|
+
return (...args: any[]) => {
|
|
228
|
+
const netArgs = args.map((a: any) => {
|
|
229
|
+
if (a && a.__ref) return { __ref: a.__ref };
|
|
230
|
+
if (typeof a === 'function') {
|
|
231
|
+
const cbId = `cb_arg_${Date.now()}_${Math.random()}`;
|
|
232
|
+
callbackRegistry.set(cbId, a);
|
|
233
|
+
return { type: 'callback', callbackId: cbId };
|
|
234
|
+
}
|
|
235
|
+
return a;
|
|
236
|
+
});
|
|
237
|
+
const res = ipc!.send({ action: 'Invoke', targetId: id, methodName: prop, args: netArgs });
|
|
238
|
+
return createProxy(res);
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
set: (target: any, prop: string, value: any) => {
|
|
244
|
+
if (typeof prop !== 'string') return false;
|
|
245
|
+
const netArg = (value && value.__ref) ? { __ref: value.__ref } : value;
|
|
246
|
+
ipc!.send({ action: 'Invoke', targetId: id, methodName: prop, args: [netArg] });
|
|
247
|
+
memberCache.set(prop, 'property');
|
|
248
|
+
return true;
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
construct: (target: any, args: any[]) => {
|
|
252
|
+
const netArgs = args.map((a: any) => {
|
|
253
|
+
if (a && a.__ref) return { __ref: a.__ref };
|
|
254
|
+
if (typeof a === 'function') {
|
|
255
|
+
const cbId = `cb_ctor_${Date.now()}_${Math.random()}`;
|
|
256
|
+
callbackRegistry.set(cbId, a);
|
|
257
|
+
return { type: 'callback', callbackId: cbId };
|
|
258
|
+
}
|
|
259
|
+
return a;
|
|
260
|
+
});
|
|
261
|
+
const res = ipc!.send({ action: 'New', typeId: id, args: netArgs });
|
|
262
|
+
return createProxy(res);
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
apply: () => { throw new Error("Cannot call .NET object as a function. Need 'new'?"); }
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
gcRegistry.register(proxy, id);
|
|
269
|
+
return proxy;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function createLazyArray(arr: any[]): any {
|
|
273
|
+
return new Proxy(arr, {
|
|
274
|
+
get(target, prop) {
|
|
275
|
+
if (typeof prop === 'symbol') return target[prop];
|
|
276
|
+
const index = Number(prop);
|
|
277
|
+
if (!isNaN(index) && index >= 0 && index < target.length) {
|
|
278
|
+
return createProxy(target[index]);
|
|
279
|
+
}
|
|
280
|
+
if (prop === 'length') return target.length;
|
|
281
|
+
if (prop === 'map' || prop === 'filter' || prop === 'forEach' || prop === 'reduce') {
|
|
282
|
+
return (...args: any[]) => {
|
|
283
|
+
const transformed = target.map((item: any) => createProxy(item));
|
|
284
|
+
return transformed[prop](...args);
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (prop === 'slice') {
|
|
288
|
+
return (...args: any[]) => {
|
|
289
|
+
const sliced = target.slice(...args);
|
|
290
|
+
return sliced.map((item: any) => createProxy(item));
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const LARGE_ARRAY_THRESHOLD = 50;
|
|
299
|
+
|
|
300
|
+
function createProxy(meta: any): any {
|
|
301
|
+
if (meta.type === 'primitive' || meta.type === 'null') return meta.value;
|
|
302
|
+
|
|
303
|
+
if (meta.type === 'array') {
|
|
304
|
+
const arr = meta.value;
|
|
305
|
+
if (arr.length <= LARGE_ARRAY_THRESHOLD) {
|
|
306
|
+
return arr.map((item: any) => createProxy(item));
|
|
307
|
+
}
|
|
308
|
+
return createLazyArray(arr);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (meta.type === 'task') {
|
|
312
|
+
const taskId = meta.id;
|
|
313
|
+
return new Promise((resolve, reject) => {
|
|
314
|
+
try {
|
|
315
|
+
const res = ipc!.send({ action: 'AwaitTask', taskId: taskId });
|
|
316
|
+
resolve(createProxy(res));
|
|
317
|
+
} catch (e) {
|
|
318
|
+
reject(e);
|
|
319
|
+
} finally {
|
|
320
|
+
try { ipc!.send({ action: 'Release', targetId: taskId }); } catch {}
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (meta.type === 'namespace') {
|
|
326
|
+
const nsName = meta.value;
|
|
327
|
+
return new Proxy({}, {
|
|
328
|
+
get: (target: any, prop: string) => {
|
|
329
|
+
if (typeof prop !== 'string') return undefined;
|
|
330
|
+
return node_ps1_dotnet._load(`${nsName}.${prop}`);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (meta.type !== 'ref') return null;
|
|
336
|
+
|
|
337
|
+
const id = meta.id!;
|
|
338
|
+
|
|
339
|
+
if (!typeMetadataCache.has(id)) {
|
|
340
|
+
typeMetadataCache.set(id, new Map());
|
|
341
|
+
}
|
|
342
|
+
const memberCache = typeMetadataCache.get(id)!;
|
|
343
|
+
|
|
344
|
+
class Stub {}
|
|
345
|
+
|
|
346
|
+
const proxy = new Proxy(Stub, {
|
|
347
|
+
get: (target: any, prop: string) => {
|
|
348
|
+
if (prop === '__ref') return id;
|
|
349
|
+
if (typeof prop !== 'string') return undefined;
|
|
350
|
+
|
|
351
|
+
if (prop.startsWith('add_')) {
|
|
352
|
+
const eventName = prop.substring(4);
|
|
353
|
+
return (callback: Function) => {
|
|
354
|
+
const cbId = `cb_${Date.now()}_${Math.random()}`;
|
|
355
|
+
callbackRegistry.set(cbId, callback);
|
|
356
|
+
ipc!.send({ action: 'AddEvent', targetId: id, eventName, callbackId: cbId });
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let memType = memberCache.get(prop);
|
|
361
|
+
|
|
362
|
+
if (!memType) {
|
|
363
|
+
ensureMemberTypeCached(id, prop, memberCache);
|
|
364
|
+
memType = memberCache.get(prop);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (memType === 'property') {
|
|
368
|
+
const res = ipc!.send({ action: 'Invoke', targetId: id, methodName: prop, args: [] });
|
|
369
|
+
return createProxy(res);
|
|
370
|
+
} else {
|
|
371
|
+
return (...args: any[]) => {
|
|
372
|
+
const netArgs = args.map((a: any) => {
|
|
373
|
+
if (a && a.__ref) return { __ref: a.__ref };
|
|
374
|
+
if (typeof a === 'function') {
|
|
375
|
+
const cbId = `cb_arg_${Date.now()}_${Math.random()}`;
|
|
376
|
+
callbackRegistry.set(cbId, a);
|
|
377
|
+
return { type: 'callback', callbackId: cbId };
|
|
378
|
+
}
|
|
379
|
+
return a;
|
|
380
|
+
});
|
|
381
|
+
const res = ipc!.send({ action: 'Invoke', targetId: id, methodName: prop, args: netArgs });
|
|
382
|
+
return createProxy(res);
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
set: (target: any, prop: string, value: any) => {
|
|
388
|
+
if (typeof prop !== 'string') return false;
|
|
389
|
+
const netArg = (value && value.__ref) ? { __ref: value.__ref } : value;
|
|
390
|
+
ipc!.send({ action: 'Invoke', targetId: id, methodName: prop, args: [netArg] });
|
|
391
|
+
memberCache.set(prop, 'property');
|
|
392
|
+
return true;
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
construct: (target: any, args: any[]) => {
|
|
396
|
+
const netArgs = args.map((a: any) => {
|
|
397
|
+
if (a && a.__ref) return { __ref: a.__ref };
|
|
398
|
+
if (typeof a === 'function') {
|
|
399
|
+
const cbId = `cb_ctor_${Date.now()}_${Math.random()}`;
|
|
400
|
+
callbackRegistry.set(cbId, a);
|
|
401
|
+
return { type: 'callback', callbackId: cbId };
|
|
402
|
+
}
|
|
403
|
+
return a;
|
|
404
|
+
});
|
|
405
|
+
const res = ipc!.send({ action: 'New', typeId: id, args: netArgs });
|
|
406
|
+
return createProxy(res);
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
apply: () => { throw new Error("Cannot call .NET object as a function. Need 'new'?"); }
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
gcRegistry.register(proxy, id);
|
|
413
|
+
return proxy;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export const node_ps1_dotnet = {
|
|
417
|
+
_load(typeName: string): any {
|
|
418
|
+
initialize();
|
|
419
|
+
const res = ipc!.send({ action: 'GetType', typeName });
|
|
420
|
+
return createProxy(res);
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
_release(id: string) {
|
|
424
|
+
if (ipc) {
|
|
425
|
+
try { ipc!.send({ action: 'Release', targetId: id }); } catch {}
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
_close() {
|
|
430
|
+
if (proc) proc.kill();
|
|
431
|
+
cleanup();
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
_getAssembly(assemblyName: string): any {
|
|
435
|
+
return this._load(assemblyName);
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
_loadAssembly(assemblyName: string): any {
|
|
439
|
+
initialize();
|
|
440
|
+
const res = ipc!.send({ action: 'LoadAssembly', assemblyName });
|
|
441
|
+
return createProxy(res);
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
_getRuntimeInfo(): { frameworkMoniker: string; runtimeVersion: string } {
|
|
445
|
+
if (cachedRuntimeInfo) return cachedRuntimeInfo;
|
|
446
|
+
initialize();
|
|
447
|
+
const res = ipc!.send({ action: 'GetRuntimeInfo' });
|
|
448
|
+
cachedRuntimeInfo = {
|
|
449
|
+
frameworkMoniker: res.frameworkMoniker || 'netstandard2.0',
|
|
450
|
+
runtimeVersion: res.runtimeVersion || '0.0.0'
|
|
451
|
+
};
|
|
452
|
+
return cachedRuntimeInfo;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
function createNamespaceProxy(assemblyName: string) {
|
|
457
|
+
return new Proxy({}, {
|
|
458
|
+
get: (target: any, prop: string) => {
|
|
459
|
+
if (typeof prop !== 'string') return undefined;
|
|
460
|
+
if (prop === 'then') return undefined; // 处理 Promise 检查
|
|
461
|
+
return node_ps1_dotnet._load(`${assemblyName}.${prop}`);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const dotnetProxy = new Proxy(function() {} as any, {
|
|
467
|
+
get: (target: any, prop: string) => {
|
|
468
|
+
if (prop === 'default') return dotnetProxy;
|
|
469
|
+
if (prop === 'then') return undefined;
|
|
470
|
+
if (prop === 'load') return (assemblyNameOrFilePath: string) => {
|
|
471
|
+
node_ps1_dotnet._loadAssembly(assemblyNameOrFilePath);
|
|
472
|
+
};
|
|
473
|
+
if (prop === 'frameworkMoniker') {
|
|
474
|
+
return node_ps1_dotnet._getRuntimeInfo().frameworkMoniker;
|
|
475
|
+
}
|
|
476
|
+
if (prop === 'runtimeVersion') {
|
|
477
|
+
return node_ps1_dotnet._getRuntimeInfo().runtimeVersion;
|
|
478
|
+
}
|
|
479
|
+
if (prop === '__inspect') {
|
|
480
|
+
return (targetId: string, memberName: string) => ipc!.send({ action: 'Inspect', targetId, memberName });
|
|
481
|
+
}
|
|
482
|
+
return node_ps1_dotnet._load(prop);
|
|
483
|
+
},
|
|
484
|
+
apply: (target: any, argArray: any[], newTarget: any) => {
|
|
485
|
+
return createNamespaceProxy(argArray[0]);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
function createExportNamespaceProxy(namespacePrefix: string): any {
|
|
490
|
+
const cache = new Map<string, any>();
|
|
491
|
+
|
|
492
|
+
return new Proxy({} as any, {
|
|
493
|
+
get: (target: any, prop: string) => {
|
|
494
|
+
if (typeof prop !== 'string') return undefined;
|
|
495
|
+
if (prop === 'then') return undefined;
|
|
496
|
+
|
|
497
|
+
const fullName = `${namespacePrefix}.${prop}`;
|
|
498
|
+
if (cache.has(fullName)) {
|
|
499
|
+
return cache.get(fullName);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const result = node_ps1_dotnet._load(fullName);
|
|
503
|
+
cache.set(fullName, result);
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export default dotnetProxy;
|
|
510
|
+
export const System = createExportNamespaceProxy('System');
|
package/src/ipc.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// src/ipc.ts
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import type { ProtocolResponse, CommandRequest } from './types.ts';
|
|
4
|
+
|
|
5
|
+
declare const Deno: any;
|
|
6
|
+
|
|
7
|
+
const MAX_LINE_LENGTH = 1024 * 1024 * 2; // 2MB buffer per line
|
|
8
|
+
const CHUNK_SIZE = 16 * 1024; // 16KB chunk size for buffering
|
|
9
|
+
const isDeno = typeof Deno !== 'undefined';
|
|
10
|
+
|
|
11
|
+
export class IpcSync {
|
|
12
|
+
public fd: number = 0;
|
|
13
|
+
private exited: boolean = false;
|
|
14
|
+
|
|
15
|
+
private readBuffer = isDeno ? new Uint8Array(CHUNK_SIZE) : Buffer.alloc(CHUNK_SIZE);
|
|
16
|
+
private resultBuffer = isDeno ? new Uint8Array(MAX_LINE_LENGTH) : Buffer.alloc(MAX_LINE_LENGTH);
|
|
17
|
+
private bufferOffset = 0;
|
|
18
|
+
private bufferLength = 0;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private pipeName: string,
|
|
22
|
+
private onEvent: (msg: ProtocolResponse) => any
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
private readLineSync(): string | null {
|
|
26
|
+
let resultOffset = 0;
|
|
27
|
+
|
|
28
|
+
while (true) {
|
|
29
|
+
if (this.bufferOffset >= this.bufferLength) {
|
|
30
|
+
try {
|
|
31
|
+
if (isDeno) {
|
|
32
|
+
this.readBuffer.fill(0);
|
|
33
|
+
} else {
|
|
34
|
+
(this.readBuffer as Buffer).fill(0);
|
|
35
|
+
}
|
|
36
|
+
const bytesRead = fs.readSync(this.fd, this.readBuffer, 0, CHUNK_SIZE, null);
|
|
37
|
+
if (bytesRead === 0) {
|
|
38
|
+
if (resultOffset === 0) return null;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
this.bufferOffset = 0;
|
|
42
|
+
this.bufferLength = bytesRead;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let lineEnd = -1;
|
|
49
|
+
for (let i = this.bufferOffset; i < this.bufferLength; i++) {
|
|
50
|
+
if (this.readBuffer[i] === 10) {
|
|
51
|
+
lineEnd = i;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (lineEnd !== -1) {
|
|
57
|
+
const lineLength = lineEnd - this.bufferOffset;
|
|
58
|
+
if (resultOffset + lineLength > MAX_LINE_LENGTH) {
|
|
59
|
+
throw new Error("IPC Pipe line length exceeded max limit.");
|
|
60
|
+
}
|
|
61
|
+
if (isDeno) {
|
|
62
|
+
this.resultBuffer.set(this.readBuffer.subarray(this.bufferOffset, lineEnd), resultOffset);
|
|
63
|
+
} else {
|
|
64
|
+
(this.readBuffer as Buffer).copy(this.resultBuffer as Buffer, resultOffset, this.bufferOffset, lineEnd);
|
|
65
|
+
}
|
|
66
|
+
resultOffset += lineLength;
|
|
67
|
+
this.bufferOffset = lineEnd + 1;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const availableLength = this.bufferLength - this.bufferOffset;
|
|
72
|
+
if (resultOffset + availableLength > MAX_LINE_LENGTH) {
|
|
73
|
+
throw new Error("IPC Pipe line length exceeded max limit.");
|
|
74
|
+
}
|
|
75
|
+
if (isDeno) {
|
|
76
|
+
this.resultBuffer.set(this.readBuffer.subarray(this.bufferOffset, this.bufferLength), resultOffset);
|
|
77
|
+
} else {
|
|
78
|
+
(this.readBuffer as Buffer).copy(this.resultBuffer as Buffer, resultOffset, this.bufferOffset, this.bufferLength);
|
|
79
|
+
}
|
|
80
|
+
resultOffset += availableLength;
|
|
81
|
+
this.bufferOffset = this.bufferLength;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (resultOffset === 0) return '';
|
|
85
|
+
|
|
86
|
+
if (isDeno) {
|
|
87
|
+
return new TextDecoder().decode(this.resultBuffer.subarray(0, resultOffset));
|
|
88
|
+
} else {
|
|
89
|
+
return (this.resultBuffer as Buffer).toString('utf8', 0, resultOffset);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private tryConnect(pipePath: string): boolean {
|
|
94
|
+
try {
|
|
95
|
+
this.fd = fs.openSync(pipePath, 'r+');
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
connect() {
|
|
103
|
+
const pipePath = `\\\\.\\pipe\\${this.pipeName}`;
|
|
104
|
+
const start = Date.now();
|
|
105
|
+
let delay = 1;
|
|
106
|
+
const maxDelay = 100;
|
|
107
|
+
|
|
108
|
+
while (true) {
|
|
109
|
+
if (this.tryConnect(pipePath)) break;
|
|
110
|
+
|
|
111
|
+
if (Date.now() - start > 5000) {
|
|
112
|
+
throw new Error(`Timeout connecting pipe: ${pipePath}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const target = Date.now() + delay;
|
|
116
|
+
while (Date.now() < target) {
|
|
117
|
+
if (this.tryConnect(pipePath)) return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
delay = Math.min(delay * 1.5, maxDelay);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
send(cmd: CommandRequest): ProtocolResponse {
|
|
125
|
+
if (this.exited) {
|
|
126
|
+
return { type: 'exit', message: '' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
fs.writeSync(this.fd, JSON.stringify(cmd) + '\n');
|
|
131
|
+
} catch (e) {
|
|
132
|
+
throw new Error("Pipe closed (Write failed)");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
while (true) {
|
|
136
|
+
const line = this.readLineSync();
|
|
137
|
+
if (line === null) throw new Error("Pipe closed (Read EOF)");
|
|
138
|
+
if (!line.trim()) continue;
|
|
139
|
+
|
|
140
|
+
let res: ProtocolResponse;
|
|
141
|
+
try {
|
|
142
|
+
res = JSON.parse(line);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
throw new Error(`Pipe closed (Invalid JSON): ${line}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (res.type === 'event') {
|
|
148
|
+
let result = null;
|
|
149
|
+
try {
|
|
150
|
+
result = this.onEvent(res);
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.error("Callback Error:", e);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const reply = { type: 'reply', result: result };
|
|
156
|
+
try {
|
|
157
|
+
fs.writeSync(this.fd, JSON.stringify(reply) + '\n');
|
|
158
|
+
} catch {}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (res.type === 'error') throw new Error(`Host Error: ${res.message}`);
|
|
163
|
+
|
|
164
|
+
if (res.type === 'exit') {
|
|
165
|
+
this.exited = true;
|
|
166
|
+
return res;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return res;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
close() {
|
|
174
|
+
this.exited = true;
|
|
175
|
+
if (this.fd) {
|
|
176
|
+
try {
|
|
177
|
+
fs.closeSync(this.fd);
|
|
178
|
+
} catch {}
|
|
179
|
+
this.fd = 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|