uloop-cli 1.7.0 → 1.7.3
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/dist/cli.bundle.cjs +352 -53
- package/dist/cli.bundle.cjs.map +4 -4
- package/package.json +6 -6
- package/src/__tests__/busy-state-order.test.ts +120 -0
- package/src/__tests__/cli-project-error.test.ts +45 -0
- package/src/__tests__/cli-update.test.ts +129 -0
- package/src/__tests__/execute-tool.test.ts +67 -2
- package/src/__tests__/package-metadata.test.ts +28 -0
- package/src/__tests__/port-resolver.test.ts +3 -5
- package/src/__tests__/unity-process.test.ts +289 -0
- package/src/cli-project-error.ts +36 -0
- package/src/cli.ts +19 -23
- package/src/default-tools.json +1 -1
- package/src/execute-tool.ts +82 -17
- package/src/port-resolver.ts +6 -6
- package/src/project-root.ts +1 -1
- package/src/unity-process.ts +337 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
const WINDOWS_PROCESS_QUERY =
|
|
6
|
+
'Get-CimInstance Win32_Process -Filter "name = \'Unity.exe\'" | Select-Object ProcessId, CommandLine | ConvertTo-Json -Compress';
|
|
7
|
+
|
|
8
|
+
interface RunningUnityProcess {
|
|
9
|
+
pid: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface RawUnityProcess {
|
|
13
|
+
pid: number;
|
|
14
|
+
commandLine: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UnityProcessCommand {
|
|
18
|
+
command: string;
|
|
19
|
+
args: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface UnityProcessDependencies {
|
|
23
|
+
platform: NodeJS.Platform;
|
|
24
|
+
runCommand: (command: string, args: string[]) => Promise<string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const defaultDependencies: UnityProcessDependencies = {
|
|
28
|
+
platform: process.platform,
|
|
29
|
+
runCommand: runUnityProcessQuery,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function buildUnityProcessCommand(platform: NodeJS.Platform): UnityProcessCommand | null {
|
|
33
|
+
if (platform === 'darwin') {
|
|
34
|
+
return {
|
|
35
|
+
command: 'ps',
|
|
36
|
+
args: ['-Ao', 'pid=,command='],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (platform === 'linux') {
|
|
41
|
+
return {
|
|
42
|
+
command: 'ps',
|
|
43
|
+
args: ['-eo', 'pid=,args='],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (platform === 'win32') {
|
|
48
|
+
return {
|
|
49
|
+
command: 'powershell.exe',
|
|
50
|
+
args: ['-NoProfile', '-NonInteractive', '-Command', WINDOWS_PROCESS_QUERY],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseUnityProcesses(platform: NodeJS.Platform, output: string): RawUnityProcess[] {
|
|
58
|
+
if (platform === 'win32') {
|
|
59
|
+
return parseWindowsUnityProcesses(output);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return parsePsUnityProcesses(output);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function tokenizeCommandLine(commandLine: string): string[] {
|
|
66
|
+
const tokens: string[] = [];
|
|
67
|
+
let current = '';
|
|
68
|
+
let inQuotes = false;
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < commandLine.length; i++) {
|
|
71
|
+
const character = commandLine[i];
|
|
72
|
+
|
|
73
|
+
if (character === '"') {
|
|
74
|
+
inQuotes = !inQuotes;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!inQuotes && /\s/.test(character)) {
|
|
79
|
+
if (current.length > 0) {
|
|
80
|
+
tokens.push(current);
|
|
81
|
+
current = '';
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
current += character;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (current.length > 0) {
|
|
90
|
+
tokens.push(current);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return tokens;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function extractUnityProjectPath(commandLine: string): string | null {
|
|
97
|
+
const tokens = tokenizeCommandLine(commandLine);
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
100
|
+
const token = tokens[i].toLowerCase();
|
|
101
|
+
if (token !== '-projectpath') {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const projectPath = tokens[i + 1];
|
|
106
|
+
return projectPath ?? null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function normalizeUnityProjectPath(projectPath: string, platform: NodeJS.Platform): string {
|
|
113
|
+
const normalizedSeparators = projectPath.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
114
|
+
if (platform === 'win32') {
|
|
115
|
+
return normalizedSeparators.toLowerCase();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return normalizedSeparators;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function isUnityProcessForProject(
|
|
122
|
+
commandLine: string,
|
|
123
|
+
projectRoot: string,
|
|
124
|
+
platform: NodeJS.Platform,
|
|
125
|
+
): boolean {
|
|
126
|
+
if (platform !== 'win32') {
|
|
127
|
+
return commandLineContainsProjectRoot(commandLine, projectRoot, platform);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const extractedProjectPath = extractUnityProjectPath(commandLine);
|
|
131
|
+
if (extractedProjectPath === null) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
normalizeUnityProjectPath(extractedProjectPath, platform) ===
|
|
137
|
+
normalizeUnityProjectPath(projectRoot, platform)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function commandLineContainsProjectRoot(
|
|
142
|
+
commandLine: string,
|
|
143
|
+
projectRoot: string,
|
|
144
|
+
platform: NodeJS.Platform,
|
|
145
|
+
): boolean {
|
|
146
|
+
const projectPathFlagIndex = commandLine.toLowerCase().indexOf(' -projectpath');
|
|
147
|
+
if (projectPathFlagIndex === -1) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const normalizedProjectRoot = normalizeUnityProjectPath(projectRoot, platform);
|
|
152
|
+
let projectRootIndex = commandLine.indexOf(normalizedProjectRoot, projectPathFlagIndex);
|
|
153
|
+
|
|
154
|
+
while (projectRootIndex !== -1) {
|
|
155
|
+
const beforeProjectRoot = commandLine[projectRootIndex - 1];
|
|
156
|
+
const projectPathEndIndex = skipTrailingProjectPathSeparators(
|
|
157
|
+
commandLine,
|
|
158
|
+
projectRootIndex + normalizedProjectRoot.length,
|
|
159
|
+
);
|
|
160
|
+
if (
|
|
161
|
+
isProjectPathBoundaryCharacter(beforeProjectRoot) &&
|
|
162
|
+
isProjectPathTerminator(commandLine, projectPathEndIndex)
|
|
163
|
+
) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
projectRootIndex = commandLine.indexOf(normalizedProjectRoot, projectRootIndex + 1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isProjectPathBoundaryCharacter(character: string | undefined): boolean {
|
|
174
|
+
return character === undefined || /\s|["']/.test(character);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function skipTrailingProjectPathSeparators(commandLine: string, startIndex: number): number {
|
|
178
|
+
let index = startIndex;
|
|
179
|
+
|
|
180
|
+
while (readCharacterAt(commandLine, index) === '/') {
|
|
181
|
+
index += 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return index;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isProjectPathTerminator(commandLine: string, projectRootEndIndex: number): boolean {
|
|
188
|
+
const character = readCharacterAt(commandLine, projectRootEndIndex);
|
|
189
|
+
if (character === null) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (character === '"' || character === "'") {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!/\s/.test(character)) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (let i = projectRootEndIndex; i < commandLine.length; i++) {
|
|
202
|
+
const trailingCharacter = readCharacterAt(commandLine, i);
|
|
203
|
+
if (trailingCharacter === null) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (/\s/.test(trailingCharacter)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return trailingCharacter === '-';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function readCharacterAt(value: string, index: number): string | null {
|
|
218
|
+
const character = value.slice(index, index + 1);
|
|
219
|
+
if (character.length === 0) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return character;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function isUnityEditorProcess(commandLine: string, platform: NodeJS.Platform): boolean {
|
|
227
|
+
const lowerCommandLine = commandLine.toLowerCase();
|
|
228
|
+
if (lowerCommandLine.length === 0) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const projectPathFlagIndex = lowerCommandLine.indexOf(' -projectpath');
|
|
233
|
+
const executableSection =
|
|
234
|
+
projectPathFlagIndex === -1
|
|
235
|
+
? lowerCommandLine
|
|
236
|
+
: lowerCommandLine.slice(0, projectPathFlagIndex);
|
|
237
|
+
|
|
238
|
+
if (platform === 'win32') {
|
|
239
|
+
return executableSection.includes('unity.exe');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (platform === 'darwin') {
|
|
243
|
+
return executableSection.includes('/unity.app/contents/macos/unity');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (platform === 'linux') {
|
|
247
|
+
return (
|
|
248
|
+
executableSection.endsWith('/unity') ||
|
|
249
|
+
executableSection.endsWith('/unity-editor') ||
|
|
250
|
+
executableSection.includes('/editor/unity')
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function findRunningUnityProcessForProject(
|
|
258
|
+
projectRoot: string,
|
|
259
|
+
dependencies: UnityProcessDependencies = defaultDependencies,
|
|
260
|
+
): Promise<RunningUnityProcess | null> {
|
|
261
|
+
const unityProcessCommand = buildUnityProcessCommand(dependencies.platform);
|
|
262
|
+
if (unityProcessCommand === null) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const output = await dependencies.runCommand(
|
|
267
|
+
unityProcessCommand.command,
|
|
268
|
+
unityProcessCommand.args,
|
|
269
|
+
);
|
|
270
|
+
const runningProcesses = parseUnityProcesses(dependencies.platform, output);
|
|
271
|
+
const matchingProcess = runningProcesses.find(
|
|
272
|
+
(processInfo) =>
|
|
273
|
+
isUnityEditorProcess(processInfo.commandLine, dependencies.platform) &&
|
|
274
|
+
isUnityProcessForProject(processInfo.commandLine, projectRoot, dependencies.platform),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (matchingProcess === undefined) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
pid: matchingProcess.pid,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function runUnityProcessQuery(command: string, args: string[]): Promise<string> {
|
|
287
|
+
const { stdout } = await execFileAsync(command, args, {
|
|
288
|
+
encoding: 'utf8',
|
|
289
|
+
maxBuffer: 1024 * 1024,
|
|
290
|
+
});
|
|
291
|
+
return stdout;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function parsePsUnityProcesses(output: string): RawUnityProcess[] {
|
|
295
|
+
return output
|
|
296
|
+
.split(/\r?\n/)
|
|
297
|
+
.map((line) => line.trim())
|
|
298
|
+
.filter((line) => line.length > 0)
|
|
299
|
+
.map((line) => {
|
|
300
|
+
const match = line.match(/^(\d+)\s+(.+)$/);
|
|
301
|
+
if (match === null) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
pid: Number.parseInt(match[1], 10),
|
|
307
|
+
commandLine: match[2],
|
|
308
|
+
};
|
|
309
|
+
})
|
|
310
|
+
.filter((processInfo): processInfo is RawUnityProcess => processInfo !== null);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function parseWindowsUnityProcesses(output: string): RawUnityProcess[] {
|
|
314
|
+
const trimmed = output.trim();
|
|
315
|
+
if (trimmed.length === 0) {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const parsed = JSON.parse(trimmed) as WindowsUnityProcessJson | WindowsUnityProcessJson[];
|
|
320
|
+
const processArray = Array.isArray(parsed) ? parsed : [parsed];
|
|
321
|
+
|
|
322
|
+
return processArray.filter(isWindowsUnityProcessWithCommandLine).map((processInfo) => ({
|
|
323
|
+
pid: processInfo.ProcessId,
|
|
324
|
+
commandLine: processInfo.CommandLine,
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface WindowsUnityProcessJson {
|
|
329
|
+
ProcessId: number;
|
|
330
|
+
CommandLine?: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isWindowsUnityProcessWithCommandLine(
|
|
334
|
+
processInfo: WindowsUnityProcessJson,
|
|
335
|
+
): processInfo is WindowsUnityProcessJson & { CommandLine: string } {
|
|
336
|
+
return typeof processInfo.ProcessId === 'number' && typeof processInfo.CommandLine === 'string';
|
|
337
|
+
}
|
package/src/version.ts
CHANGED