@devscholar/node-ps1-dotnet 0.0.1 → 0.0.2

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/src/index.ts CHANGED
@@ -1,52 +1,44 @@
1
- // src/index.ts
2
1
  import * as fs from 'node:fs';
3
2
  import * as path from 'node:path';
4
3
  import * as cp from 'node:child_process';
5
4
  import { fileURLToPath } from 'node:url';
6
- import { getPowerShellPath } from './utils.ts';
7
- import { IpcSync } from './ipc.ts';
5
+ import { getPowerShellPath } from './utils.js';
6
+ import { IpcSync } from './ipc.js';
7
+ import { getIpc, setIpc, getProc, setProc, getInitialized, setInitialized, getCachedRuntimeInfo, setCachedRuntimeInfo } from './state.js';
8
+ import { callbackRegistry, createProxyWithInlineProps, createProxy, setNodePs1Dotnet } from './proxy.js';
9
+ import { createNamespaceProxy, createExportNamespaceProxy } from './namespace.js';
8
10
 
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;
11
+ export const __filename = fileURLToPath(import.meta.url);
12
+ export const __dirname = path.dirname(__filename);
26
13
 
27
14
  function cleanup() {
28
- if (!initialized) return;
29
- initialized = false;
15
+ if (!getInitialized()) return;
16
+ setInitialized(false);
30
17
 
18
+ const ipc = getIpc();
31
19
  if (ipc) {
32
20
  try {
33
21
  ipc.close();
34
22
  } catch {}
35
23
  }
36
24
 
25
+ const proc = getProc();
37
26
  if (proc && !proc.killed) {
38
27
  try {
39
- // Windows 下强制杀死进程,防止标准流卡死 Node.js 的事件循环
40
28
  proc.kill('SIGKILL');
41
29
  } catch {}
42
30
  }
43
31
 
44
- proc = null;
45
- ipc = null;
32
+ setProc(null);
33
+ setIpc(null);
46
34
  }
47
35
 
