launch-unity 0.2.0 → 0.3.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/dist/launch.js +391 -132
- package/dist/quit.js +25 -5
- package/package.json +1 -1
package/dist/launch.js
CHANGED
|
@@ -3,33 +3,88 @@
|
|
|
3
3
|
launch-unity: Open a Unity project with the matching Editor version.
|
|
4
4
|
Platforms: macOS, Windows
|
|
5
5
|
*/
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
7
|
-
import { existsSync, readFileSync,
|
|
6
|
+
import { execFile, spawn } from "node:child_process";
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync } from "node:fs";
|
|
8
|
+
import { rm } from "node:fs/promises";
|
|
8
9
|
import { join, resolve } from "node:path";
|
|
9
|
-
import
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
const UNITY_EXECUTABLE_PATTERN_MAC = /Unity\.app\/Contents\/MacOS\/Unity/i;
|
|
13
|
+
const UNITY_EXECUTABLE_PATTERN_WINDOWS = /Unity\.exe/i;
|
|
14
|
+
const PROJECT_PATH_PATTERN = /-(?:projectPath|projectpath)(?:=|\s+)("[^"]+"|'[^']+'|[^\s"']+)/i;
|
|
15
|
+
const PROCESS_LIST_COMMAND_MAC = "ps";
|
|
16
|
+
const PROCESS_LIST_ARGS_MAC = ["-axo", "pid=,command=", "-ww"];
|
|
17
|
+
const WINDOWS_POWERSHELL = "powershell";
|
|
18
|
+
const UNITY_LOCKFILE_NAME = "UnityLockfile";
|
|
19
|
+
const TEMP_DIRECTORY_NAME = "Temp";
|
|
10
20
|
function parseArgs(argv) {
|
|
11
|
-
const defaultProjectPath = process.cwd();
|
|
12
21
|
const args = argv.slice(2);
|
|
13
22
|
const doubleDashIndex = args.indexOf("--");
|
|
14
23
|
const cliArgs = doubleDashIndex >= 0 ? args.slice(0, doubleDashIndex) : args;
|
|
15
24
|
const unityArgs = doubleDashIndex >= 0 ? args.slice(doubleDashIndex + 1) : [];
|
|
16
25
|
const positionals = [];
|
|
17
|
-
|
|
26
|
+
let maxDepth = 3; // default 3; -1 means unlimited
|
|
27
|
+
for (let i = 0; i < cliArgs.length; i++) {
|
|
28
|
+
const arg = cliArgs[i] ?? "";
|
|
18
29
|
if (arg === "--help" || arg === "-h") {
|
|
19
30
|
printHelp();
|
|
20
31
|
process.exit(0);
|
|
21
32
|
}
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
if (arg.startsWith("--max-depth")) {
|
|
34
|
+
const parts = arg.split("=");
|
|
35
|
+
if (parts.length === 2) {
|
|
36
|
+
const value = Number.parseInt(parts[1] ?? "", 10);
|
|
37
|
+
if (Number.isFinite(value)) {
|
|
38
|
+
maxDepth = value;
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const next = cliArgs[i + 1];
|
|
43
|
+
if (typeof next === "string" && !next.startsWith("-")) {
|
|
44
|
+
const value = Number.parseInt(next, 10);
|
|
45
|
+
if (Number.isFinite(value)) {
|
|
46
|
+
maxDepth = value;
|
|
47
|
+
}
|
|
48
|
+
i += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
24
51
|
continue;
|
|
25
52
|
}
|
|
53
|
+
if (arg.startsWith("-")) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
positionals.push(arg);
|
|
57
|
+
}
|
|
58
|
+
let projectPath;
|
|
59
|
+
let platform;
|
|
60
|
+
if (positionals.length === 0) {
|
|
61
|
+
projectPath = undefined; // trigger search
|
|
62
|
+
platform = undefined;
|
|
63
|
+
}
|
|
64
|
+
else if (positionals.length === 1) {
|
|
65
|
+
const first = positionals[0] ?? "";
|
|
66
|
+
const resolvedFirst = resolve(first);
|
|
67
|
+
if (existsSync(resolvedFirst)) {
|
|
68
|
+
projectPath = resolvedFirst;
|
|
69
|
+
platform = undefined;
|
|
70
|
+
}
|
|
26
71
|
else {
|
|
27
|
-
|
|
72
|
+
// Treat as platform when path does not exist
|
|
73
|
+
projectPath = undefined; // trigger search
|
|
74
|
+
platform = String(first);
|
|
28
75
|
}
|
|
29
76
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
77
|
+
else {
|
|
78
|
+
projectPath = resolve(positionals[0] ?? "");
|
|
79
|
+
platform = String(positionals[1] ?? "");
|
|
80
|
+
}
|
|
81
|
+
const options = { unityArgs, searchMaxDepth: maxDepth };
|
|
82
|
+
if (projectPath !== undefined) {
|
|
83
|
+
options.projectPath = projectPath;
|
|
84
|
+
}
|
|
85
|
+
if (platform !== undefined) {
|
|
86
|
+
options.platform = platform;
|
|
87
|
+
}
|
|
33
88
|
return options;
|
|
34
89
|
}
|
|
35
90
|
function printHelp() {
|
|
@@ -47,7 +102,8 @@ Forwarding:
|
|
|
47
102
|
If UNITY_ARGS includes -buildTarget, the PLATFORM argument is ignored.
|
|
48
103
|
|
|
49
104
|
Flags:
|
|
50
|
-
-h, --help
|
|
105
|
+
-h, --help Show this help message
|
|
106
|
+
--max-depth <N> Search depth when PROJECT_PATH is omitted (default 3, -1 unlimited)
|
|
51
107
|
`;
|
|
52
108
|
process.stdout.write(help);
|
|
53
109
|
}
|
|
@@ -72,8 +128,9 @@ function getUnityPathWindows(version) {
|
|
|
72
128
|
const programFilesX86 = process.env["PROGRAMFILES(X86)"];
|
|
73
129
|
const localAppData = process.env["LOCALAPPDATA"];
|
|
74
130
|
const addCandidate = (base) => {
|
|
75
|
-
if (!base)
|
|
131
|
+
if (!base) {
|
|
76
132
|
return;
|
|
133
|
+
}
|
|
77
134
|
candidates.push(join(base, "Unity", "Hub", "Editor", version, "Editor", "Unity.exe"));
|
|
78
135
|
};
|
|
79
136
|
addCandidate(programFiles);
|
|
@@ -81,8 +138,9 @@ function getUnityPathWindows(version) {
|
|
|
81
138
|
addCandidate(localAppData);
|
|
82
139
|
candidates.push(join("C:\\", "Program Files", "Unity", "Hub", "Editor", version, "Editor", "Unity.exe"));
|
|
83
140
|
for (const candidate of candidates) {
|
|
84
|
-
if (existsSync(candidate))
|
|
141
|
+
if (existsSync(candidate)) {
|
|
85
142
|
return candidate;
|
|
143
|
+
}
|
|
86
144
|
}
|
|
87
145
|
return candidates[0] ?? join("C:\\", "Program Files", "Unity", "Hub", "Editor", version, "Editor", "Unity.exe");
|
|
88
146
|
}
|
|
@@ -101,140 +159,319 @@ function ensureProjectPath(projectPath) {
|
|
|
101
159
|
process.exit(1);
|
|
102
160
|
}
|
|
103
161
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return { rl, close };
|
|
162
|
+
const removeTrailingSeparators = (target) => {
|
|
163
|
+
let trimmed = target;
|
|
164
|
+
while (trimmed.length > 1 && (trimmed.endsWith("/") || trimmed.endsWith("\\"))) {
|
|
165
|
+
trimmed = trimmed.slice(0, -1);
|
|
109
166
|
}
|
|
167
|
+
return trimmed;
|
|
168
|
+
};
|
|
169
|
+
const normalizePath = (target) => {
|
|
170
|
+
const resolvedPath = resolve(target);
|
|
171
|
+
const trimmed = removeTrailingSeparators(resolvedPath);
|
|
172
|
+
return trimmed;
|
|
173
|
+
};
|
|
174
|
+
const toComparablePath = (value) => {
|
|
175
|
+
return value.replace(/\\/g, "/").toLocaleLowerCase();
|
|
176
|
+
};
|
|
177
|
+
const pathsEqual = (left, right) => {
|
|
178
|
+
return toComparablePath(normalizePath(left)) === toComparablePath(normalizePath(right));
|
|
179
|
+
};
|
|
180
|
+
function extractProjectPath(command) {
|
|
181
|
+
const match = command.match(PROJECT_PATH_PATTERN);
|
|
182
|
+
if (!match) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
const raw = match[1];
|
|
186
|
+
if (!raw) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const trimmed = raw.trim();
|
|
190
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
|
191
|
+
return trimmed.slice(1, -1);
|
|
192
|
+
}
|
|
193
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
194
|
+
return trimmed.slice(1, -1);
|
|
195
|
+
}
|
|
196
|
+
return trimmed;
|
|
197
|
+
}
|
|
198
|
+
async function listUnityProcessesMac() {
|
|
199
|
+
let stdout = "";
|
|
110
200
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
201
|
+
const result = await execFileAsync(PROCESS_LIST_COMMAND_MAC, PROCESS_LIST_ARGS_MAC);
|
|
202
|
+
stdout = result.stdout;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
206
|
+
console.error(`Failed to retrieve Unity process list: ${message}`);
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
const lines = stdout
|
|
210
|
+
.split("\n")
|
|
211
|
+
.map((line) => line.trim())
|
|
212
|
+
.filter((line) => line.length > 0);
|
|
213
|
+
const processes = [];
|
|
214
|
+
for (const line of lines) {
|
|
215
|
+
const match = line.match(/^(\d+)\s+(.*)$/);
|
|
216
|
+
if (!match) {
|
|
217
|
+
continue;
|
|
132
218
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const rl = readline.createInterface({ input, output });
|
|
137
|
-
const close = () => {
|
|
138
|
-
rl.close();
|
|
139
|
-
input.destroy();
|
|
140
|
-
output.end();
|
|
141
|
-
};
|
|
142
|
-
return { rl, close };
|
|
219
|
+
const pidValue = Number.parseInt(match[1] ?? "", 10);
|
|
220
|
+
if (!Number.isFinite(pidValue)) {
|
|
221
|
+
continue;
|
|
143
222
|
}
|
|
223
|
+
const command = match[2] ?? "";
|
|
224
|
+
if (!UNITY_EXECUTABLE_PATTERN_MAC.test(command)) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const projectArgument = extractProjectPath(command);
|
|
228
|
+
if (!projectArgument) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
processes.push({
|
|
232
|
+
pid: pidValue,
|
|
233
|
+
projectPath: normalizePath(projectArgument),
|
|
234
|
+
});
|
|
144
235
|
}
|
|
145
|
-
|
|
146
|
-
// fallthrough
|
|
147
|
-
}
|
|
148
|
-
return null;
|
|
236
|
+
return processes;
|
|
149
237
|
}
|
|
150
|
-
async function
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const prompt = createPromptInterface();
|
|
164
|
-
if (!prompt) {
|
|
165
|
-
console.error("UnityLockfile exists. No interactive console available for confirmation.");
|
|
166
|
-
return false;
|
|
238
|
+
async function listUnityProcessesWindows() {
|
|
239
|
+
const scriptLines = [
|
|
240
|
+
"$ErrorActionPreference = 'Stop'",
|
|
241
|
+
"$processes = Get-CimInstance Win32_Process -Filter \"Name = 'Unity.exe'\" | Where-Object { $_.CommandLine }",
|
|
242
|
+
"foreach ($process in $processes) {",
|
|
243
|
+
" $commandLine = $process.CommandLine -replace \"`r\", ' ' -replace \"`n\", ' '",
|
|
244
|
+
" Write-Output (\"{0}|{1}\" -f $process.ProcessId, $commandLine)",
|
|
245
|
+
"}",
|
|
246
|
+
];
|
|
247
|
+
let stdout = "";
|
|
248
|
+
try {
|
|
249
|
+
const result = await execFileAsync(WINDOWS_POWERSHELL, ["-NoProfile", "-Command", scriptLines.join("\n")]);
|
|
250
|
+
stdout = result.stdout ?? "";
|
|
167
251
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
252
|
+
catch (error) {
|
|
253
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
254
|
+
console.error(`Failed to retrieve Unity process list on Windows: ${message}`);
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
const lines = stdout
|
|
258
|
+
.split("\n")
|
|
259
|
+
.map((line) => line.trim())
|
|
260
|
+
.filter((line) => line.length > 0);
|
|
261
|
+
const processes = [];
|
|
262
|
+
for (const line of lines) {
|
|
263
|
+
const delimiterIndex = line.indexOf("|");
|
|
264
|
+
if (delimiterIndex < 0) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const pidText = line.slice(0, delimiterIndex).trim();
|
|
268
|
+
const command = line.slice(delimiterIndex + 1).trim();
|
|
269
|
+
const pidValue = Number.parseInt(pidText, 10);
|
|
270
|
+
if (!Number.isFinite(pidValue)) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (!UNITY_EXECUTABLE_PATTERN_WINDOWS.test(command)) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const projectArgument = extractProjectPath(command);
|
|
277
|
+
if (!projectArgument) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
processes.push({
|
|
281
|
+
pid: pidValue,
|
|
282
|
+
projectPath: normalizePath(projectArgument),
|
|
171
283
|
});
|
|
172
|
-
});
|
|
173
|
-
prompt.close();
|
|
174
|
-
if (!confirmed) {
|
|
175
|
-
console.log("Aborted by user.");
|
|
176
|
-
return false;
|
|
177
284
|
}
|
|
178
|
-
|
|
179
|
-
console.log("Deleted UnityLockfile. Continuing launch.");
|
|
180
|
-
return true;
|
|
285
|
+
return processes;
|
|
181
286
|
}
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
287
|
+
async function listUnityProcesses() {
|
|
288
|
+
if (process.platform === "darwin") {
|
|
289
|
+
return await listUnityProcessesMac();
|
|
290
|
+
}
|
|
291
|
+
if (process.platform === "win32") {
|
|
292
|
+
return await listUnityProcessesWindows();
|
|
293
|
+
}
|
|
294
|
+
return [];
|
|
185
295
|
}
|
|
186
|
-
async function
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
296
|
+
async function findRunningUnityProcess(projectPath) {
|
|
297
|
+
const normalizedTarget = normalizePath(projectPath);
|
|
298
|
+
const processes = await listUnityProcesses();
|
|
299
|
+
return processes.find((candidate) => pathsEqual(candidate.projectPath, normalizedTarget));
|
|
300
|
+
}
|
|
301
|
+
async function focusUnityProcess(pid) {
|
|
302
|
+
if (process.platform === "darwin") {
|
|
303
|
+
await focusUnityProcessMac(pid);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (process.platform === "win32") {
|
|
307
|
+
await focusUnityProcessWindows(pid);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function focusUnityProcessMac(pid) {
|
|
311
|
+
const script = `tell application "System Events" to set frontmost of (first process whose unix id is ${pid}) to true`;
|
|
312
|
+
try {
|
|
313
|
+
await execFileAsync("osascript", ["-e", script]);
|
|
314
|
+
console.log("Brought existing Unity to the front.");
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
318
|
+
console.warn(`Failed to bring Unity to front: ${message}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function focusUnityProcessWindows(pid) {
|
|
322
|
+
const addTypeLines = [
|
|
323
|
+
"Add-Type -TypeDefinition @\"",
|
|
324
|
+
"using System;",
|
|
325
|
+
"using System.Runtime.InteropServices;",
|
|
326
|
+
"public static class Win32Interop {",
|
|
327
|
+
" [DllImport(\"user32.dll\")] public static extern bool SetForegroundWindow(IntPtr hWnd);",
|
|
328
|
+
" [DllImport(\"user32.dll\")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);",
|
|
329
|
+
"}",
|
|
330
|
+
"\"@",
|
|
331
|
+
];
|
|
332
|
+
const scriptLines = [
|
|
333
|
+
"$ErrorActionPreference = 'Stop'",
|
|
334
|
+
...addTypeLines,
|
|
335
|
+
`try { $process = Get-Process -Id ${pid} -ErrorAction Stop } catch { return }`,
|
|
336
|
+
"$handle = $process.MainWindowHandle",
|
|
337
|
+
"if ($handle -eq 0) { return }",
|
|
338
|
+
"[Win32Interop]::ShowWindowAsync($handle, 9) | Out-Null",
|
|
339
|
+
"[Win32Interop]::SetForegroundWindow($handle) | Out-Null",
|
|
340
|
+
];
|
|
341
|
+
try {
|
|
342
|
+
await execFileAsync(WINDOWS_POWERSHELL, ["-NoProfile", "-Command", scriptLines.join("\n")]);
|
|
343
|
+
console.log("Brought existing Unity to the front.");
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
347
|
+
console.warn(`Failed to bring Unity to front on Windows: ${message}`);
|
|
348
|
+
}
|
|
220
349
|
}
|
|
221
|
-
async function
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
350
|
+
async function handleStaleLockfile(projectPath) {
|
|
351
|
+
const tempDirectoryPath = join(projectPath, TEMP_DIRECTORY_NAME);
|
|
352
|
+
const lockfilePath = join(tempDirectoryPath, UNITY_LOCKFILE_NAME);
|
|
353
|
+
if (!existsSync(lockfilePath)) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
console.log(`UnityLockfile found without active Unity process: ${lockfilePath}`);
|
|
357
|
+
console.log("Assuming previous crash. Cleaning Temp directory and continuing launch.");
|
|
358
|
+
try {
|
|
359
|
+
await rm(tempDirectoryPath, { recursive: true, force: true });
|
|
360
|
+
console.log("Deleted Temp directory.");
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
364
|
+
console.warn(`Failed to delete Temp directory: ${message}`);
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
await rm(lockfilePath, { force: true });
|
|
368
|
+
console.log("Deleted UnityLockfile.");
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
372
|
+
console.warn(`Failed to delete UnityLockfile: ${message}`);
|
|
373
|
+
}
|
|
228
374
|
}
|
|
229
375
|
function hasBuildTargetArg(unityArgs) {
|
|
230
376
|
for (const arg of unityArgs) {
|
|
231
|
-
if (arg === "-buildTarget")
|
|
377
|
+
if (arg === "-buildTarget") {
|
|
232
378
|
return true;
|
|
233
|
-
|
|
379
|
+
}
|
|
380
|
+
if (arg.startsWith("-buildTarget=")) {
|
|
234
381
|
return true;
|
|
382
|
+
}
|
|
235
383
|
}
|
|
236
384
|
return false;
|
|
237
385
|
}
|
|
386
|
+
const EXCLUDED_DIR_NAMES = new Set([
|
|
387
|
+
"library",
|
|
388
|
+
"temp",
|
|
389
|
+
"logs",
|
|
390
|
+
"obj",
|
|
391
|
+
".git",
|
|
392
|
+
"node_modules",
|
|
393
|
+
".idea",
|
|
394
|
+
".vscode",
|
|
395
|
+
".vs",
|
|
396
|
+
]);
|
|
397
|
+
function isUnityProjectRoot(candidateDir) {
|
|
398
|
+
const versionFile = join(candidateDir, "ProjectSettings", "ProjectVersion.txt");
|
|
399
|
+
const hasVersion = existsSync(versionFile);
|
|
400
|
+
if (!hasVersion) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
const libraryDir = join(candidateDir, "Library");
|
|
404
|
+
return existsSync(libraryDir);
|
|
405
|
+
}
|
|
406
|
+
function listSubdirectoriesSorted(dir) {
|
|
407
|
+
let entries = [];
|
|
408
|
+
try {
|
|
409
|
+
const dirents = readdirSync(dir, { withFileTypes: true });
|
|
410
|
+
const subdirs = dirents
|
|
411
|
+
.filter((d) => d.isDirectory())
|
|
412
|
+
.map((d) => d.name)
|
|
413
|
+
.filter((name) => !EXCLUDED_DIR_NAMES.has(name.toLocaleLowerCase()));
|
|
414
|
+
subdirs.sort((a, b) => a.localeCompare(b));
|
|
415
|
+
entries = subdirs.map((name) => join(dir, name));
|
|
416
|
+
}
|
|
417
|
+
catch (_err) {
|
|
418
|
+
// Ignore directories we cannot read
|
|
419
|
+
entries = [];
|
|
420
|
+
}
|
|
421
|
+
return entries;
|
|
422
|
+
}
|
|
423
|
+
function findUnityProjectBfs(rootDir, maxDepth) {
|
|
424
|
+
const queue = [];
|
|
425
|
+
let rootCanonical;
|
|
426
|
+
try {
|
|
427
|
+
rootCanonical = realpathSync(rootDir);
|
|
428
|
+
}
|
|
429
|
+
catch (_err) {
|
|
430
|
+
rootCanonical = rootDir;
|
|
431
|
+
}
|
|
432
|
+
queue.push({ dir: rootCanonical, depth: 0 });
|
|
433
|
+
const visited = new Set([toComparablePath(normalizePath(rootCanonical))]);
|
|
434
|
+
while (queue.length > 0) {
|
|
435
|
+
const current = queue.shift();
|
|
436
|
+
if (!current) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const { dir, depth } = current;
|
|
440
|
+
if (isUnityProjectRoot(dir)) {
|
|
441
|
+
return normalizePath(dir);
|
|
442
|
+
}
|
|
443
|
+
const canDescend = maxDepth === -1 || depth < maxDepth;
|
|
444
|
+
if (!canDescend) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const children = listSubdirectoriesSorted(dir);
|
|
448
|
+
for (const child of children) {
|
|
449
|
+
let childCanonical = child;
|
|
450
|
+
try {
|
|
451
|
+
const stat = lstatSync(child);
|
|
452
|
+
if (stat.isSymbolicLink()) {
|
|
453
|
+
try {
|
|
454
|
+
childCanonical = realpathSync(child);
|
|
455
|
+
}
|
|
456
|
+
catch (_e) {
|
|
457
|
+
// Broken symlink: skip
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch (_err) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const key = toComparablePath(normalizePath(childCanonical));
|
|
466
|
+
if (visited.has(key)) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
visited.add(key);
|
|
470
|
+
queue.push({ dir: childCanonical, depth: depth + 1 });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
238
475
|
function launch(opts) {
|
|
239
476
|
const { projectPath, platform, unityArgs } = opts;
|
|
240
477
|
const unityVersion = getUnityVersion(projectPath);
|
|
@@ -260,13 +497,35 @@ function launch(opts) {
|
|
|
260
497
|
}
|
|
261
498
|
async function main() {
|
|
262
499
|
const options = parseArgs(process.argv);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
500
|
+
let resolvedProjectPath = options.projectPath;
|
|
501
|
+
if (!resolvedProjectPath) {
|
|
502
|
+
const searchRoot = process.cwd();
|
|
503
|
+
const depthInfo = options.searchMaxDepth === -1 ? "unlimited" : String(options.searchMaxDepth);
|
|
504
|
+
console.log(`No PROJECT_PATH provided. Searching under ${searchRoot} (max-depth: ${depthInfo})...`);
|
|
505
|
+
const found = findUnityProjectBfs(searchRoot, options.searchMaxDepth);
|
|
506
|
+
if (!found) {
|
|
507
|
+
console.error(`Error: Unity project not found under ${searchRoot}.`);
|
|
508
|
+
process.exit(1);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
console.log(`Selected project: ${found}`);
|
|
512
|
+
resolvedProjectPath = found;
|
|
513
|
+
}
|
|
514
|
+
ensureProjectPath(resolvedProjectPath);
|
|
515
|
+
const runningProcess = await findRunningUnityProcess(resolvedProjectPath);
|
|
516
|
+
if (runningProcess) {
|
|
517
|
+
console.log(`Unity process already running for project: ${resolvedProjectPath} (PID: ${runningProcess.pid})`);
|
|
518
|
+
await focusUnityProcess(runningProcess.pid);
|
|
266
519
|
process.exit(0);
|
|
267
520
|
return;
|
|
268
521
|
}
|
|
269
|
-
|
|
522
|
+
await handleStaleLockfile(resolvedProjectPath);
|
|
523
|
+
const resolved = {
|
|
524
|
+
projectPath: resolvedProjectPath,
|
|
525
|
+
platform: options.platform,
|
|
526
|
+
unityArgs: options.unityArgs,
|
|
527
|
+
};
|
|
528
|
+
launch(resolved);
|
|
270
529
|
}
|
|
271
530
|
main().catch((error) => {
|
|
272
531
|
console.error(error);
|
package/dist/quit.js
CHANGED
|
@@ -4,13 +4,17 @@
|
|
|
4
4
|
Platforms: macOS, Windows
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { rm } from "node:fs/promises";
|
|
7
8
|
import { join, resolve } from "node:path";
|
|
9
|
+
const TEMP_DIRECTORY_NAME = "Temp";
|
|
8
10
|
function parseArgs(argv) {
|
|
9
11
|
const defaultProjectPath = process.cwd();
|
|
12
|
+
const defaultTimeoutMs = 15000;
|
|
13
|
+
const defaultForce = false;
|
|
10
14
|
const args = argv.slice(2);
|
|
11
15
|
let projectPath = defaultProjectPath;
|
|
12
|
-
let timeoutMs =
|
|
13
|
-
let force =
|
|
16
|
+
let timeoutMs = defaultTimeoutMs;
|
|
17
|
+
let force = defaultForce;
|
|
14
18
|
for (let i = 0; i < args.length; i++) {
|
|
15
19
|
const arg = args[i] ?? "";
|
|
16
20
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -27,7 +31,8 @@ function parseArgs(argv) {
|
|
|
27
31
|
console.error("Error: --timeout requires a millisecond value");
|
|
28
32
|
process.exit(1);
|
|
29
33
|
}
|
|
30
|
-
const
|
|
34
|
+
const parsedValue = Number(value);
|
|
35
|
+
const parsed = parsedValue;
|
|
31
36
|
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
32
37
|
console.error("Error: --timeout must be a non-negative number (milliseconds)");
|
|
33
38
|
process.exit(1);
|
|
@@ -106,7 +111,8 @@ function isProcessAlive(pid) {
|
|
|
106
111
|
}
|
|
107
112
|
async function waitForExit(pid, timeoutMs) {
|
|
108
113
|
const start = Date.now();
|
|
109
|
-
const
|
|
114
|
+
const stepIntervalMs = 200;
|
|
115
|
+
const stepMs = stepIntervalMs;
|
|
110
116
|
while (Date.now() - start < timeoutMs) {
|
|
111
117
|
if (!isProcessAlive(pid))
|
|
112
118
|
return true;
|
|
@@ -119,7 +125,7 @@ async function quitByPid(pid, force, timeoutMs) {
|
|
|
119
125
|
try {
|
|
120
126
|
process.kill(pid, "SIGTERM");
|
|
121
127
|
}
|
|
122
|
-
catch
|
|
128
|
+
catch {
|
|
123
129
|
// If process already exited, consider it success
|
|
124
130
|
if (!isProcessAlive(pid))
|
|
125
131
|
return true;
|
|
@@ -142,6 +148,19 @@ async function quitByPid(pid, force, timeoutMs) {
|
|
|
142
148
|
// Give a short moment after force
|
|
143
149
|
return await waitForExit(pid, 2000);
|
|
144
150
|
}
|
|
151
|
+
async function removeTempDirectory(projectPath) {
|
|
152
|
+
const tempDirectoryPath = join(projectPath, TEMP_DIRECTORY_NAME);
|
|
153
|
+
if (!existsSync(tempDirectoryPath))
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
await rm(tempDirectoryPath, { recursive: true, force: true });
|
|
157
|
+
console.log(`Deleted Temp directory: ${tempDirectoryPath}`);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
161
|
+
console.warn(`Failed to delete Temp directory: ${message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
145
164
|
async function main() {
|
|
146
165
|
const options = parseArgs(process.argv);
|
|
147
166
|
ensureProjectPath(options.projectPath);
|
|
@@ -159,6 +178,7 @@ async function main() {
|
|
|
159
178
|
return;
|
|
160
179
|
}
|
|
161
180
|
console.log("Unity has exited.");
|
|
181
|
+
await removeTempDirectory(options.projectPath);
|
|
162
182
|
}
|
|
163
183
|
main().catch((error) => {
|
|
164
184
|
console.error(error);
|