@steipete/oracle 0.9.0 → 0.11.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/LICENSE +1 -1
- package/README.md +107 -49
- package/dist/bin/oracle-cli.js +551 -410
- package/dist/bin/oracle-mcp.js +2 -2
- package/dist/bin/oracle.js +165 -279
- package/dist/scripts/agent-send.js +31 -31
- package/dist/scripts/check.js +6 -6
- package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
- package/dist/scripts/docs-list.js +30 -30
- package/dist/scripts/git-policy.js +25 -23
- package/dist/scripts/run-cli.js +8 -8
- package/dist/scripts/runner.js +203 -195
- package/dist/scripts/test-browser.js +21 -18
- package/dist/scripts/test-remote-chrome.js +20 -20
- package/dist/src/bridge/connection.js +18 -18
- package/dist/src/bridge/userConfigFile.js +7 -7
- package/dist/src/browser/actions/archiveConversation.js +224 -0
- package/dist/src/browser/actions/assistantResponse.js +175 -101
- package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
- package/dist/src/browser/actions/attachments.js +246 -150
- package/dist/src/browser/actions/deepResearch.js +662 -0
- package/dist/src/browser/actions/domEvents.js +2 -2
- package/dist/src/browser/actions/modelSelection.js +342 -119
- package/dist/src/browser/actions/navigation.js +183 -137
- package/dist/src/browser/actions/projectSources.js +491 -0
- package/dist/src/browser/actions/promptComposer.js +152 -91
- package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
- package/dist/src/browser/actions/thinkingStatus.js +391 -0
- package/dist/src/browser/actions/thinkingTime.js +207 -110
- package/dist/src/browser/artifacts.js +150 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptImages.js +315 -0
- package/dist/src/browser/chromeLifecycle.js +276 -63
- package/dist/src/browser/config.js +59 -16
- package/dist/src/browser/constants.js +25 -12
- package/dist/src/browser/controlPlan.js +81 -0
- package/dist/src/browser/cookies.js +19 -19
- package/dist/src/browser/detect.js +250 -77
- package/dist/src/browser/domDebug.js +50 -1
- package/dist/src/browser/index.js +1559 -692
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/modelStrategy.js +1 -1
- package/dist/src/browser/pageActions.js +5 -5
- package/dist/src/browser/policies.js +16 -13
- package/dist/src/browser/profileState.js +127 -42
- package/dist/src/browser/projectSourcesRunner.js +366 -0
- package/dist/src/browser/prompt.js +72 -42
- package/dist/src/browser/promptSummary.js +5 -5
- package/dist/src/browser/providerDomFlow.js +1 -1
- package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/reattach.js +178 -73
- package/dist/src/browser/reattachHelpers.js +32 -27
- package/dist/src/browser/sessionRunner.js +89 -25
- package/dist/src/browser/tabLeaseRegistry.js +182 -0
- package/dist/src/browser/utils.js +9 -9
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/bridge/claudeConfig.js +24 -20
- package/dist/src/cli/bridge/client.js +28 -20
- package/dist/src/cli/bridge/codexConfig.js +16 -16
- package/dist/src/cli/bridge/doctor.js +47 -39
- package/dist/src/cli/bridge/host.js +58 -56
- package/dist/src/cli/browserConfig.js +102 -48
- package/dist/src/cli/browserDefaults.js +51 -26
- package/dist/src/cli/browserTabs.js +228 -0
- package/dist/src/cli/bundleWarnings.js +1 -1
- package/dist/src/cli/clipboard.js +11 -2
- package/dist/src/cli/detach.js +2 -2
- package/dist/src/cli/dryRun.js +62 -26
- package/dist/src/cli/duplicatePromptGuard.js +12 -4
- package/dist/src/cli/engine.js +9 -9
- package/dist/src/cli/errorUtils.js +1 -1
- package/dist/src/cli/fileSize.js +3 -3
- package/dist/src/cli/format.js +2 -2
- package/dist/src/cli/help.js +28 -28
- package/dist/src/cli/hiddenAliases.js +3 -3
- package/dist/src/cli/markdownBundle.js +7 -7
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +131 -106
- package/dist/src/cli/oscUtils.js +1 -1
- package/dist/src/cli/projectSources.js +116 -0
- package/dist/src/cli/promptRequirement.js +2 -2
- package/dist/src/cli/renderOutput.js +1 -1
- package/dist/src/cli/rootAlias.js +1 -1
- package/dist/src/cli/runOptions.js +32 -28
- package/dist/src/cli/sessionCommand.js +82 -21
- package/dist/src/cli/sessionDisplay.js +213 -87
- package/dist/src/cli/sessionLineage.js +6 -2
- package/dist/src/cli/sessionRunner.js +149 -95
- package/dist/src/cli/sessionTable.js +26 -23
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +139 -128
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +19 -15
- package/dist/src/gemini-web/client.js +76 -70
- package/dist/src/gemini-web/executionMode.js +6 -8
- package/dist/src/gemini-web/executor.js +98 -93
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/consultPresets.js +19 -0
- package/dist/src/mcp/server.js +18 -12
- package/dist/src/mcp/tools/consult.js +246 -67
- package/dist/src/mcp/tools/projectSources.js +123 -0
- package/dist/src/mcp/tools/sessionResources.js +12 -12
- package/dist/src/mcp/tools/sessions.js +26 -17
- package/dist/src/mcp/types.js +12 -5
- package/dist/src/mcp/utils.js +21 -8
- package/dist/src/oracle/background.js +15 -15
- package/dist/src/oracle/claude.js +53 -25
- package/dist/src/oracle/client.js +50 -41
- package/dist/src/oracle/config.js +96 -66
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +55 -46
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -33
- package/dist/src/oracle/logging.js +7 -7
- package/dist/src/oracle/markdown.js +28 -28
- package/dist/src/oracle/modelResolver.js +16 -16
- package/dist/src/oracle/multiModelRunner.js +12 -12
- package/dist/src/oracle/oscProgress.js +8 -8
- package/dist/src/oracle/promptAssembly.js +6 -3
- package/dist/src/oracle/request.js +16 -13
- package/dist/src/oracle/run.js +160 -135
- package/dist/src/oracle/runUtils.js +8 -5
- package/dist/src/oracle/tokenEstimate.js +6 -6
- package/dist/src/oracle/tokenStats.js +5 -5
- package/dist/src/oracle/tokenStringifier.js +5 -5
- package/dist/src/oracle.js +12 -12
- package/dist/src/oracleHome.js +3 -3
- package/dist/src/projectSources/plan.js +27 -0
- package/dist/src/projectSources/url.js +23 -0
- package/dist/src/remote/client.js +25 -25
- package/dist/src/remote/health.js +20 -20
- package/dist/src/remote/remoteServiceConfig.js +9 -9
- package/dist/src/remote/server.js +129 -118
- package/dist/src/sessionManager.js +78 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +67 -62
- package/vendor/oracle-notifier/README.md +2 -0
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/profileSync.js +0 -141
- /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import { randomUUID } from
|
|
3
|
-
import { mkdir, readFile, rm, writeFile } from
|
|
4
|
-
import { execFile } from
|
|
5
|
-
import { promisify } from
|
|
6
|
-
import { delay } from
|
|
7
|
-
const DEVTOOLS_ACTIVE_PORT_FILENAME =
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { delay } from "./utils.js";
|
|
7
|
+
const DEVTOOLS_ACTIVE_PORT_FILENAME = "DevToolsActivePort";
|
|
8
8
|
const DEVTOOLS_ACTIVE_PORT_RELATIVE_PATHS = [
|
|
9
9
|
DEVTOOLS_ACTIVE_PORT_FILENAME,
|
|
10
|
-
path.join(
|
|
10
|
+
path.join("Default", DEVTOOLS_ACTIVE_PORT_FILENAME),
|
|
11
11
|
];
|
|
12
|
-
const CHROME_PID_FILENAME =
|
|
13
|
-
const ORACLE_PROFILE_LOCK_FILENAME =
|
|
12
|
+
const CHROME_PID_FILENAME = "chrome.pid";
|
|
13
|
+
const ORACLE_PROFILE_LOCK_FILENAME = "oracle-automation.lock";
|
|
14
14
|
const execFileAsync = promisify(execFile);
|
|
15
15
|
export function getDevToolsActivePortPaths(userDataDir) {
|
|
16
16
|
return DEVTOOLS_ACTIVE_PORT_RELATIVE_PATHS.map((relative) => path.join(userDataDir, relative));
|
|
@@ -18,9 +18,9 @@ export function getDevToolsActivePortPaths(userDataDir) {
|
|
|
18
18
|
export async function readDevToolsPort(userDataDir) {
|
|
19
19
|
for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
|
|
20
20
|
try {
|
|
21
|
-
const raw = await readFile(candidate,
|
|
21
|
+
const raw = await readFile(candidate, "utf8");
|
|
22
22
|
const firstLine = raw.split(/\r?\n/u)[0]?.trim();
|
|
23
|
-
const port = Number.parseInt(firstLine ??
|
|
23
|
+
const port = Number.parseInt(firstLine ?? "", 10);
|
|
24
24
|
if (Number.isFinite(port)) {
|
|
25
25
|
return port;
|
|
26
26
|
}
|
|
@@ -36,7 +36,7 @@ export async function writeDevToolsActivePort(userDataDir, port) {
|
|
|
36
36
|
for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
|
|
37
37
|
try {
|
|
38
38
|
await mkdir(path.dirname(candidate), { recursive: true });
|
|
39
|
-
await writeFile(candidate, contents,
|
|
39
|
+
await writeFile(candidate, contents, "utf8");
|
|
40
40
|
}
|
|
41
41
|
catch {
|
|
42
42
|
// best effort
|
|
@@ -46,7 +46,7 @@ export async function writeDevToolsActivePort(userDataDir, port) {
|
|
|
46
46
|
export async function readChromePid(userDataDir) {
|
|
47
47
|
const pidPath = path.join(userDataDir, CHROME_PID_FILENAME);
|
|
48
48
|
try {
|
|
49
|
-
const raw = (await readFile(pidPath,
|
|
49
|
+
const raw = (await readFile(pidPath, "utf8")).trim();
|
|
50
50
|
const pid = Number.parseInt(raw, 10);
|
|
51
51
|
if (!Number.isFinite(pid) || pid <= 0) {
|
|
52
52
|
return null;
|
|
@@ -63,12 +63,83 @@ export async function writeChromePid(userDataDir, pid) {
|
|
|
63
63
|
const pidPath = path.join(userDataDir, CHROME_PID_FILENAME);
|
|
64
64
|
try {
|
|
65
65
|
await mkdir(path.dirname(pidPath), { recursive: true });
|
|
66
|
-
await writeFile(pidPath, `${Math.trunc(pid)}\n`,
|
|
66
|
+
await writeFile(pidPath, `${Math.trunc(pid)}\n`, "utf8");
|
|
67
67
|
}
|
|
68
68
|
catch {
|
|
69
69
|
// best effort
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
export async function findRunningChromeDebugTargetForProfile(userDataDir) {
|
|
73
|
+
if (process.platform === "win32") {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const { stdout } = await execFileAsync("ps", ["-ax", "-o", "pid=", "-o", "command="], {
|
|
78
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
79
|
+
});
|
|
80
|
+
return findChromeDebugTargetForProfileFromProcessList(String(stdout ?? ""), userDataDir);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function findChromeDebugTargetForProfileFromProcessList(processList, userDataDir) {
|
|
87
|
+
for (const line of processList.split("\n")) {
|
|
88
|
+
const match = line.match(/^\s*(\d+)\s+(.+)$/);
|
|
89
|
+
if (!match)
|
|
90
|
+
continue;
|
|
91
|
+
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
92
|
+
const command = match[2] ?? "";
|
|
93
|
+
const lower = command.toLowerCase();
|
|
94
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
95
|
+
continue;
|
|
96
|
+
if (!lower.includes("chrome") && !lower.includes("chromium"))
|
|
97
|
+
continue;
|
|
98
|
+
if (!lower.includes("user-data-dir") || !command.includes(userDataDir))
|
|
99
|
+
continue;
|
|
100
|
+
const portMatch = command.match(/--remote-debugging-port(?:=|\s+)(\d+)/);
|
|
101
|
+
const port = Number.parseInt(portMatch?.[1] ?? "", 10);
|
|
102
|
+
if (!Number.isFinite(port) || port <= 0)
|
|
103
|
+
continue;
|
|
104
|
+
return { pid, port };
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
export function findChromeDebugTargetForProfileFromProcessListForTest(processList, userDataDir) {
|
|
109
|
+
return findChromeDebugTargetForProfileFromProcessList(processList, userDataDir);
|
|
110
|
+
}
|
|
111
|
+
export async function terminateRecordedChromeForProfile(userDataDir, logger) {
|
|
112
|
+
const pid = await readChromePid(userDataDir);
|
|
113
|
+
if (!pid || !isProcessAlive(pid)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const command = await readProcessCommand(pid);
|
|
117
|
+
if (!isChromeCommandForUserDataDir(command, userDataDir)) {
|
|
118
|
+
logger?.(`Recorded Chrome pid ${pid} does not match ${userDataDir}; skipping termination`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, "SIGTERM");
|
|
123
|
+
logger?.(`Terminated shared manual-login Chrome pid ${pid}`);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
128
|
+
logger?.(`Failed to terminate shared manual-login Chrome pid ${pid}: ${message}`);
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function isChromeCommandForUserDataDir(command, userDataDir) {
|
|
133
|
+
if (!command)
|
|
134
|
+
return false;
|
|
135
|
+
const lower = command.toLowerCase();
|
|
136
|
+
return ((lower.includes("chrome") || lower.includes("chromium")) &&
|
|
137
|
+
lower.includes("user-data-dir") &&
|
|
138
|
+
command.includes(userDataDir));
|
|
139
|
+
}
|
|
140
|
+
export function isChromeCommandForUserDataDirForTest(command, userDataDir) {
|
|
141
|
+
return isChromeCommandForUserDataDir(command, userDataDir);
|
|
142
|
+
}
|
|
72
143
|
export function isProcessAlive(pid) {
|
|
73
144
|
if (!Number.isFinite(pid) || pid <= 0)
|
|
74
145
|
return false;
|
|
@@ -78,7 +149,10 @@ export function isProcessAlive(pid) {
|
|
|
78
149
|
}
|
|
79
150
|
catch (error) {
|
|
80
151
|
// EPERM means "exists but no permission"; treat as alive.
|
|
81
|
-
if (error &&
|
|
152
|
+
if (error &&
|
|
153
|
+
typeof error === "object" &&
|
|
154
|
+
"code" in error &&
|
|
155
|
+
error.code === "EPERM") {
|
|
82
156
|
return true;
|
|
83
157
|
}
|
|
84
158
|
return false;
|
|
@@ -91,7 +165,7 @@ function parseProfileRunLock(payload) {
|
|
|
91
165
|
const parsed = JSON.parse(payload);
|
|
92
166
|
if (!Number.isFinite(parsed.pid) || parsed.pid <= 0)
|
|
93
167
|
return null;
|
|
94
|
-
if (!parsed.lockId || typeof parsed.lockId !==
|
|
168
|
+
if (!parsed.lockId || typeof parsed.lockId !== "string")
|
|
95
169
|
return null;
|
|
96
170
|
return parsed;
|
|
97
171
|
}
|
|
@@ -104,7 +178,7 @@ export async function acquireProfileRunLock(userDataDir, options) {
|
|
|
104
178
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
105
179
|
return null;
|
|
106
180
|
}
|
|
107
|
-
const pollMs = typeof options.pollMs ===
|
|
181
|
+
const pollMs = typeof options.pollMs === "number" && Number.isFinite(options.pollMs) && options.pollMs > 0
|
|
108
182
|
? options.pollMs
|
|
109
183
|
: 1000;
|
|
110
184
|
const lockPath = path.join(userDataDir, ORACLE_PROFILE_LOCK_FILENAME);
|
|
@@ -120,7 +194,7 @@ export async function acquireProfileRunLock(userDataDir, options) {
|
|
|
120
194
|
sessionId: options.sessionId,
|
|
121
195
|
};
|
|
122
196
|
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
123
|
-
await writeFile(lockPath, JSON.stringify(payload), { encoding:
|
|
197
|
+
await writeFile(lockPath, JSON.stringify(payload), { encoding: "utf8", flag: "wx" });
|
|
124
198
|
options.logger?.(`Acquired Oracle profile lock at ${lockPath}`);
|
|
125
199
|
return {
|
|
126
200
|
path: lockPath,
|
|
@@ -130,16 +204,16 @@ export async function acquireProfileRunLock(userDataDir, options) {
|
|
|
130
204
|
}
|
|
131
205
|
catch (error) {
|
|
132
206
|
const code = error.code;
|
|
133
|
-
if (code !==
|
|
207
|
+
if (code !== "EEXIST") {
|
|
134
208
|
throw error;
|
|
135
209
|
}
|
|
136
|
-
let existing = parseProfileRunLock(await readFile(lockPath,
|
|
210
|
+
let existing = parseProfileRunLock(await readFile(lockPath, "utf8").catch(() => null));
|
|
137
211
|
if (!existing) {
|
|
138
212
|
// Likely partial write / corruption; re-read once, then delete (user preference: delete unreadable lockfiles).
|
|
139
213
|
await delay(200);
|
|
140
|
-
existing = parseProfileRunLock(await readFile(lockPath,
|
|
214
|
+
existing = parseProfileRunLock(await readFile(lockPath, "utf8").catch(() => null));
|
|
141
215
|
if (!existing) {
|
|
142
|
-
options.logger?.(
|
|
216
|
+
options.logger?.("Oracle profile lock unreadable; deleting lockfile.");
|
|
143
217
|
await rm(lockPath, { force: true }).catch(() => undefined);
|
|
144
218
|
continue;
|
|
145
219
|
}
|
|
@@ -163,7 +237,7 @@ export async function acquireProfileRunLock(userDataDir, options) {
|
|
|
163
237
|
}
|
|
164
238
|
export async function releaseProfileRunLock(lockPath, lockId, logger) {
|
|
165
239
|
try {
|
|
166
|
-
const existing = parseProfileRunLock(await readFile(lockPath,
|
|
240
|
+
const existing = parseProfileRunLock(await readFile(lockPath, "utf8").catch(() => null));
|
|
167
241
|
if (!existing || existing.lockId !== lockId) {
|
|
168
242
|
return;
|
|
169
243
|
}
|
|
@@ -174,7 +248,7 @@ export async function releaseProfileRunLock(lockPath, lockId, logger) {
|
|
|
174
248
|
// best effort
|
|
175
249
|
}
|
|
176
250
|
}
|
|
177
|
-
export async function verifyDevToolsReachable({ port, host =
|
|
251
|
+
export async function verifyDevToolsReachable({ port, host = "127.0.0.1", attempts = 3, timeoutMs = 3000, }) {
|
|
178
252
|
const versionUrl = `http://${host}:${port}/json/version`;
|
|
179
253
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
180
254
|
try {
|
|
@@ -196,12 +270,9 @@ export async function verifyDevToolsReachable({ port, host = '127.0.0.1', attemp
|
|
|
196
270
|
return { ok: false, error: message };
|
|
197
271
|
}
|
|
198
272
|
}
|
|
199
|
-
return { ok: false, error:
|
|
273
|
+
return { ok: false, error: "unreachable" };
|
|
200
274
|
}
|
|
201
275
|
export async function shouldCleanupManualLoginProfileState(userDataDir, logger, options = {}) {
|
|
202
|
-
if (!options.connectionClosedUnexpectedly) {
|
|
203
|
-
return true;
|
|
204
|
-
}
|
|
205
276
|
const port = await readDevToolsPort(userDataDir);
|
|
206
277
|
if (!port) {
|
|
207
278
|
return true;
|
|
@@ -224,8 +295,8 @@ export async function cleanupStaleProfileState(userDataDir, logger, options = {}
|
|
|
224
295
|
// ignore cleanup errors
|
|
225
296
|
}
|
|
226
297
|
}
|
|
227
|
-
const lockRemovalMode = options.lockRemovalMode ??
|
|
228
|
-
if (lockRemovalMode ===
|
|
298
|
+
const lockRemovalMode = options.lockRemovalMode ?? "never";
|
|
299
|
+
if (lockRemovalMode === "never") {
|
|
229
300
|
return;
|
|
230
301
|
}
|
|
231
302
|
const pid = await readChromePid(userDataDir);
|
|
@@ -239,36 +310,38 @@ export async function cleanupStaleProfileState(userDataDir, logger, options = {}
|
|
|
239
310
|
// Extra safety: if Chrome is running with this profile (but with a different PID, e.g. user relaunched
|
|
240
311
|
// without remote debugging), never delete lock files.
|
|
241
312
|
if (await isChromeUsingUserDataDir(userDataDir)) {
|
|
242
|
-
logger?.(
|
|
313
|
+
logger?.("Detected running Chrome using this profile; skipping profile lock cleanup");
|
|
243
314
|
return;
|
|
244
315
|
}
|
|
245
316
|
const lockFiles = [
|
|
246
|
-
path.join(userDataDir,
|
|
247
|
-
path.join(userDataDir,
|
|
248
|
-
path.join(userDataDir,
|
|
249
|
-
path.join(userDataDir,
|
|
317
|
+
path.join(userDataDir, "lockfile"),
|
|
318
|
+
path.join(userDataDir, "SingletonLock"),
|
|
319
|
+
path.join(userDataDir, "SingletonSocket"),
|
|
320
|
+
path.join(userDataDir, "SingletonCookie"),
|
|
250
321
|
];
|
|
251
322
|
for (const lock of lockFiles) {
|
|
252
323
|
await rm(lock, { force: true }).catch(() => undefined);
|
|
253
324
|
}
|
|
254
|
-
logger?.(
|
|
325
|
+
logger?.("Cleaned up stale Chrome profile locks");
|
|
255
326
|
}
|
|
256
327
|
async function isChromeUsingUserDataDir(userDataDir) {
|
|
257
|
-
if (process.platform ===
|
|
328
|
+
if (process.platform === "win32") {
|
|
258
329
|
// On Windows, lockfiles are typically held open and removal should fail anyway; avoid expensive process scans.
|
|
259
330
|
return false;
|
|
260
331
|
}
|
|
261
332
|
try {
|
|
262
|
-
const { stdout } = await execFileAsync(
|
|
263
|
-
|
|
333
|
+
const { stdout } = await execFileAsync("ps", ["-ax", "-o", "command="], {
|
|
334
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
335
|
+
});
|
|
336
|
+
const lines = String(stdout ?? "").split("\n");
|
|
264
337
|
const needle = userDataDir;
|
|
265
338
|
for (const line of lines) {
|
|
266
339
|
if (!line)
|
|
267
340
|
continue;
|
|
268
341
|
const lower = line.toLowerCase();
|
|
269
|
-
if (!lower.includes(
|
|
342
|
+
if (!lower.includes("chrome") && !lower.includes("chromium"))
|
|
270
343
|
continue;
|
|
271
|
-
if (line.includes(needle) && lower.includes(
|
|
344
|
+
if (line.includes(needle) && lower.includes("user-data-dir")) {
|
|
272
345
|
return true;
|
|
273
346
|
}
|
|
274
347
|
}
|
|
@@ -278,3 +351,15 @@ async function isChromeUsingUserDataDir(userDataDir) {
|
|
|
278
351
|
}
|
|
279
352
|
return false;
|
|
280
353
|
}
|
|
354
|
+
async function readProcessCommand(pid) {
|
|
355
|
+
try {
|
|
356
|
+
const { stdout } = await execFileAsync("ps", ["-p", String(Math.trunc(pid)), "-o", "command="], {
|
|
357
|
+
maxBuffer: 1024 * 1024,
|
|
358
|
+
});
|
|
359
|
+
const command = String(stdout ?? "").trim();
|
|
360
|
+
return command || null;
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { closeTab, connectWithNewTab, hideChromeWindow, launchChrome, registerTerminationHooks, } from "./chromeLifecycle.js";
|
|
5
|
+
import { resolveBrowserConfig } from "./config.js";
|
|
6
|
+
import { syncCookies } from "./cookies.js";
|
|
7
|
+
import { installJavaScriptDialogAutoDismissal, navigateToChatGPT, ensureLoggedIn, } from "./pageActions.js";
|
|
8
|
+
import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLeaseRegistry.js";
|
|
9
|
+
import { acquireProfileRunLock, cleanupStaleProfileState, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
|
|
10
|
+
import { CHATGPT_URL } from "./constants.js";
|
|
11
|
+
import { delay } from "./utils.js";
|
|
12
|
+
import { openProjectSourcesTab, uploadProjectSources, waitForProjectSourcesReady, waitForProjectSourcesListSettled, } from "./actions/projectSources.js";
|
|
13
|
+
import { normalizeProjectSourcesUrl } from "../projectSources/url.js";
|
|
14
|
+
import { buildProjectSourcesUploadPlan, diffAddedProjectSources } from "../projectSources/plan.js";
|
|
15
|
+
export async function runBrowserProjectSources(request) {
|
|
16
|
+
const startedAt = Date.now();
|
|
17
|
+
const logger = ((message) => request.log?.(message));
|
|
18
|
+
const projectUrl = normalizeProjectSourcesUrl(request.chatgptUrl);
|
|
19
|
+
const operation = request.operation;
|
|
20
|
+
const files = request.files ?? [];
|
|
21
|
+
const plannedUploads = buildProjectSourcesUploadPlan(files);
|
|
22
|
+
const warnings = [];
|
|
23
|
+
if (operation === "add" && files.length === 0) {
|
|
24
|
+
throw new Error("Project Sources add requires at least one file.");
|
|
25
|
+
}
|
|
26
|
+
if (request.dryRun) {
|
|
27
|
+
return {
|
|
28
|
+
status: "dry-run",
|
|
29
|
+
operation,
|
|
30
|
+
projectUrl,
|
|
31
|
+
dryRun: true,
|
|
32
|
+
plannedUploads,
|
|
33
|
+
warnings,
|
|
34
|
+
tookMs: Date.now() - startedAt,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
let config = resolveBrowserConfig({
|
|
38
|
+
...request.config,
|
|
39
|
+
url: projectUrl,
|
|
40
|
+
chatgptUrl: projectUrl,
|
|
41
|
+
});
|
|
42
|
+
if (config.remoteChrome) {
|
|
43
|
+
throw new Error("Project Sources v1 uses local browser automation only. Run it on the signed-in browser host.");
|
|
44
|
+
}
|
|
45
|
+
const manualLogin = Boolean(config.manualLogin);
|
|
46
|
+
const manualProfileDir = config.manualLoginProfileDir
|
|
47
|
+
? path.resolve(config.manualLoginProfileDir)
|
|
48
|
+
: path.join(os.homedir(), ".oracle", "browser-profile");
|
|
49
|
+
const userDataDir = manualLogin
|
|
50
|
+
? manualProfileDir
|
|
51
|
+
: await mkdtemp(path.join(os.tmpdir(), "oracle-project-sources-"));
|
|
52
|
+
if (manualLogin) {
|
|
53
|
+
await mkdir(userDataDir, { recursive: true });
|
|
54
|
+
logger(`Manual login mode enabled; reusing persistent profile at ${userDataDir}`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
logger(`Created temporary Chrome profile at ${userDataDir}`);
|
|
58
|
+
}
|
|
59
|
+
let tabLease = null;
|
|
60
|
+
if (manualLogin) {
|
|
61
|
+
tabLease = await acquireBrowserTabLease(userDataDir, {
|
|
62
|
+
maxConcurrentTabs: config.maxConcurrentTabs,
|
|
63
|
+
timeoutMs: config.timeoutMs,
|
|
64
|
+
logger,
|
|
65
|
+
sessionId: "project-sources",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
let chrome = null;
|
|
69
|
+
let reusedChrome = null;
|
|
70
|
+
let client = null;
|
|
71
|
+
let isolatedTargetId = null;
|
|
72
|
+
let removeTerminationHooks = null;
|
|
73
|
+
let removeDialogHandler = null;
|
|
74
|
+
let connectionClosedUnexpectedly = false;
|
|
75
|
+
let completed = false;
|
|
76
|
+
const effectiveKeepBrowser = Boolean(config.keepBrowser);
|
|
77
|
+
try {
|
|
78
|
+
const acquired = manualLogin
|
|
79
|
+
? await acquireManualLoginChromeForProjectSources(userDataDir, config, logger)
|
|
80
|
+
: {
|
|
81
|
+
chrome: await launchChrome({ ...config, remoteChrome: null }, userDataDir, logger),
|
|
82
|
+
reusedChrome: null,
|
|
83
|
+
};
|
|
84
|
+
chrome = acquired.chrome;
|
|
85
|
+
reusedChrome = acquired.reusedChrome;
|
|
86
|
+
const chromeHost = chrome.host ?? "127.0.0.1";
|
|
87
|
+
if (tabLease) {
|
|
88
|
+
await tabLease.update({ chromeHost, chromePort: chrome.port });
|
|
89
|
+
}
|
|
90
|
+
removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger, {
|
|
91
|
+
isInFlight: () => !completed,
|
|
92
|
+
preserveUserDataDir: manualLogin,
|
|
93
|
+
});
|
|
94
|
+
const strictTabIsolation = Boolean(manualLogin && reusedChrome);
|
|
95
|
+
const connection = await connectWithNewTab(chrome.port, logger, "about:blank", chromeHost, {
|
|
96
|
+
fallbackToDefault: !strictTabIsolation,
|
|
97
|
+
retries: strictTabIsolation ? 3 : 0,
|
|
98
|
+
retryDelayMs: 500,
|
|
99
|
+
});
|
|
100
|
+
client = connection.client;
|
|
101
|
+
isolatedTargetId = connection.targetId ?? null;
|
|
102
|
+
if (tabLease && isolatedTargetId) {
|
|
103
|
+
await tabLease.update({
|
|
104
|
+
chromeHost,
|
|
105
|
+
chromePort: chrome.port,
|
|
106
|
+
chromeTargetId: isolatedTargetId,
|
|
107
|
+
tabUrl: projectUrl,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const disconnectPromise = new Promise((_, reject) => {
|
|
111
|
+
client?.on("disconnect", () => {
|
|
112
|
+
connectionClosedUnexpectedly = true;
|
|
113
|
+
reject(new Error("Chrome window closed before Project Sources finished."));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
117
|
+
const { Network, Page, Runtime, Input, DOM } = client;
|
|
118
|
+
if (!config.headless && config.hideWindow) {
|
|
119
|
+
await hideChromeWindow(chrome, logger);
|
|
120
|
+
}
|
|
121
|
+
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
122
|
+
if (DOM && typeof DOM.enable === "function") {
|
|
123
|
+
domainEnablers.push(DOM.enable());
|
|
124
|
+
}
|
|
125
|
+
await Promise.all(domainEnablers);
|
|
126
|
+
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
127
|
+
if (!manualLogin) {
|
|
128
|
+
await Network.clearBrowserCookies();
|
|
129
|
+
}
|
|
130
|
+
const appliedCookies = await applyProjectSourcesCookies({
|
|
131
|
+
config,
|
|
132
|
+
network: Network,
|
|
133
|
+
manualLogin,
|
|
134
|
+
logger,
|
|
135
|
+
});
|
|
136
|
+
await raceWithDisconnect(navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger));
|
|
137
|
+
await raceWithDisconnect(waitForProjectSourcesLogin({
|
|
138
|
+
runtime: Runtime,
|
|
139
|
+
logger,
|
|
140
|
+
appliedCookies,
|
|
141
|
+
manualLogin,
|
|
142
|
+
timeoutMs: config.timeoutMs,
|
|
143
|
+
}));
|
|
144
|
+
await raceWithDisconnect(navigateToChatGPT(Page, Runtime, projectUrl, logger));
|
|
145
|
+
await raceWithDisconnect(openProjectSourcesTab(Runtime, Input, config.inputTimeoutMs, logger));
|
|
146
|
+
await raceWithDisconnect(waitForProjectSourcesReady(Runtime, config.inputTimeoutMs, logger));
|
|
147
|
+
const sourcesBefore = await raceWithDisconnect(waitForProjectSourcesListSettled(Runtime, config.inputTimeoutMs, logger));
|
|
148
|
+
let sourcesAfter = sourcesBefore;
|
|
149
|
+
if (operation === "add") {
|
|
150
|
+
sourcesAfter = await raceWithDisconnect(uploadProjectSources({ runtime: Runtime, dom: DOM, input: Input }, files, logger, config.timeoutMs));
|
|
151
|
+
}
|
|
152
|
+
const added = operation === "add" ? diffAddedProjectSources(sourcesBefore, sourcesAfter) : [];
|
|
153
|
+
completed = true;
|
|
154
|
+
return {
|
|
155
|
+
status: "ok",
|
|
156
|
+
operation,
|
|
157
|
+
projectUrl,
|
|
158
|
+
dryRun: false,
|
|
159
|
+
sourcesBefore,
|
|
160
|
+
sourcesAfter,
|
|
161
|
+
plannedUploads,
|
|
162
|
+
added,
|
|
163
|
+
warnings,
|
|
164
|
+
tookMs: Date.now() - startedAt,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
removeDialogHandler?.();
|
|
169
|
+
removeTerminationHooks?.();
|
|
170
|
+
const chromeHost = chrome?.host ?? "127.0.0.1";
|
|
171
|
+
try {
|
|
172
|
+
await client?.close();
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// ignore close failures
|
|
176
|
+
}
|
|
177
|
+
if (completed && isolatedTargetId && chrome?.port) {
|
|
178
|
+
await closeTab(chrome.port, isolatedTargetId, logger, chromeHost).catch(() => undefined);
|
|
179
|
+
}
|
|
180
|
+
let keepBrowserOpen = effectiveKeepBrowser;
|
|
181
|
+
let cleanupProfileLock = null;
|
|
182
|
+
let terminatedRecordedChrome = false;
|
|
183
|
+
if (!keepBrowserOpen && manualLogin && tabLease) {
|
|
184
|
+
const cleanupLockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
|
|
185
|
+
if (cleanupLockTimeoutMs > 0) {
|
|
186
|
+
cleanupProfileLock = await acquireProfileRunLock(userDataDir, {
|
|
187
|
+
timeoutMs: cleanupLockTimeoutMs,
|
|
188
|
+
logger,
|
|
189
|
+
sessionId: "project-sources",
|
|
190
|
+
}).catch(() => null);
|
|
191
|
+
}
|
|
192
|
+
keepBrowserOpen = await hasOtherActiveBrowserTabLeases(userDataDir, tabLease.id).catch(() => false);
|
|
193
|
+
if (keepBrowserOpen) {
|
|
194
|
+
logger("[browser] Other ChatGPT tab leases still active; leaving shared Chrome running.");
|
|
195
|
+
}
|
|
196
|
+
else if (reusedChrome && !connectionClosedUnexpectedly) {
|
|
197
|
+
keepBrowserOpen = true;
|
|
198
|
+
logger("[browser] Reused shared Chrome; leaving browser process running.");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (tabLease) {
|
|
202
|
+
const handle = tabLease;
|
|
203
|
+
tabLease = null;
|
|
204
|
+
await handle.release().catch(() => undefined);
|
|
205
|
+
}
|
|
206
|
+
if (!keepBrowserOpen && chrome) {
|
|
207
|
+
if (!connectionClosedUnexpectedly) {
|
|
208
|
+
try {
|
|
209
|
+
if (!terminatedRecordedChrome) {
|
|
210
|
+
await chrome.kill();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// ignore kill failures
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (manualLogin) {
|
|
218
|
+
const shouldCleanup = await shouldCleanupManualLoginProfileState(userDataDir, logger.verbose ? logger : undefined, { connectionClosedUnexpectedly, host: chrome.host ?? "127.0.0.1" });
|
|
219
|
+
if (shouldCleanup) {
|
|
220
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else if (chrome) {
|
|
228
|
+
try {
|
|
229
|
+
chrome.process?.unref();
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// best effort
|
|
233
|
+
}
|
|
234
|
+
logger(`Chrome left running on port ${chrome.port} with profile ${userDataDir}`);
|
|
235
|
+
}
|
|
236
|
+
if (cleanupProfileLock) {
|
|
237
|
+
await cleanupProfileLock.release().catch(() => undefined);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function applyProjectSourcesCookies({ config, network, manualLogin, logger, }) {
|
|
242
|
+
const manualLoginCookieSync = manualLogin && Boolean(config.manualLoginCookieSync);
|
|
243
|
+
const cookieSyncEnabled = config.cookieSync && (!manualLogin || manualLoginCookieSync);
|
|
244
|
+
if (!cookieSyncEnabled) {
|
|
245
|
+
logger(manualLogin
|
|
246
|
+
? "Skipping Chrome cookie sync (--browser-manual-login enabled); reuse the opened profile after signing in."
|
|
247
|
+
: "Skipping Chrome cookie sync (--browser-no-cookie-sync)");
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
const cookieCount = await syncCookies(network, config.url, config.chromeProfile, logger, {
|
|
251
|
+
allowErrors: config.allowCookieErrors ?? false,
|
|
252
|
+
filterNames: config.cookieNames ?? undefined,
|
|
253
|
+
inlineCookies: config.inlineCookies ?? undefined,
|
|
254
|
+
cookiePath: config.chromeCookiePath ?? undefined,
|
|
255
|
+
waitMs: config.cookieSyncWaitMs ?? 0,
|
|
256
|
+
});
|
|
257
|
+
logger(cookieCount > 0
|
|
258
|
+
? config.inlineCookies
|
|
259
|
+
? `Applied ${cookieCount} inline cookies`
|
|
260
|
+
: `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ?? "Default"}`
|
|
261
|
+
: "No Chrome cookies found; continuing without session reuse");
|
|
262
|
+
return cookieCount;
|
|
263
|
+
}
|
|
264
|
+
async function waitForProjectSourcesLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, }) {
|
|
265
|
+
if (!manualLogin) {
|
|
266
|
+
await ensureLoggedIn(runtime, logger, { appliedCookies });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const deadline = Date.now() + Math.min(timeoutMs ?? 1_200_000, 20 * 60_000);
|
|
270
|
+
let lastNotice = 0;
|
|
271
|
+
while (Date.now() < deadline) {
|
|
272
|
+
try {
|
|
273
|
+
await ensureLoggedIn(runtime, logger, { appliedCookies });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
278
|
+
const retryable = message.toLowerCase().includes("login button") ||
|
|
279
|
+
message.toLowerCase().includes("session not detected");
|
|
280
|
+
if (!retryable) {
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
const now = Date.now();
|
|
284
|
+
if (now - lastNotice > 5000) {
|
|
285
|
+
logger("Manual login mode: please sign into chatgpt.com in the opened Chrome window; waiting for session to appear...");
|
|
286
|
+
lastNotice = now;
|
|
287
|
+
}
|
|
288
|
+
await delay(1000);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
throw new Error("Manual login mode timed out waiting for ChatGPT session; please sign in and retry.");
|
|
292
|
+
}
|
|
293
|
+
async function acquireManualLoginChromeForProjectSources(userDataDir, config, logger) {
|
|
294
|
+
const lockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
|
|
295
|
+
let launchLock = null;
|
|
296
|
+
if (lockTimeoutMs > 0) {
|
|
297
|
+
launchLock = await acquireProfileRunLock(userDataDir, {
|
|
298
|
+
timeoutMs: lockTimeoutMs,
|
|
299
|
+
logger,
|
|
300
|
+
sessionId: "project-sources",
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const reusedChrome = await maybeReuseProjectSourcesChrome(userDataDir, logger, {
|
|
305
|
+
waitForPortMs: config.reuseChromeWaitMs,
|
|
306
|
+
});
|
|
307
|
+
const chrome = reusedChrome ??
|
|
308
|
+
(await launchChrome({
|
|
309
|
+
...config,
|
|
310
|
+
remoteChrome: null,
|
|
311
|
+
}, userDataDir, logger));
|
|
312
|
+
if (chrome.port) {
|
|
313
|
+
await writeDevToolsActivePort(userDataDir, chrome.port);
|
|
314
|
+
if (!reusedChrome && chrome.pid) {
|
|
315
|
+
await writeChromePid(userDataDir, chrome.pid);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return { chrome, reusedChrome };
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
await launchLock?.release().catch(() => undefined);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function maybeReuseProjectSourcesChrome(userDataDir, logger, options = {}) {
|
|
325
|
+
const waitForPortMs = Math.max(0, options.waitForPortMs ?? 0);
|
|
326
|
+
let port = await readDevToolsPort(userDataDir);
|
|
327
|
+
if (!port && waitForPortMs > 0) {
|
|
328
|
+
const deadline = Date.now() + waitForPortMs;
|
|
329
|
+
logger(`Waiting up to ${Math.round(waitForPortMs / 1000)}s for shared Chrome to appear...`);
|
|
330
|
+
while (!port && Date.now() < deadline) {
|
|
331
|
+
await delay(250);
|
|
332
|
+
port = await readDevToolsPort(userDataDir);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
let pid = await readChromePid(userDataDir);
|
|
336
|
+
if (!port) {
|
|
337
|
+
const discovered = await findRunningChromeDebugTargetForProfile(userDataDir);
|
|
338
|
+
if (!discovered) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
const probe = await verifyDevToolsReachable({ port: discovered.port });
|
|
342
|
+
if (!probe.ok) {
|
|
343
|
+
logger(`Discovered Chrome for ${userDataDir} on port ${discovered.port} but it was unreachable (${probe.error}); launching new Chrome.`);
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
await writeDevToolsActivePort(userDataDir, discovered.port);
|
|
347
|
+
await writeChromePid(userDataDir, discovered.pid);
|
|
348
|
+
port = discovered.port;
|
|
349
|
+
pid = discovered.pid;
|
|
350
|
+
logger(`Discovered running Chrome for ${userDataDir}; reusing (DevTools port ${port}, pid ${pid})`);
|
|
351
|
+
return { port, pid, kill: async () => { }, process: undefined };
|
|
352
|
+
}
|
|
353
|
+
const probe = await verifyDevToolsReachable({ port });
|
|
354
|
+
if (!probe.ok) {
|
|
355
|
+
logger(`Recorded Chrome DevTools port ${port} is stale (${probe.error}); launching new Chrome.`);
|
|
356
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "if_oracle_pid_dead" });
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
logger(`Reusing running Chrome on port ${port} with profile ${userDataDir}`);
|
|
360
|
+
return {
|
|
361
|
+
port,
|
|
362
|
+
pid: pid ?? undefined,
|
|
363
|
+
kill: async () => { },
|
|
364
|
+
process: undefined,
|
|
365
|
+
};
|
|
366
|
+
}
|