@aflueza/dev-tools 0.0.0-stage → 0.2.1-rc.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.
@@ -0,0 +1,708 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdtemp, opendir, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { tmpdir, totalmem } from 'node:os';
4
+ import { join, resolve } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import * as systeminformation from 'systeminformation';
7
+ const execFileAsync = promisify(execFile);
8
+ const FILE_LOCK_BATCH_SIZE = 64;
9
+ export class ProcessManagerError extends Error {
10
+ code;
11
+ details;
12
+ constructor(code, message, details = {}) {
13
+ super(message);
14
+ this.name = 'ProcessManagerError';
15
+ this.code = code;
16
+ this.details = details;
17
+ }
18
+ }
19
+ export async function listProcesses(query) {
20
+ const snapshot = await buildProcessSnapshot();
21
+ const normalizedQuery = normalizeQuery(query);
22
+ const summaries = snapshot.processes.flatMap((process) => {
23
+ const { summary, commandLine } = summarizeProcess(process, snapshot.portMap);
24
+ const matchReason = matchesQuery(summary, commandLine, normalizedQuery);
25
+ if (normalizedQuery !== null && matchReason === null)
26
+ return [];
27
+ return [{ ...summary, matchReason }];
28
+ });
29
+ summaries.sort((left, right) => {
30
+ return (right.cpuPercent - left.cpuPercent ||
31
+ right.memoryBytes - left.memoryBytes ||
32
+ left.name.localeCompare(right.name) ||
33
+ left.pid - right.pid);
34
+ });
35
+ return summaries;
36
+ }
37
+ export async function getProcessDetail(pid) {
38
+ const snapshot = await buildProcessSnapshot();
39
+ const process = snapshot.processes.find((item) => item.pid === pid);
40
+ if (process === undefined) {
41
+ throw new ProcessManagerError('process-not-found', `process ${pid} was not found`);
42
+ }
43
+ const { summary, commandLine, bindings } = summarizeProcess(process, snapshot.portMap);
44
+ return {
45
+ ...summary,
46
+ commandLine,
47
+ parentPid: process.parentPid > 0 ? process.parentPid : null,
48
+ userName: process.user || null,
49
+ portBindings: bindings,
50
+ };
51
+ }
52
+ export async function findFileLockers(path) {
53
+ const normalizedPath = normalizeFilePath(path);
54
+ const target = await resolveFileLockTarget(normalizedPath);
55
+ const scan = target.targetType === 'directory'
56
+ ? await scanDirectoryFileLockers(target.path)
57
+ : {
58
+ candidates: await queryFileLockerCandidates([target.path]),
59
+ scannedFileCount: null,
60
+ };
61
+ if (scan.candidates.length === 0) {
62
+ return {
63
+ path: target.path,
64
+ targetType: target.targetType,
65
+ scannedFileCount: scan.scannedFileCount,
66
+ lockers: [],
67
+ };
68
+ }
69
+ const snapshot = await buildProcessSnapshot();
70
+ const processMap = new Map(snapshot.processes.map((process) => [process.pid, process]));
71
+ const lockers = scan.candidates.map((candidate) => {
72
+ const process = processMap.get(candidate.pid);
73
+ const summary = process
74
+ ? summarizeProcess(process, snapshot.portMap).summary
75
+ : emptyProcessSummary(candidate.pid, candidate.applicationName);
76
+ return {
77
+ ...summary,
78
+ matchReason: 'fileLock',
79
+ applicationName: candidate.applicationName,
80
+ applicationType: candidate.applicationType,
81
+ restartable: candidate.restartable,
82
+ };
83
+ });
84
+ lockers.sort((left, right) => {
85
+ return left.name.localeCompare(right.name) || left.pid - right.pid;
86
+ });
87
+ return {
88
+ path: target.path,
89
+ targetType: target.targetType,
90
+ scannedFileCount: scan.scannedFileCount,
91
+ lockers,
92
+ };
93
+ }
94
+ export function terminateProcess(pid, force = false) {
95
+ const signalMode = force ? 'force' : 'graceful';
96
+ const signal = force ? 'SIGKILL' : 'SIGTERM';
97
+ try {
98
+ process.kill(pid, signal);
99
+ }
100
+ catch (error) {
101
+ throw mapKillError(error, pid, force);
102
+ }
103
+ return {
104
+ success: true,
105
+ pid,
106
+ action: 'terminate',
107
+ signalMode,
108
+ message: force ? '进程已强制结束' : '进程已发送结束信号',
109
+ };
110
+ }
111
+ function summarizeProcess(process, portMap) {
112
+ const bindings = portMap.get(process.pid) ?? [];
113
+ const commandLine = commandLineOf(process);
114
+ return {
115
+ summary: {
116
+ pid: process.pid,
117
+ name: process.name || process.command || `PID ${process.pid}`,
118
+ exePath: process.path || null,
119
+ commandPreview: commandPreview(commandLine),
120
+ ports: uniquePorts(bindings),
121
+ cpuPercent: roundCpu(process.cpu),
122
+ memoryBytes: memoryBytesOf(process),
123
+ status: mapProcessStatus(process.state),
124
+ startedAt: normalizeStartedAt(process.started),
125
+ matchReason: null,
126
+ },
127
+ commandLine,
128
+ bindings,
129
+ };
130
+ }
131
+ function emptyProcessSummary(pid, applicationName) {
132
+ return {
133
+ pid,
134
+ name: applicationName || `PID ${pid}`,
135
+ exePath: null,
136
+ commandPreview: null,
137
+ ports: [],
138
+ cpuPercent: 0,
139
+ memoryBytes: 0,
140
+ status: 'unknown',
141
+ startedAt: null,
142
+ matchReason: 'fileLock',
143
+ };
144
+ }
145
+ async function buildProcessSnapshot() {
146
+ try {
147
+ const [processInfo, connections] = await Promise.all([
148
+ systeminformation.processes(),
149
+ systeminformation.networkConnections(),
150
+ ]);
151
+ return {
152
+ processes: processInfo.list,
153
+ portMap: collectPortMap(connections),
154
+ };
155
+ }
156
+ catch (error) {
157
+ throw new ProcessManagerError('collection-failed', error instanceof Error ? error.message : String(error));
158
+ }
159
+ }
160
+ async function queryFileLockerCandidates(paths) {
161
+ if (process.platform === 'win32') {
162
+ return queryWindowsFileLockers(paths);
163
+ }
164
+ return queryUnixFileLockers(paths);
165
+ }
166
+ async function queryWindowsFileLockers(paths) {
167
+ let tempDirectory = null;
168
+ try {
169
+ tempDirectory = await mkdtemp(join(tmpdir(), 'dsh-dev-tools-lock-'));
170
+ const manifestPath = join(tempDirectory, 'paths.json');
171
+ await writeFile(manifestPath, JSON.stringify(paths), 'utf8');
172
+ const { stdout } = await execFileAsync('powershell.exe', [
173
+ '-NoLogo',
174
+ '-NoProfile',
175
+ '-NonInteractive',
176
+ '-ExecutionPolicy',
177
+ 'Bypass',
178
+ '-Command',
179
+ WINDOWS_FILE_LOCK_SCRIPT,
180
+ ], {
181
+ encoding: 'utf8',
182
+ env: { ...process.env, DEV_TOOLS_LOCK_PATHS_PATH: manifestPath },
183
+ maxBuffer: 1024 * 1024,
184
+ windowsHide: true,
185
+ });
186
+ const parsed = JSON.parse(stdout.trim() || '[]');
187
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
188
+ const candidates = rows.flatMap((value) => {
189
+ if (typeof value !== 'object' || value === null)
190
+ return [];
191
+ const row = value;
192
+ const pid = Number(row.ProcessId ?? row.processId);
193
+ if (!Number.isInteger(pid) || pid <= 0)
194
+ return [];
195
+ return [
196
+ {
197
+ pid,
198
+ applicationName: stringOrNull(row.ApplicationName ?? row.applicationName),
199
+ applicationType: mapWindowsApplicationType(Number(row.ApplicationType ?? row.applicationType)),
200
+ restartable: booleanOrNull(row.Restartable ?? row.restartable),
201
+ },
202
+ ];
203
+ });
204
+ return dedupeFileLockerCandidates(candidates);
205
+ }
206
+ catch (error) {
207
+ if (isExecFileError(error) && error.code === 'ENOENT') {
208
+ throw new ProcessManagerError('unsupported-platform', '未找到 powershell.exe,当前系统无法查询文件占用进程。');
209
+ }
210
+ throw new ProcessManagerError('file-lock-query-failed', fileLockErrorMessage(error), {
211
+ fileCount: paths.length,
212
+ });
213
+ }
214
+ finally {
215
+ if (tempDirectory !== null) {
216
+ await rm(tempDirectory, { recursive: true, force: true }).catch(() => undefined);
217
+ }
218
+ }
219
+ }
220
+ async function queryUnixFileLockers(paths) {
221
+ const candidates = [];
222
+ const batchSize = 128;
223
+ for (let index = 0; index < paths.length; index += batchSize) {
224
+ const batch = paths.slice(index, index + batchSize);
225
+ try {
226
+ const { stdout } = await execFileAsync('lsof', ['-t', '--', ...batch], {
227
+ encoding: 'utf8',
228
+ maxBuffer: 1024 * 1024,
229
+ });
230
+ candidates.push(...stdout
231
+ .split(/\r?\n/)
232
+ .map((line) => Number(line.trim()))
233
+ .filter((pid) => Number.isInteger(pid) && pid > 0)
234
+ .map((pid) => ({
235
+ pid,
236
+ applicationName: null,
237
+ applicationType: null,
238
+ restartable: null,
239
+ })));
240
+ }
241
+ catch (error) {
242
+ if (isExecFileError(error) && error.code === 'ENOENT') {
243
+ throw new ProcessManagerError('unsupported-platform', '当前系统未安装 lsof,无法查询文件占用进程。');
244
+ }
245
+ if (isExecFileError(error) && String(error.code) === '1') {
246
+ continue;
247
+ }
248
+ throw new ProcessManagerError('file-lock-query-failed', fileLockErrorMessage(error), {
249
+ fileCount: paths.length,
250
+ });
251
+ }
252
+ }
253
+ return dedupeFileLockerCandidates(candidates);
254
+ }
255
+ function dedupeFileLockerCandidates(candidates) {
256
+ return [...new Map(candidates.map((candidate) => [candidate.pid, candidate])).values()];
257
+ }
258
+ function normalizeFilePath(value) {
259
+ const trimmed = value.trim().replace(/^"(.*)"$/, '$1');
260
+ if (!trimmed) {
261
+ throw new ProcessManagerError('invalid-payload', '文件路径不能为空');
262
+ }
263
+ return resolve(trimmed);
264
+ }
265
+ async function resolveFileLockTarget(path) {
266
+ let info;
267
+ try {
268
+ info = await stat(path);
269
+ }
270
+ catch (error) {
271
+ throw mapPathAccessError(error, path);
272
+ }
273
+ if (info.isFile()) {
274
+ return {
275
+ path,
276
+ targetType: 'file',
277
+ };
278
+ }
279
+ if (info.isDirectory()) {
280
+ return {
281
+ path,
282
+ targetType: 'directory',
283
+ };
284
+ }
285
+ throw new ProcessManagerError('invalid-payload', '仅支持查询文件或目录的占用进程', {
286
+ path,
287
+ });
288
+ }
289
+ async function scanDirectoryFileLockers(rootPath) {
290
+ let scannedFileCount = 0;
291
+ for await (const batch of iterateDirectoryFileBatches(rootPath)) {
292
+ scannedFileCount += batch.length;
293
+ const candidates = await queryFileLockerCandidates(batch);
294
+ if (candidates.length > 0) {
295
+ return { candidates, scannedFileCount };
296
+ }
297
+ }
298
+ return { candidates: [], scannedFileCount };
299
+ }
300
+ async function* iterateDirectoryFileBatches(rootPath) {
301
+ const pendingDirectories = [rootPath];
302
+ let batch = [];
303
+ while (pendingDirectories.length > 0) {
304
+ const directory = pendingDirectories.pop();
305
+ let directoryHandle;
306
+ try {
307
+ directoryHandle = await opendir(directory);
308
+ }
309
+ catch (error) {
310
+ if (directory === rootPath)
311
+ throw mapPathAccessError(error, rootPath);
312
+ continue;
313
+ }
314
+ try {
315
+ for await (const entry of directoryHandle) {
316
+ const entryPath = join(directory, entry.name);
317
+ if (entry.isDirectory()) {
318
+ pendingDirectories.push(entryPath);
319
+ continue;
320
+ }
321
+ if (entry.isFile()) {
322
+ batch.push(entryPath);
323
+ }
324
+ else if (entry.isSymbolicLink()) {
325
+ try {
326
+ const targetInfo = await stat(entryPath);
327
+ if (targetInfo.isFile())
328
+ batch.push(entryPath);
329
+ }
330
+ catch {
331
+ // A broken or inaccessible link cannot contribute to a directory lock scan.
332
+ }
333
+ }
334
+ if (batch.length >= FILE_LOCK_BATCH_SIZE) {
335
+ yield batch;
336
+ batch = [];
337
+ }
338
+ }
339
+ }
340
+ finally {
341
+ await directoryHandle.close().catch(() => undefined);
342
+ }
343
+ }
344
+ if (batch.length > 0)
345
+ yield batch;
346
+ }
347
+ function mapPathAccessError(error, path) {
348
+ if (isNodeError(error) && error.code === 'ENOENT') {
349
+ return new ProcessManagerError('file-not-found', `路径不存在: ${path}`, { path });
350
+ }
351
+ if (isNodeError(error) && (error.code === 'EACCES' || error.code === 'EPERM')) {
352
+ return new ProcessManagerError('permission-denied', `没有权限读取路径: ${path}`, { path });
353
+ }
354
+ return new ProcessManagerError('file-lock-query-failed', fileLockErrorMessage(error), {
355
+ path,
356
+ });
357
+ }
358
+ function collectPortMap(connections) {
359
+ const result = new Map();
360
+ for (const connection of connections) {
361
+ if (connection.pid <= 0)
362
+ continue;
363
+ const protocol = mapPortProtocol(connection.protocol);
364
+ if (protocol === null)
365
+ continue;
366
+ const localPort = parsePort(connection.localPort);
367
+ if (localPort === null)
368
+ continue;
369
+ const binding = {
370
+ protocol,
371
+ localAddress: connection.localAddress || '-',
372
+ localPort,
373
+ remoteAddress: normalizePeerValue(connection.peerAddress),
374
+ remotePort: parsePort(connection.peerPort),
375
+ state: connection.state || null,
376
+ };
377
+ const bindings = result.get(connection.pid) ?? [];
378
+ bindings.push(binding);
379
+ result.set(connection.pid, bindings);
380
+ }
381
+ return result;
382
+ }
383
+ function mapPortProtocol(protocol) {
384
+ const normalized = protocol.toLowerCase();
385
+ if (normalized.startsWith('tcp'))
386
+ return 'tcp';
387
+ if (normalized.startsWith('udp'))
388
+ return 'udp';
389
+ return null;
390
+ }
391
+ function parsePort(value) {
392
+ if (!value || value === '*')
393
+ return null;
394
+ const port = Number.parseInt(value, 10);
395
+ if (!Number.isInteger(port) || port < 0 || port > 65535)
396
+ return null;
397
+ return port;
398
+ }
399
+ function normalizePeerValue(value) {
400
+ if (!value || value === '*' || value === '0.0.0.0' || value === '::')
401
+ return null;
402
+ return value;
403
+ }
404
+ function commandLineOf(process) {
405
+ const command = process.command.trim();
406
+ const params = process.params.trim();
407
+ const value = params && !command.includes(params) ? `${command} ${params}` : command;
408
+ return value ? [value] : [];
409
+ }
410
+ function commandPreview(commandLine) {
411
+ const value = commandLine.join(' ').trim();
412
+ return value || null;
413
+ }
414
+ function memoryBytesOf(process) {
415
+ const rssBytes = Math.max(0, process.memRss) * 1024;
416
+ if (rssBytes > 0)
417
+ return rssBytes;
418
+ return Math.max(0, Math.round((Math.max(0, process.mem) / 100) * totalmem()));
419
+ }
420
+ function uniquePorts(bindings) {
421
+ return [...new Set(bindings.map((binding) => binding.localPort))].sort((left, right) => left - right);
422
+ }
423
+ function normalizeQuery(query) {
424
+ const normalized = query.trim().toLowerCase();
425
+ return normalized || null;
426
+ }
427
+ function matchesQuery(summary, commandLine, query) {
428
+ if (query === null)
429
+ return null;
430
+ if (summary.name.toLowerCase().includes(query))
431
+ return 'name';
432
+ if (String(summary.pid).includes(query))
433
+ return 'pid';
434
+ if (summary.ports.some((port) => String(port).includes(query)))
435
+ return 'port';
436
+ if (summary.exePath?.toLowerCase().includes(query))
437
+ return 'path';
438
+ if (commandLine.some((segment) => segment.toLowerCase().includes(query)))
439
+ return 'commandLine';
440
+ return null;
441
+ }
442
+ function roundCpu(value) {
443
+ return Math.round(Math.max(0, value) * 10) / 10;
444
+ }
445
+ function normalizeStartedAt(value) {
446
+ const trimmed = value.trim();
447
+ if (!trimmed)
448
+ return null;
449
+ const timestamp = new Date(trimmed);
450
+ return Number.isNaN(timestamp.getTime()) ? trimmed : timestamp.toISOString();
451
+ }
452
+ function mapProcessStatus(value) {
453
+ const status = value.toLowerCase();
454
+ if (status.includes('running') || status === 'run' || status === 'ready')
455
+ return 'running';
456
+ if (status.includes('sleeping') || status === 'sleep')
457
+ return 'sleeping';
458
+ if (status.includes('idle'))
459
+ return 'idle';
460
+ if (status.includes('zombie'))
461
+ return 'zombie';
462
+ if (status.includes('tracing') || status.includes('trace'))
463
+ return 'tracing';
464
+ if (status.includes('dead') || status.includes('terminated'))
465
+ return 'dead';
466
+ if (status.includes('wakekill'))
467
+ return 'wakekill';
468
+ if (status.includes('waking'))
469
+ return 'waking';
470
+ if (status.includes('parked'))
471
+ return 'parked';
472
+ if (status.includes('uninterruptible'))
473
+ return 'uninterruptibleDiskSleep';
474
+ if (status.includes('blocked'))
475
+ return 'lockBlocked';
476
+ if (status.includes('stop') || status.includes('suspended'))
477
+ return 'stopped';
478
+ return 'unknown';
479
+ }
480
+ function mapKillError(error, pid, force) {
481
+ const code = errorCode(error);
482
+ const action = force ? '强制结束' : '结束';
483
+ if (code === 'ESRCH') {
484
+ return new ProcessManagerError('process-not-found', `process ${pid} was not found`, { pid });
485
+ }
486
+ if (code === 'EPERM' || code === 'EACCES') {
487
+ return new ProcessManagerError('permission-denied', `没有权限${action}进程 ${pid}`, { pid });
488
+ }
489
+ return new ProcessManagerError('terminate-failed', error instanceof Error ? error.message : String(error), { pid });
490
+ }
491
+ function errorCode(error) {
492
+ if (typeof error !== 'object' || error === null || !('code' in error))
493
+ return null;
494
+ const code = error.code;
495
+ return typeof code === 'string' ? code : null;
496
+ }
497
+ function stringOrNull(value) {
498
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
499
+ }
500
+ function booleanOrNull(value) {
501
+ return typeof value === 'boolean' ? value : null;
502
+ }
503
+ function mapWindowsApplicationType(value) {
504
+ if (!Number.isInteger(value))
505
+ return null;
506
+ switch (value) {
507
+ case 0:
508
+ return 'unknown';
509
+ case 1:
510
+ return 'mainWindow';
511
+ case 2:
512
+ return 'otherWindow';
513
+ case 3:
514
+ return 'service';
515
+ case 4:
516
+ return 'explorer';
517
+ case 5:
518
+ return 'console';
519
+ case 1000:
520
+ return 'critical';
521
+ default:
522
+ return `type${value}`;
523
+ }
524
+ }
525
+ function isExecFileError(error) {
526
+ return error instanceof Error && 'code' in error;
527
+ }
528
+ function isNodeError(error) {
529
+ return error instanceof Error && 'code' in error;
530
+ }
531
+ function fileLockErrorMessage(error) {
532
+ if (isExecFileError(error)) {
533
+ const stderr = Buffer.isBuffer(error.stderr) ? error.stderr.toString('utf8') : error.stderr;
534
+ const detail = stderr
535
+ ?.split(/\r?\n/)
536
+ .map((line) => line.trim())
537
+ .filter((line) => line && !line.startsWith('+') && !line.startsWith('At line'))
538
+ .find((line) => /exception|异常|failed/i.test(line)) ??
539
+ stderr
540
+ ?.split(/\r?\n/)
541
+ .map((line) => line.trim())
542
+ .filter((line) => line && !line.startsWith('+') && !line.startsWith('At line'))
543
+ .at(-1);
544
+ if (detail)
545
+ return detail;
546
+ }
547
+ return error instanceof Error ? error.message : String(error);
548
+ }
549
+ const WINDOWS_FILE_LOCK_SCRIPT = String.raw `
550
+ $ErrorActionPreference = 'Stop'
551
+ [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
552
+
553
+ $source = @'
554
+ using System;
555
+ using System.Runtime.InteropServices;
556
+ using System.Text;
557
+
558
+ public static class RestartManagerProbe
559
+ {
560
+ private const int ERROR_SUCCESS = 0;
561
+ private const int ERROR_MORE_DATA = 234;
562
+
563
+ [StructLayout(LayoutKind.Sequential)]
564
+ private struct RM_UNIQUE_PROCESS
565
+ {
566
+ public int dwProcessId;
567
+ public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
568
+ }
569
+
570
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
571
+ private struct RM_PROCESS_INFO
572
+ {
573
+ public RM_UNIQUE_PROCESS Process;
574
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
575
+ public string strAppName;
576
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
577
+ public string strServiceShortName;
578
+ public int ApplicationType;
579
+ public uint AppStatus;
580
+ public uint TSSessionId;
581
+ [MarshalAs(UnmanagedType.Bool)]
582
+ public bool bRestartable;
583
+ }
584
+
585
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
586
+ public struct RestartManagerEntry
587
+ {
588
+ public int ProcessId;
589
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
590
+ public string ApplicationName;
591
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
592
+ public string ServiceShortName;
593
+ public int ApplicationType;
594
+ public uint ApplicationStatus;
595
+ public uint TerminalSessionId;
596
+ [MarshalAs(UnmanagedType.Bool)]
597
+ public bool Restartable;
598
+ }
599
+
600
+ [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
601
+ private static extern int RmStartSession(
602
+ out uint pSessionHandle,
603
+ int dwSessionFlags,
604
+ StringBuilder strSessionKey);
605
+
606
+ [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
607
+ private static extern int RmRegisterResources(
608
+ uint dwSessionHandle,
609
+ uint nFiles,
610
+ [In, MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr)]
611
+ string[] rgsFilenames,
612
+ uint nApplications,
613
+ RM_UNIQUE_PROCESS[] rgApplications,
614
+ uint nServices,
615
+ [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr)]
616
+ string[] rgsServiceNames);
617
+
618
+ [DllImport("rstrtmgr.dll")]
619
+ private static extern int RmGetList(
620
+ uint dwSessionHandle,
621
+ out uint pnProcInfoNeeded,
622
+ ref uint pnProcInfo,
623
+ [In, Out] RM_PROCESS_INFO[] rgAffectedApps,
624
+ ref uint lpdwRebootReasons);
625
+
626
+ [DllImport("rstrtmgr.dll")]
627
+ private static extern int RmEndSession(uint dwSessionHandle);
628
+
629
+ public static RestartManagerEntry[] Lockers(string[] paths)
630
+ {
631
+ if (paths == null || paths.Length == 0)
632
+ {
633
+ return Array.Empty<RestartManagerEntry>();
634
+ }
635
+
636
+ uint session;
637
+ var key = new StringBuilder(33);
638
+ var result = RmStartSession(out session, 0, key);
639
+ if (result != ERROR_SUCCESS)
640
+ {
641
+ throw new System.ComponentModel.Win32Exception(result);
642
+ }
643
+
644
+ try
645
+ {
646
+ result = RmRegisterResources(session, (uint)paths.Length, paths, 0, null, 0, null);
647
+ if (result != ERROR_SUCCESS)
648
+ {
649
+ throw new System.ComponentModel.Win32Exception(
650
+ result,
651
+ "RmRegisterResources failed for " + paths.Length + " files");
652
+ }
653
+
654
+ uint needed = 0;
655
+ uint count = 0;
656
+ uint reasons = 0;
657
+ result = RmGetList(session, out needed, ref count, null, ref reasons);
658
+ if (result == ERROR_SUCCESS)
659
+ {
660
+ return Array.Empty<RestartManagerEntry>();
661
+ }
662
+ if (result != ERROR_MORE_DATA)
663
+ {
664
+ throw new System.ComponentModel.Win32Exception(
665
+ result,
666
+ "RmGetList size query failed (" + result + ": "
667
+ + new System.ComponentModel.Win32Exception(result).Message + ")");
668
+ }
669
+
670
+ count = needed;
671
+ var processes = new RM_PROCESS_INFO[count];
672
+ result = RmGetList(session, out needed, ref count, processes, ref reasons);
673
+ if (result != ERROR_SUCCESS)
674
+ {
675
+ throw new System.ComponentModel.Win32Exception(
676
+ result,
677
+ "RmGetList result read failed");
678
+ }
679
+
680
+ var lockers = new RestartManagerEntry[count];
681
+ for (var index = 0; index < count; index += 1)
682
+ {
683
+ lockers[index] = new RestartManagerEntry
684
+ {
685
+ ProcessId = processes[index].Process.dwProcessId,
686
+ ApplicationName = processes[index].strAppName,
687
+ ServiceShortName = processes[index].strServiceShortName,
688
+ ApplicationType = processes[index].ApplicationType,
689
+ ApplicationStatus = processes[index].AppStatus,
690
+ TerminalSessionId = processes[index].TSSessionId,
691
+ Restartable = processes[index].bRestartable,
692
+ };
693
+ }
694
+ return lockers;
695
+ }
696
+ finally
697
+ {
698
+ RmEndSession(session);
699
+ }
700
+ }
701
+ }
702
+ '@
703
+
704
+ Add-Type -TypeDefinition $source -Language CSharp
705
+ $paths = [string[]](Get-Content -Raw -LiteralPath $env:DEV_TOOLS_LOCK_PATHS_PATH | ConvertFrom-Json)
706
+ $lockers = @([RestartManagerProbe]::Lockers([string[]]$paths))
707
+ ConvertTo-Json -InputObject $lockers -Compress -Depth 4
708
+ `;
@@ -0,0 +1 @@
1
+ export {};