48
- function initialize() {
49
- if (initialized) return;
36
+ function doInitialize() {
37
+ if (getInitialized()) return;
38
+
39
+ if (process.platform !== 'win32') {
40
+ throw new Error('node-ps1-dotnet is only supported on Windows. Use node-with-gjs for Linux/macOS.');
41
+ }
50
42
 
51
43
  const pipeName = `PsNode_${process.pid}_${Math.floor(Math.random() * 10000)}`;
52
44
  const scriptPath = path.join(__dirname, '..', 'scripts', 'PsHost.ps1');
@@ -56,20 +48,18 @@ function initialize() {
56
48
  }
57
49
 
58
50
  const powerShellPath = getPowerShellPath();
59
- proc = cp.spawn(powerShellPath, [
51
+ const proc = cp.spawn(powerShellPath, [
60
52
  '-NoProfile', '-ExecutionPolicy', 'Bypass',
61
53
  '-Command', `& '${scriptPath}' -PipeName '${pipeName}'`
62
54
  ], { stdio: 'inherit', windowsHide: false });
63
55
 
64
- // 让子进程尽量不阻止 Node.js 退出
56
+ setProc(proc);
65
57
  proc.unref();
66
58
 
67
59
  proc.on('exit', (code) => {
68
60
  process.exit(0);
69
61
  });
70
62
 
71
- // 修复脚本跑完后无法停机卡死的 Bug:
72
- // 当 V8 执行到底,事件循环清空时,触发 beforeExit -> 强杀子进程并立刻退出
73
63
  process.on('beforeExit', () => {
74
64
  cleanup();
75
65
  process.exit(0);
@@ -95,7 +85,7 @@ function initialize() {
95
85
  process.exit(1);
96
86
  });
97
87
 
98
- ipc = new IpcSync(pipeName, (res: any) => {
88
+ const ipc = new IpcSync(pipeName, (res: any) => {
99
89
  const cb = callbackRegistry.get(res.callbackId!);
100
90
  if (cb) {
101
91
  const wrappedArgs = (res.args || []).map((arg: any) => {
@@ -110,323 +100,27 @@ function initialize() {
110
100
  });
111
101
 
112
102
  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;
103
+ setIpc(ipc);
104
+ setInitialized(true);
414
105
  }
415
106
 
416
107
  export const node_ps1_dotnet = {
417
108
  _load(typeName: string): any {
418
- initialize();
109
+ doInitialize();
110
+ const ipc = getIpc();
419
111
  const res = ipc!.send({ action: 'GetType', typeName });
420
112
  return createProxy(res);
421
113
  },
422
114
 
423
115
  _release(id: string) {
116
+ const ipc = getIpc();
424
117
  if (ipc) {
425
118
  try { ipc!.send({ action: 'Release', targetId: id }); } catch {}
426
119
  }
427
120
  },
428
121
 
429
122
  _close() {
123
+ const proc = getProc();
430
124
  if (proc) proc.kill();
431
125
  cleanup();
432
126
  },
@@ -436,32 +130,27 @@ export const node_ps1_dotnet = {
436
130
  },
437
131
 
438
132
  _loadAssembly(assemblyName: string): any {
439
- initialize();
133
+ doInitialize();
134
+ const ipc = getIpc();
440
135
  const res = ipc!.send({ action: 'LoadAssembly', assemblyName });
441
136
  return createProxy(res);
442
137
  },
443
138
 
444
139
  _getRuntimeInfo(): { frameworkMoniker: string; runtimeVersion: string } {
445
- if (cachedRuntimeInfo) return cachedRuntimeInfo;
446
- initialize();
140
+ if (getCachedRuntimeInfo()) return getCachedRuntimeInfo()!;
141
+ doInitialize();
142
+ const ipc = getIpc();
447
143
  const res = ipc!.send({ action: 'GetRuntimeInfo' });
448
- cachedRuntimeInfo = {
144
+ const info = {
449
145
  frameworkMoniker: res.frameworkMoniker || 'netstandard2.0',
450
146
  runtimeVersion: res.runtimeVersion || '0.0.0'
451
147
  };
452
- return cachedRuntimeInfo;
148
+ setCachedRuntimeInfo(info);
149
+ return info;
453
150
  }
454
151
  };
455
152
 
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
- }
153
+ setNodePs1Dotnet(() => node_ps1_dotnet);
465
154
 
466
155
  const dotnetProxy = new Proxy(function() {} as any, {
467
156
  get: (target: any, prop: string) => {
@@ -477,34 +166,29 @@ const dotnetProxy = new Proxy(function() {} as any, {
477
166
  return node_ps1_dotnet._getRuntimeInfo().runtimeVersion;
478
167
  }
479
168
  if (prop === '__inspect') {
169
+ const ipc = getIpc();
480
170
  return (targetId: string, memberName: string) => ipc!.send({ action: 'Inspect', targetId, memberName });
481
171
  }
482
172
  return node_ps1_dotnet._load(prop);
483
173
  },
484
174
  apply: (target: any, argArray: any[], newTarget: any) => {
485
- return createNamespaceProxy(argArray[0]);
175
+ return createNamespaceProxy(argArray[0], node_ps1_dotnet);
486
176
  }
487
177
  });
488
178
 
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
- });
179
+ export default dotnetProxy;
180
+
181
+ let _System: any;
182
+ export function getSystem(): any {
183
+ if (!_System) {
184
+ _System = createExportNamespaceProxy('System', node_ps1_dotnet);
185
+ }
186
+ return _System;
507
187
  }
508
188
 
509
- export default dotnetProxy;
510
- export const System = createExportNamespaceProxy('System');
189
+ export const System = new Proxy({} as any, {
190
+ get: (target: any, prop: string) => {
191
+ if (prop === 'then') return undefined;
192
+ return getSystem()[prop];
193
+ }
194
+ });
@@ -0,0 +1,116 @@
1
+ import { getIpc } from './state.js';
2
+ import { createProxy } from './proxy.js';
3
+
4
+ export function createNamespaceProxy(assemblyName: string, dotnet: any) {
5
+ return new Proxy({}, {
6
+ get: (target: any, prop: string) => {
7
+ if (typeof prop !== 'string') return undefined;
8
+ if (prop === 'then') return undefined;
9
+
10
+ const fullName = `${assemblyName}.${prop}`;
11
+ const loaded = dotnet._load(fullName);
12
+
13
+ let typeId: string | null = null;
14
+ try {
15
+ const ref = loaded.__ref;
16
+ if (typeof ref === 'string' && ref.length > 0) {
17
+ typeId = ref;
18
+ }
19
+ } catch {}
20
+
21
+ if (typeId) {
22
+ return new Proxy({}, {
23
+ get: (target2: any, prop2: string) => {
24
+ if (typeof prop2 !== 'string') return undefined;
25
+
26
+ if (prop2 === '__ref') {
27
+ return typeId;
28
+ }
29
+
30
+ if (prop2 === 'value__') {
31
+ const ipc = getIpc();
32
+ try {
33
+ const res = ipc!.send({ action: 'Invoke', targetId: typeId, methodName: prop2, args: [] });
34
+ if (res && res.type === 'primitive') {
35
+ return res.value;
36
+ }
37
+ } catch {}
38
+ return 0;
39
+ }
40
+
41
+ const ipc = getIpc();
42
+ try {
43
+ const res = ipc!.send({ action: 'Invoke', targetId: typeId, methodName: prop2, args: [] });
44
+ if (res) {
45
+ if (res.type === 'primitive') {
46
+ return res.value;
47
+ }
48
+ if (res.type === 'ref') {
49
+ try {
50
+ const valueRes = ipc!.send({ action: 'Invoke', targetId: res.id, methodName: 'value__', args: [] });
51
+ if (valueRes && valueRes.type === 'primitive') {
52
+ return valueRes.value;
53
+ }
54
+ } catch {
55
+ }
56
+
57
+ try {
58
+ const typeNameRes = ipc!.send({ action: 'GetTypeName', targetId: res.id });
59
+ const typeName = typeNameRes?.typeName || '';
60
+
61
+ if (typeName && !typeName.startsWith('System.')) {
62
+ try {
63
+ const hashRes = ipc!.send({ action: 'Invoke', targetId: res.id, methodName: 'GetHashCode', args: [] });
64
+ if (hashRes && hashRes.type === 'primitive') {
65
+ const proxy = createProxy(res);
66
+ (proxy as any).__value = hashRes.value;
67
+ return proxy;
68
+ }
69
+ } catch {}
70
+ }
71
+ } catch {}
72
+
73
+ return createProxy(res);
74
+ }
75
+ }
76
+ } catch {}
77
+
78
+ return (...args: any[]) => {
79
+ const ipc = getIpc();
80
+ const netArgs = args.map((a: any) => {
81
+ if (a && a.__ref) return { __ref: a.__ref };
82
+ return a;
83
+ });
84
+ const res = ipc!.send({ action: 'Invoke', targetId: typeId, methodName: prop2, args: netArgs });
85
+ return createProxy(res);
86
+ };
87
+ },
88
+ set: () => false,
89
+ apply: () => { throw new Error("Cannot call .NET enum as function"); }
90
+ });
91
+ }
92
+
93
+ return loaded;
94
+ }
95
+ });
96
+ }
97
+
98
+ export function createExportNamespaceProxy(namespacePrefix: string, dotnet: any) {
99
+ const cache = new Map<string, any>();
100
+
101
+ return new Proxy({} as any, {
102
+ get: (target: any, prop: string) => {
103
+ if (typeof prop !== 'string') return undefined;
104
+ if (prop === 'then') return undefined;
105
+
106
+ const fullName = `${namespacePrefix}.${prop}`;
107
+ if (cache.has(fullName)) {
108
+ return cache.get(fullName);
109
+ }
110
+
111
+ const result = dotnet._load(fullName);
112
+ cache.set(fullName, result);
113
+ return result;
114
+ }
115
+ });
116
+ }