@pasko70/pibo 1.9.11 → 1.9.13
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/README.md +183 -183
- package/context/codex-base-prompt.md +148 -148
- package/context/compute-worker.md +23 -23
- package/context/pibo-compaction-prompt.md +100 -100
- package/context/pibo-native-tooling.md +18 -18
- package/context/pibo-system-prompt.md +77 -77
- package/dist/apps/chat/agent-store.js +248 -248
- package/dist/apps/chat/data/project-service.js +168 -168
- package/dist/apps/chat/data/read-state-service.js +18 -18
- package/dist/apps/chat/data/session-query-service.js +25 -25
- package/dist/apps/chat/data/timeline-query-service.js +25 -14
- package/dist/apps/chat/static-assets.js +854 -854
- package/dist/apps/chat/trace.js +2 -0
- package/dist/apps/chat/web-app.js +7 -0
- package/dist/apps/chat/workflow-persistence.js +255 -255
- package/dist/apps/chat-ui/index.html +16 -16
- package/dist/apps/chat-ui/manifest.webmanifest +25 -25
- package/dist/apps/chat-ui/sw.js +47 -47
- package/dist/apps/chat-vscode-web/index.html +12 -12
- package/dist/apps/cli-ui/cliSessionsCommand.js +23 -23
- package/dist/apps/context-files-ui/index.html +11 -11
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.9.13.vsix +0 -0
- package/dist/cli.js +39 -39
- package/dist/compute/cli.js +54 -54
- package/dist/cron/cli.js +15 -15
- package/dist/cron/store.js +49 -49
- package/dist/data/cli.js +23 -23
- package/dist/data/event-log.js +23 -23
- package/dist/data/ingest-service.js +7 -0
- package/dist/data/message-store.js +31 -20
- package/dist/data/navigation-store.js +9 -9
- package/dist/data/observation-store.js +4 -4
- package/dist/data/payload-store.js +17 -17
- package/dist/data/schema.js +433 -431
- package/dist/data/session-store.js +4 -4
- package/dist/data/telemetry-queries.js +54 -54
- package/dist/data/telemetry.js +197 -197
- package/dist/debug/events.js +12 -12
- package/dist/debug/failures.js +6 -6
- package/dist/debug/index.js +227 -227
- package/dist/debug/messages.js +6 -6
- package/dist/debug/pty.js +124 -124
- package/dist/debug/session.js +29 -29
- package/dist/debug/tools.js +5 -5
- package/dist/debug/web-snapshot-browser-scripts.js +294 -294
- package/dist/debug/web-streaming-browser-library.js +925 -925
- package/dist/debug/web-streaming-browser-scripts.js +232 -232
- package/dist/debug/web-streaming-provider-telemetry.js +4 -4
- package/dist/debug/web.js +93 -93
- package/dist/gateway/cli.js +19 -19
- package/dist/mcp/config-command.js +53 -53
- package/dist/mcp/index.js +21 -21
- package/dist/mcp/registry.js +11 -11
- package/dist/pi-packages/cli.js +11 -11
- package/dist/plugins/context-files-store.js +110 -110
- package/dist/plugins/context-files.js +4 -4
- package/dist/ralph/cli.js +18 -18
- package/dist/ralph/templates.js +140 -140
- package/dist/reliability/store.js +226 -226
- package/dist/resources/lifecycle.js +4 -6
- package/dist/resources/reaper-state.js +30 -3
- package/dist/resources/reaper.js +43 -15
- package/dist/sessions/pibo-data-store.js +16 -16
- package/dist/sessions/sqlite-store.js +53 -53
- package/dist/setup/cli.js +58 -58
- package/dist/shared/trace-engine.js +3 -2
- package/dist/shared/trace-event-projection.js +26 -0
- package/dist/tools/agent-browser-wrapper.js +80 -80
- package/dist/tools/browser-use-cdp.js +12 -12
- package/dist/tools/browser-use-wrapper.js +762 -762
- package/dist/tools/guides.js +538 -538
- package/dist/tools/index.js +99 -99
- package/dist/tools/runtime/node-worker-source.js +205 -205
- package/dist/tools/runtime/python-worker-source.js +177 -177
- package/dist/vscode/cli.js +9 -9
- package/dist/web-annotations/cdp.js +900 -900
- package/dist/web-annotations/store.js +96 -96
- package/docs/README.md +23 -23
- package/docs/ops/install-developer-host.md +112 -112
- package/docs/ops/install-user-host.md +96 -96
- package/docs/ops/upgrade-user-to-developer-host.md +69 -69
- package/docs/ops/vscode-extension-release.md +160 -160
- package/package.json +95 -95
- package/skills/builtin/graphify/SKILL.md +52 -52
- package/skills/builtin/pi-agent-harness/SKILL.md +319 -319
- package/skills/builtin/pi-agent-harness/agents/openai.yaml +4 -4
- package/skills/builtin/pibo-docker-system/SKILL.md +170 -170
- package/skills/builtin/pibo-spec-writing/SKILL.md +330 -330
- package/skills/builtin/prd/SKILL.md +143 -143
- package/skills/builtin/ralph-loop/SKILL.md +359 -359
- package/skills/builtin/ralph-prd-json/SKILL.md +123 -123
- package/skills/builtin/skill-creator/LICENSE.txt +201 -201
- package/skills/builtin/skill-creator/SKILL.md +513 -513
- package/skills/builtin/skill-creator/agents/analyzer.md +274 -274
- package/skills/builtin/skill-creator/agents/comparator.md +202 -202
- package/skills/builtin/skill-creator/agents/grader.md +223 -223
- package/skills/builtin/skill-creator/assets/eval_review.html +146 -146
- package/skills/builtin/skill-creator/eval-viewer/generate_review.py +471 -471
- package/skills/builtin/skill-creator/eval-viewer/viewer.html +1325 -1325
- package/skills/builtin/skill-creator/references/schemas.md +430 -430
- package/skills/builtin/skill-creator/scripts/aggregate_benchmark.py +401 -401
- package/skills/builtin/skill-creator/scripts/generate_report.py +326 -326
- package/skills/builtin/skill-creator/scripts/improve_description.py +247 -247
- package/skills/builtin/skill-creator/scripts/package_skill.py +136 -136
- package/skills/builtin/skill-creator/scripts/quick_validate.py +102 -102
- package/skills/builtin/skill-creator/scripts/run_eval.py +310 -310
- package/skills/builtin/skill-creator/scripts/run_loop.py +328 -328
- package/skills/builtin/skill-creator/scripts/utils.py +47 -47
- package/skills/builtin/web-annotations/SKILL.md +93 -93
- package/src/mcp/LICENSE.mcp-cli +21 -21
|
@@ -6,768 +6,768 @@ function shellQuote(value) {
|
|
|
6
6
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
7
7
|
}
|
|
8
8
|
function createBrowserUseWrapper(realExecutablePath) {
|
|
9
|
-
return `#!/bin/sh
|
|
10
|
-
set -eu
|
|
11
|
-
|
|
12
|
-
real_browser_use=${shellQuote(realExecutablePath)}
|
|
13
|
-
default_profile=\${PIBO_BROWSER_USE_DEFAULT_PROFILE:-${shellQuote(BROWSER_USE_DEFAULT_PROFILE)}}
|
|
14
|
-
fresh_flag=${shellQuote(BROWSER_USE_FRESH_PROFILE_FLAG)}
|
|
15
|
-
python_cmd=\${PIBO_BROWSER_USE_PYTHON:-$(dirname "$real_browser_use")/python}
|
|
16
|
-
if [ ! -x "$python_cmd" ]; then
|
|
17
|
-
python_cmd=$(command -v python3 || command -v python || printf '%s' "$python_cmd")
|
|
18
|
-
fi
|
|
19
|
-
|
|
20
|
-
session=\${PIBO_BROWSER_USE_SESSION:-default}
|
|
21
|
-
explicit_profile=0
|
|
22
|
-
fresh_profile=0
|
|
23
|
-
starts_browser=0
|
|
24
|
-
show_help=0
|
|
25
|
-
headed=0
|
|
26
|
-
pibo_ensure_chrome=0
|
|
27
|
-
sanitized_args=
|
|
28
|
-
|
|
29
|
-
append_arg() {
|
|
30
|
-
if [ "$#" -eq 0 ]; then
|
|
31
|
-
return
|
|
32
|
-
fi
|
|
33
|
-
if [ -z "$sanitized_args" ]; then
|
|
34
|
-
sanitized_args=$(printf '%s\\n' "$1")
|
|
35
|
-
else
|
|
36
|
-
sanitized_args=$(printf '%s\\n%s\\n' "$sanitized_args" "$1")
|
|
37
|
-
fi
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
find_chrome() {
|
|
41
|
-
if [ -n "\${PIBO_BROWSER_USE_CHROME:-}" ]; then
|
|
42
|
-
printf '%s\\n' "$PIBO_BROWSER_USE_CHROME"
|
|
43
|
-
return
|
|
44
|
-
fi
|
|
45
|
-
for candidate in google-chrome chrome chromium chromium-browser /opt/google/chrome/chrome; do
|
|
46
|
-
if command -v "$candidate" >/dev/null 2>&1; then
|
|
47
|
-
command -v "$candidate"
|
|
48
|
-
return
|
|
49
|
-
fi
|
|
50
|
-
if [ -x "$candidate" ]; then
|
|
51
|
-
printf '%s\\n' "$candidate"
|
|
52
|
-
return
|
|
53
|
-
fi
|
|
54
|
-
done
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
resolve_profile_directory() {
|
|
58
|
-
user_data_dir=$1
|
|
59
|
-
profile_name=$2
|
|
60
|
-
|
|
61
|
-
if [ -n "\${PIBO_BROWSER_USE_CHROME_PROFILE_DIRECTORY:-}" ]; then
|
|
62
|
-
printf '%s\\n' "$PIBO_BROWSER_USE_CHROME_PROFILE_DIRECTORY"
|
|
63
|
-
return
|
|
64
|
-
fi
|
|
65
|
-
|
|
66
|
-
"$python_cmd" - "$user_data_dir" "$profile_name" <<'PY'
|
|
67
|
-
import json
|
|
68
|
-
import sys
|
|
69
|
-
from pathlib import Path
|
|
70
|
-
|
|
71
|
-
user_data_dir = Path(sys.argv[1])
|
|
72
|
-
profile_name = sys.argv[2]
|
|
73
|
-
local_state = user_data_dir / "Local State"
|
|
74
|
-
try:
|
|
75
|
-
data = json.loads(local_state.read_text())
|
|
76
|
-
except Exception:
|
|
77
|
-
print(profile_name if (user_data_dir / profile_name).is_dir() else "Default")
|
|
78
|
-
raise SystemExit
|
|
79
|
-
|
|
80
|
-
info_cache = data.get("profile", {}).get("info_cache", {})
|
|
81
|
-
if profile_name in info_cache:
|
|
82
|
-
print(profile_name)
|
|
83
|
-
raise SystemExit
|
|
84
|
-
|
|
85
|
-
profile_name_lower = profile_name.lower()
|
|
86
|
-
for directory, info in info_cache.items():
|
|
87
|
-
if str(info.get("name", directory)).lower() == profile_name_lower:
|
|
88
|
-
print(directory)
|
|
89
|
-
raise SystemExit
|
|
90
|
-
|
|
91
|
-
for directory in info_cache:
|
|
92
|
-
if directory.lower() == profile_name_lower:
|
|
93
|
-
print(directory)
|
|
94
|
-
raise SystemExit
|
|
95
|
-
|
|
96
|
-
print(profile_name if (user_data_dir / profile_name).is_dir() else "Default")
|
|
97
|
-
PY
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
find_free_port() {
|
|
101
|
-
"$python_cmd" - <<'PY'
|
|
102
|
-
import socket
|
|
103
|
-
|
|
104
|
-
with socket.socket() as sock:
|
|
105
|
-
sock.bind(("127.0.0.1", 0))
|
|
106
|
-
print(sock.getsockname()[1])
|
|
107
|
-
PY
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
pibo_cdp_is_ready() {
|
|
111
|
-
"$python_cmd" - "$1" <<'PY'
|
|
112
|
-
import socket
|
|
113
|
-
import sys
|
|
114
|
-
|
|
115
|
-
port = int(sys.argv[1])
|
|
116
|
-
try:
|
|
117
|
-
with socket.create_connection(("127.0.0.1", port), timeout=0.2) as sock:
|
|
118
|
-
sock.sendall(b"GET /json/version HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nConnection: close\\r\\n\\r\\n")
|
|
119
|
-
data = sock.recv(128)
|
|
120
|
-
raise SystemExit(0 if b"200" in data else 1)
|
|
121
|
-
except Exception:
|
|
122
|
-
raise SystemExit(1)
|
|
123
|
-
PY
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
find_chrome_pids_for_profile() {
|
|
127
|
-
user_data_dir=$1
|
|
128
|
-
"$python_cmd" - "$user_data_dir" <<'PYFINDCHROME'
|
|
129
|
-
import os
|
|
130
|
-
import shlex
|
|
131
|
-
import subprocess
|
|
132
|
-
import sys
|
|
133
|
-
|
|
134
|
-
managed_dir = os.path.normpath(os.path.abspath(sys.argv[1]))
|
|
135
|
-
browser_names = {"chrome", "google-chrome", "chromium", "chromium-browser"}
|
|
136
|
-
|
|
137
|
-
def normalize_path(value):
|
|
138
|
-
return os.path.normpath(os.path.abspath(value))
|
|
139
|
-
|
|
140
|
-
def has_managed_user_data_dir(args):
|
|
141
|
-
for index, arg in enumerate(args):
|
|
142
|
-
if arg.startswith("--user-data-dir="):
|
|
143
|
-
return normalize_path(arg.split("=", 1)[1]) == managed_dir
|
|
144
|
-
if arg == "--user-data-dir" and index + 1 < len(args):
|
|
145
|
-
return normalize_path(args[index + 1]) == managed_dir
|
|
146
|
-
return False
|
|
147
|
-
|
|
148
|
-
try:
|
|
149
|
-
output = subprocess.check_output(["ps", "-eo", "pid=,args="], text=True, stderr=subprocess.DEVNULL)
|
|
150
|
-
except Exception:
|
|
151
|
-
raise SystemExit(0)
|
|
152
|
-
|
|
153
|
-
for line in output.splitlines():
|
|
154
|
-
line = line.strip()
|
|
155
|
-
if not line:
|
|
156
|
-
continue
|
|
157
|
-
pid_text, _, command = line.partition(" ")
|
|
158
|
-
if not pid_text.isdigit() or not command:
|
|
159
|
-
continue
|
|
160
|
-
try:
|
|
161
|
-
args = shlex.split(command)
|
|
162
|
-
except ValueError:
|
|
163
|
-
continue
|
|
164
|
-
if not args:
|
|
165
|
-
continue
|
|
166
|
-
command_name = os.path.basename(args[0]).lower()
|
|
167
|
-
if command_name not in browser_names:
|
|
168
|
-
continue
|
|
169
|
-
if has_managed_user_data_dir(args):
|
|
170
|
-
print(pid_text)
|
|
171
|
-
PYFINDCHROME
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
find_recorded_browser_targets_for_profile() {
|
|
175
|
-
user_data_dir=$1
|
|
176
|
-
pid_file=$2
|
|
177
|
-
pool_state_file=$3
|
|
178
|
-
"$python_cmd" - "$user_data_dir" "$pid_file" "$pool_state_file" <<'PYRECORDED'
|
|
179
|
-
import json
|
|
180
|
-
import os
|
|
181
|
-
import sys
|
|
182
|
-
from pathlib import Path
|
|
183
|
-
|
|
184
|
-
managed_dir = os.path.normpath(os.path.abspath(sys.argv[1]))
|
|
185
|
-
pid_file = Path(sys.argv[2])
|
|
186
|
-
pool_state_file = Path(sys.argv[3])
|
|
187
|
-
|
|
188
|
-
def normalize_path(value):
|
|
189
|
-
return os.path.normpath(os.path.abspath(value))
|
|
190
|
-
|
|
191
|
-
def print_target(source, pid, pgid=""):
|
|
192
|
-
try:
|
|
193
|
-
pid_text = str(int(str(pid).strip()))
|
|
194
|
-
except Exception:
|
|
195
|
-
return
|
|
196
|
-
if int(pid_text) <= 0:
|
|
197
|
-
return
|
|
198
|
-
pgid_text = ""
|
|
199
|
-
if pgid not in (None, ""):
|
|
200
|
-
try:
|
|
201
|
-
pgid_text = str(int(str(pgid).strip()))
|
|
202
|
-
except Exception:
|
|
203
|
-
pgid_text = ""
|
|
204
|
-
print(f"{source}\t{pid_text}\t{pgid_text}")
|
|
205
|
-
|
|
206
|
-
try:
|
|
207
|
-
data = json.loads(pool_state_file.read_text())
|
|
208
|
-
if normalize_path(str(data.get("userDataDir", ""))) == managed_dir:
|
|
209
|
-
print_target("pool-state", data.get("pid"), data.get("processGroupId"))
|
|
210
|
-
except FileNotFoundError:
|
|
211
|
-
pass
|
|
212
|
-
except Exception:
|
|
213
|
-
pass
|
|
214
|
-
|
|
215
|
-
try:
|
|
216
|
-
print_target("cdp-pid-file", pid_file.read_text())
|
|
217
|
-
except FileNotFoundError:
|
|
218
|
-
pass
|
|
219
|
-
except Exception:
|
|
220
|
-
pass
|
|
221
|
-
PYRECORDED
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
process_group_for_pid() {
|
|
225
|
-
pid=$1
|
|
226
|
-
ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' '
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
process_is_running() {
|
|
230
|
-
pid=$1
|
|
231
|
-
if ! kill -0 "$pid" 2>/dev/null; then
|
|
232
|
-
return 1
|
|
233
|
-
fi
|
|
234
|
-
state=$(ps -o stat= -p "$pid" 2>/dev/null | tr -d ' ' | cut -c1)
|
|
235
|
-
[ "$state" != "Z" ]
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
wait_for_pid_exit() {
|
|
239
|
-
pid=$1
|
|
240
|
-
attempts=0
|
|
241
|
-
while [ "$attempts" -lt "\${PIBO_BROWSER_POOL_TERM_WAIT_ATTEMPTS:-10}" ]; do
|
|
242
|
-
if ! process_is_running "$pid"; then
|
|
243
|
-
return 0
|
|
244
|
-
fi
|
|
245
|
-
attempts=$((attempts + 1))
|
|
246
|
-
sleep "\${PIBO_BROWSER_POOL_TERM_WAIT_SECONDS:-0.1}"
|
|
247
|
-
done
|
|
248
|
-
return 1
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
terminate_managed_browser_target() {
|
|
252
|
-
source=$1
|
|
253
|
-
pid=$2
|
|
254
|
-
recorded_pgid=$3
|
|
255
|
-
if [ -z "$pid" ] || ! process_is_running "$pid"; then
|
|
256
|
-
return 0
|
|
257
|
-
fi
|
|
258
|
-
|
|
259
|
-
actual_pgid=$(process_group_for_pid "$pid")
|
|
260
|
-
target_group=
|
|
261
|
-
if [ -n "$recorded_pgid" ] && [ "$actual_pgid" = "$recorded_pgid" ]; then
|
|
262
|
-
target_group=$recorded_pgid
|
|
263
|
-
elif [ -n "$actual_pgid" ] && [ "$actual_pgid" = "$pid" ]; then
|
|
264
|
-
target_group=$actual_pgid
|
|
265
|
-
fi
|
|
266
|
-
|
|
267
|
-
if [ -n "$target_group" ]; then
|
|
268
|
-
echo "pibo browser-use: terminating stale Chrome process group $target_group for managed profile ($source pid $pid)..." >&2
|
|
269
|
-
kill -TERM "-$target_group" 2>/dev/null || true
|
|
270
|
-
if ! wait_for_pid_exit "$pid"; then
|
|
271
|
-
kill -KILL "-$target_group" 2>/dev/null || true
|
|
272
|
-
wait_for_pid_exit "$pid" || true
|
|
273
|
-
fi
|
|
274
|
-
return 0
|
|
275
|
-
fi
|
|
276
|
-
|
|
277
|
-
echo "pibo browser-use: terminating stale Chrome process $pid for managed profile ($source)..." >&2
|
|
278
|
-
kill -TERM "$pid" 2>/dev/null || true
|
|
279
|
-
if ! wait_for_pid_exit "$pid"; then
|
|
280
|
-
kill -KILL "$pid" 2>/dev/null || true
|
|
281
|
-
wait_for_pid_exit "$pid" || true
|
|
282
|
-
fi
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
pid_file_process_is_dead() {
|
|
286
|
-
pid_file=$1
|
|
287
|
-
if [ ! -s "$pid_file" ]; then
|
|
288
|
-
return 0
|
|
289
|
-
fi
|
|
290
|
-
recorded_pid=$(cat "$pid_file" 2>/dev/null || true)
|
|
291
|
-
if [ -z "$recorded_pid" ]; then
|
|
292
|
-
return 0
|
|
293
|
-
fi
|
|
294
|
-
! process_is_running "$recorded_pid"
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
kill_stale_chrome_for_profile() {
|
|
298
|
-
user_data_dir=$1
|
|
299
|
-
safe_session=$(printf '%s' "$session" | tr -c 'A-Za-z0-9_.-' '_')
|
|
300
|
-
state_dir=\${BROWSER_USE_HOME:-$HOME/.browser-use}/pibo-cdp
|
|
301
|
-
pid_file=$state_dir/$safe_session.pid
|
|
302
|
-
port_file=$state_dir/$safe_session.port
|
|
303
|
-
paths=$(browser_pool_paths)
|
|
304
|
-
pool_state_file=$(printf '%s\n' "$paths" | sed -n '1p')
|
|
305
|
-
|
|
306
|
-
# Prefer recorded pool/CDP pid metadata before falling back to exact profile matching.
|
|
307
|
-
recorded_targets=$(find_recorded_browser_targets_for_profile "$user_data_dir" "$pid_file" "$pool_state_file")
|
|
308
|
-
if [ -n "$recorded_targets" ]; then
|
|
309
|
-
while IFS='\t' read -r source recorded_pid recorded_pgid; do
|
|
310
|
-
terminate_managed_browser_target "$source" "$recorded_pid" "$recorded_pgid"
|
|
311
|
-
done <<EOFRECORDED
|
|
312
|
-
$recorded_targets
|
|
313
|
-
EOFRECORDED
|
|
314
|
-
fi
|
|
315
|
-
|
|
316
|
-
# Then terminate any Chrome/Chromium process using this exact managed profile directory.
|
|
317
|
-
stale_pids=$(find_chrome_pids_for_profile "$user_data_dir")
|
|
318
|
-
for stale_pid in $stale_pids; do
|
|
319
|
-
terminate_managed_browser_target "profile-match" "$stale_pid" ""
|
|
320
|
-
done
|
|
321
|
-
|
|
322
|
-
# Remove stale lock files if the managed browser is confirmed gone.
|
|
323
|
-
sleep 0.2
|
|
324
|
-
if [ -z "$(find_chrome_pids_for_profile "$user_data_dir")" ]; then
|
|
325
|
-
for stale_file in SingletonLock SingletonCookie SingletonSocket; do
|
|
326
|
-
if [ -f "$user_data_dir/$stale_file" ]; then
|
|
327
|
-
rm -f "$user_data_dir/$stale_file" 2>/dev/null || true
|
|
328
|
-
fi
|
|
329
|
-
done
|
|
330
|
-
fi
|
|
331
|
-
|
|
332
|
-
# Remove stale CDP pid/port files only after their recorded process is gone.
|
|
333
|
-
if pid_file_process_is_dead "$pid_file"; then
|
|
334
|
-
rm -f "$pid_file" "$port_file" 2>/dev/null || true
|
|
335
|
-
fi
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
persistent_chrome_url_if_alive() {
|
|
339
|
-
safe_session=$(printf '%s' "$session" | tr -c 'A-Za-z0-9_.-' '_')
|
|
340
|
-
state_dir=\${BROWSER_USE_HOME:-$HOME/.browser-use}/pibo-cdp
|
|
341
|
-
pid_file=$state_dir/$safe_session.pid
|
|
342
|
-
port_file=$state_dir/$safe_session.port
|
|
343
|
-
|
|
344
|
-
if [ -s "$pid_file" ] && [ -s "$port_file" ]; then
|
|
345
|
-
chrome_pid=$(cat "$pid_file" 2>/dev/null || true)
|
|
346
|
-
chrome_port=$(cat "$port_file" 2>/dev/null || true)
|
|
347
|
-
if [ -n "$chrome_pid" ] && [ -n "$chrome_port" ] && kill -0 "$chrome_pid" 2>/dev/null && pibo_cdp_is_ready "$chrome_port"; then
|
|
348
|
-
printf 'http://127.0.0.1:%s\\n' "$chrome_port"
|
|
349
|
-
return 0
|
|
350
|
-
fi
|
|
351
|
-
# Clean up stale state files when process is dead or CDP unreachable
|
|
352
|
-
rm -f "$pid_file" "$port_file" 2>/dev/null || true
|
|
353
|
-
fi
|
|
354
|
-
|
|
355
|
-
return 1
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
start_persistent_chrome() {
|
|
359
|
-
safe_session=$(printf '%s' "$session" | tr -c 'A-Za-z0-9_.-' '_')
|
|
360
|
-
state_dir=\${BROWSER_USE_HOME:-$HOME/.browser-use}/pibo-cdp
|
|
361
|
-
mkdir -p "$state_dir"
|
|
362
|
-
pid_file=$state_dir/$safe_session.pid
|
|
363
|
-
port_file=$state_dir/$safe_session.port
|
|
364
|
-
log_file=$state_dir/$safe_session.chrome.log
|
|
365
|
-
|
|
366
|
-
chrome_bin=$(find_chrome)
|
|
367
|
-
if [ -z "$chrome_bin" ]; then
|
|
368
|
-
echo "pibo browser-use: could not find Chrome; managed browser-pool acquire cannot start a browser. Install Chrome/Chromium or use $fresh_flag for Browser Use managed Chromium." >&2
|
|
369
|
-
return 1
|
|
370
|
-
fi
|
|
371
|
-
|
|
372
|
-
user_data_dir=\${PIBO_BROWSER_USE_CHROME_USER_DATA_DIR:-}
|
|
373
|
-
if [ -z "$user_data_dir" ]; then
|
|
374
|
-
user_data_dir=\${BROWSER_USE_HOME:-$HOME/.browser-use}/chrome-profiles/$default_profile
|
|
375
|
-
fi
|
|
376
|
-
|
|
377
|
-
mkdir -p "$user_data_dir"
|
|
378
|
-
|
|
379
|
-
retry=0
|
|
380
|
-
while [ "$retry" -lt 2 ]; do
|
|
381
|
-
kill_stale_chrome_for_profile "$user_data_dir"
|
|
382
|
-
profile_directory=$(resolve_profile_directory "$user_data_dir" "$default_profile")
|
|
383
|
-
chrome_port=$(find_free_port)
|
|
384
|
-
headless_arg=
|
|
385
|
-
if [ "$headed" -eq 0 ]; then
|
|
386
|
-
headless_arg=--headless=new
|
|
387
|
-
fi
|
|
388
|
-
rm -f "$log_file"
|
|
389
|
-
if command -v setsid >/dev/null 2>&1; then
|
|
390
|
-
setsid "$chrome_bin" \
|
|
391
|
-
--user-data-dir="$user_data_dir" \
|
|
392
|
-
--profile-directory="$profile_directory" \
|
|
393
|
-
--remote-debugging-port="$chrome_port" \
|
|
394
|
-
--no-first-run \
|
|
395
|
-
--no-default-browser-check \
|
|
396
|
-
--disable-default-apps \
|
|
397
|
-
--no-sandbox \
|
|
398
|
-
--disable-setuid-sandbox \
|
|
399
|
-
--disable-dev-shm-usage \
|
|
400
|
-
$headless_arg \
|
|
401
|
-
about:blank >"$log_file" 2>&1 &
|
|
402
|
-
else
|
|
403
|
-
nohup "$chrome_bin" \
|
|
404
|
-
--user-data-dir="$user_data_dir" \
|
|
405
|
-
--profile-directory="$profile_directory" \
|
|
406
|
-
--remote-debugging-port="$chrome_port" \
|
|
407
|
-
--no-first-run \
|
|
408
|
-
--no-default-browser-check \
|
|
409
|
-
--disable-default-apps \
|
|
410
|
-
--no-sandbox \
|
|
411
|
-
--disable-setuid-sandbox \
|
|
412
|
-
--disable-dev-shm-usage \
|
|
413
|
-
$headless_arg \
|
|
414
|
-
about:blank >"$log_file" 2>&1 &
|
|
415
|
-
fi
|
|
416
|
-
chrome_pid=$!
|
|
417
|
-
|
|
418
|
-
ready=0
|
|
419
|
-
if [ "\${PIBO_BROWSER_USE_SKIP_CDP_WAIT:-}" = "1" ]; then
|
|
420
|
-
ready=1
|
|
421
|
-
fi
|
|
422
|
-
attempt=0
|
|
423
|
-
while [ "$ready" -ne 1 ] && [ "$attempt" -lt 50 ]; do
|
|
424
|
-
if pibo_cdp_is_ready "$chrome_port"; then
|
|
425
|
-
ready=1
|
|
426
|
-
break
|
|
427
|
-
fi
|
|
428
|
-
if ! kill -0 "$chrome_pid" 2>/dev/null; then
|
|
429
|
-
break
|
|
430
|
-
fi
|
|
431
|
-
attempt=$((attempt + 1))
|
|
432
|
-
sleep 0.1
|
|
433
|
-
done
|
|
434
|
-
|
|
435
|
-
if [ "$ready" -eq 1 ]; then
|
|
436
|
-
printf '%s\n' "$chrome_pid" > "$pid_file"
|
|
437
|
-
printf '%s\n' "$chrome_port" > "$port_file"
|
|
438
|
-
printf 'pibo browser-use: started Chrome profile "%s" (%s/%s) on CDP port %s.\n' "$default_profile" "$user_data_dir" "$profile_directory" "$chrome_port" >&2
|
|
439
|
-
printf '%s\n%s\nhttp://127.0.0.1:%s\n%s\n' "$chrome_pid" "$chrome_port" "$chrome_port" "$user_data_dir"
|
|
440
|
-
return 0
|
|
441
|
-
fi
|
|
442
|
-
|
|
443
|
-
echo "pibo browser-use: Chrome did not expose CDP on port $chrome_port (attempt $retry)." >&2
|
|
444
|
-
sed -n '1,80p' "$log_file" >&2 2>/dev/null || true
|
|
445
|
-
if grep -q "SingletonLock" "$log_file" 2>/dev/null; then
|
|
446
|
-
echo "pibo browser-use: retrying after lock cleanup..." >&2
|
|
447
|
-
fi
|
|
448
|
-
retry=$((retry + 1))
|
|
449
|
-
sleep 0.5
|
|
450
|
-
done
|
|
451
|
-
|
|
452
|
-
echo "pibo browser-use: Chrome failed to start after retries." >&2
|
|
453
|
-
return 1
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
browser_pool_safe_segment() {
|
|
457
|
-
printf '%s' "$1" | tr -c 'A-Za-z0-9_.-' '_'
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
browser_pool_worker_id() {
|
|
461
|
-
printf '%s\n' "\${PIBO_BROWSER_POOL_WORKER_ID:-\${PIBO_COMPUTE_WORKER_ID:-\${HOSTNAME:-local}}}"
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
browser_pool_id() {
|
|
465
|
-
printf '%s\n' "\${PIBO_BROWSER_POOL_ID:-default}"
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
browser_pool_root() {
|
|
469
|
-
printf '%s\n' "\${PIBO_BROWSER_POOL_ROOT:-\${BROWSER_USE_HOME:-$HOME/.browser-use}/pibo-browser-pool}"
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
browser_pool_paths() {
|
|
473
|
-
worker=$(browser_pool_safe_segment "$(browser_pool_worker_id)")
|
|
474
|
-
pool=$(browser_pool_safe_segment "$(browser_pool_id)")
|
|
475
|
-
base=$(browser_pool_root)/browser-pools/$worker/$pool
|
|
476
|
-
printf '%s\n%s\n' "$base/state.json" "$base/state.lock"
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
browser_pool_read_summary() {
|
|
480
|
-
state_file=$1
|
|
481
|
-
"$python_cmd" - "$state_file" <<'PYSUMMARY'
|
|
482
|
-
import json
|
|
483
|
-
import sys
|
|
484
|
-
from pathlib import Path
|
|
485
|
-
path = Path(sys.argv[1])
|
|
486
|
-
try:
|
|
487
|
-
data = json.loads(path.read_text())
|
|
488
|
-
except FileNotFoundError:
|
|
489
|
-
print("missing")
|
|
490
|
-
raise SystemExit(0)
|
|
491
|
-
except Exception as exc:
|
|
492
|
-
print("malformed")
|
|
493
|
-
print(str(exc))
|
|
494
|
-
raise SystemExit(0)
|
|
495
|
-
for key in ("state", "pid", "cdpPort", "cdpUrl", "userDataDir", "activeLeaseId", "activeLeaseCount", "idleExpiresAt", "holder"):
|
|
496
|
-
value = data.get(key, "")
|
|
497
|
-
print(value if value is not None else "")
|
|
498
|
-
PYSUMMARY
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
browser_pool_lease_is_busy() {
|
|
502
|
-
active_lease=$1
|
|
503
|
-
requested_lease=$2
|
|
504
|
-
idle_expires_at=$3
|
|
505
|
-
if [ -z "$active_lease" ] || [ "$active_lease" = "$requested_lease" ]; then
|
|
506
|
-
return 1
|
|
507
|
-
fi
|
|
508
|
-
"$python_cmd" - "$idle_expires_at" <<'PYBUSY'
|
|
509
|
-
import sys
|
|
510
|
-
from datetime import datetime, timezone
|
|
511
|
-
value = sys.argv[1]
|
|
512
|
-
if not value:
|
|
513
|
-
raise SystemExit(0)
|
|
514
|
-
try:
|
|
515
|
-
expires_at = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
516
|
-
except Exception:
|
|
517
|
-
raise SystemExit(0)
|
|
518
|
-
if expires_at.tzinfo is None:
|
|
519
|
-
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
|
520
|
-
raise SystemExit(1 if expires_at <= datetime.now(timezone.utc) else 0)
|
|
521
|
-
PYBUSY
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
browser_pool_write_state() {
|
|
525
|
-
state_file=$1
|
|
526
|
-
lifecycle_state=$2
|
|
527
|
-
chrome_pid=$3
|
|
528
|
-
chrome_port=$4
|
|
529
|
-
cdp_url=$5
|
|
530
|
-
user_data_dir=$6
|
|
531
|
-
active_lease_id=$7
|
|
532
|
-
holder=$8
|
|
533
|
-
last_error=$9
|
|
534
|
-
"$python_cmd" - "$state_file" "$(browser_pool_worker_id)" "$(browser_pool_id)" "\${PIBO_BROWSER_POOL_MAX_PROCESSES:-1}" "$lifecycle_state" "$chrome_pid" "$chrome_port" "$cdp_url" "$user_data_dir" "$active_lease_id" "$holder" "$last_error" <<'PYSTATE'
|
|
535
|
-
import json
|
|
536
|
-
import os
|
|
537
|
-
import sys
|
|
538
|
-
from datetime import datetime, timedelta, timezone
|
|
539
|
-
from pathlib import Path
|
|
540
|
-
state_file = Path(sys.argv[1])
|
|
541
|
-
worker_id, pool_id, max_processes, lifecycle_state = sys.argv[2:6]
|
|
542
|
-
pid, port, cdp_url, user_data_dir, lease_id, holder, last_error = sys.argv[6:13]
|
|
543
|
-
now = datetime.now(timezone.utc)
|
|
544
|
-
record = {
|
|
545
|
-
"workerId": worker_id,
|
|
546
|
-
"poolId": pool_id,
|
|
547
|
-
"maxBrowserProcesses": int(max_processes or "1"),
|
|
548
|
-
"state": lifecycle_state,
|
|
549
|
-
"lastUsedAt": now.isoformat(timespec="milliseconds").replace("+00:00", "Z"),
|
|
550
|
-
}
|
|
551
|
-
if pid:
|
|
552
|
-
record["pid"] = int(pid)
|
|
553
|
-
record["processGroupId"] = int(pid)
|
|
554
|
-
if port:
|
|
555
|
-
record["cdpPort"] = int(port)
|
|
556
|
-
if cdp_url:
|
|
557
|
-
record["cdpUrl"] = cdp_url
|
|
558
|
-
if user_data_dir:
|
|
559
|
-
record["userDataDir"] = user_data_dir
|
|
560
|
-
if lease_id:
|
|
561
|
-
record["activeLeaseId"] = lease_id
|
|
562
|
-
record["activeLeaseCount"] = 1
|
|
563
|
-
record["cleanupStatus"] = "not-attempted"
|
|
564
|
-
record["idleExpiresAt"] = (now + timedelta(milliseconds=int(os.environ.get("PIBO_BROWSER_POOL_IDLE_TIMEOUT_MS", "600000")))).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
565
|
-
else:
|
|
566
|
-
record["activeLeaseCount"] = 0
|
|
567
|
-
if holder:
|
|
568
|
-
record["holder"] = holder
|
|
569
|
-
if last_error:
|
|
570
|
-
record["lastError"] = last_error
|
|
571
|
-
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
572
|
-
tmp = state_file.with_name(f"{state_file.name}.{os.getpid()}.tmp")
|
|
573
|
-
tmp.write_text(json.dumps(record, indent=2) + "\\n")
|
|
574
|
-
tmp.replace(state_file)
|
|
575
|
-
PYSTATE
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
browser_pool_acquire() {
|
|
579
|
-
paths=$(browser_pool_paths)
|
|
580
|
-
state_file=$(printf '%s\n' "$paths" | sed -n '1p')
|
|
581
|
-
lock_dir=$(printf '%s\n' "$paths" | sed -n '2p')
|
|
582
|
-
mkdir -p "$(dirname "$state_file")"
|
|
583
|
-
safe_session=$(browser_pool_safe_segment "$session")
|
|
584
|
-
lease_id=\${PIBO_BROWSER_POOL_LEASE_ID:-browser-use:$safe_session}
|
|
585
|
-
holder=\${PIBO_BROWSER_POOL_HOLDER:-browser-use:$session}
|
|
586
|
-
attempts=0
|
|
587
|
-
while ! mkdir "$lock_dir" 2>/dev/null; do
|
|
588
|
-
attempts=$((attempts + 1))
|
|
589
|
-
if [ "$attempts" -ge "\${PIBO_BROWSER_POOL_LOCK_ATTEMPTS:-100}" ]; then
|
|
590
|
-
echo "pibo browser-use: managed browser-pool acquire timed out waiting for $lock_dir" >&2
|
|
591
|
-
return 1
|
|
592
|
-
fi
|
|
593
|
-
sleep 0.05
|
|
594
|
-
done
|
|
595
|
-
trap 'rm -rf "$lock_dir"' EXIT INT TERM
|
|
596
|
-
|
|
597
|
-
summary=$(browser_pool_read_summary "$state_file")
|
|
598
|
-
summary_state=$(printf '%s\n' "$summary" | sed -n '1p')
|
|
599
|
-
if [ "$summary_state" = "malformed" ]; then
|
|
600
|
-
malformed_reason=$(printf '%s\n' "$summary" | sed -n '2p')
|
|
601
|
-
browser_pool_write_state "$state_file" dirty "" "" "" "" "" "$holder" "Malformed browser pool state: $malformed_reason"
|
|
602
|
-
elif [ "$summary_state" != "missing" ]; then
|
|
603
|
-
recorded_state=$summary_state
|
|
604
|
-
recorded_pid=$(printf '%s\n' "$summary" | sed -n '2p')
|
|
605
|
-
recorded_port=$(printf '%s\n' "$summary" | sed -n '3p')
|
|
606
|
-
recorded_cdp_url=$(printf '%s\n' "$summary" | sed -n '4p')
|
|
607
|
-
recorded_user_data_dir=$(printf '%s\n' "$summary" | sed -n '5p')
|
|
608
|
-
recorded_active_lease_id=$(printf '%s\n' "$summary" | sed -n '6p')
|
|
609
|
-
recorded_idle_expires_at=$(printf '%s\n' "$summary" | sed -n '8p')
|
|
610
|
-
recorded_holder=$(printf '%s\n' "$summary" | sed -n '9p')
|
|
611
|
-
if [ "$recorded_state" = "leased" ] && browser_pool_lease_is_busy "$recorded_active_lease_id" "$lease_id" "$recorded_idle_expires_at" && [ -n "$recorded_pid" ] && [ -n "$recorded_port" ] && [ -n "$recorded_cdp_url" ] && kill -0 "$recorded_pid" 2>/dev/null && pibo_cdp_is_ready "$recorded_port"; then
|
|
612
|
-
busy_reason="pool-exhausted: browser pool $(browser_pool_id) is already leased by $recorded_active_lease_id until \${recorded_idle_expires_at:-unknown}"
|
|
613
|
-
echo "pibo browser-use: $busy_reason" >&2
|
|
614
|
-
rm -rf "$lock_dir"
|
|
615
|
-
trap - EXIT INT TERM
|
|
616
|
-
return 1
|
|
617
|
-
fi
|
|
618
|
-
if [ "$recorded_state" != "dirty" ] && [ -n "$recorded_pid" ] && [ -n "$recorded_port" ] && [ -n "$recorded_cdp_url" ] && kill -0 "$recorded_pid" 2>/dev/null && pibo_cdp_is_ready "$recorded_port"; then
|
|
619
|
-
browser_pool_write_state "$state_file" leased "$recorded_pid" "$recorded_port" "$recorded_cdp_url" "$recorded_user_data_dir" "$lease_id" "$holder" ""
|
|
620
|
-
rm -rf "$lock_dir"
|
|
621
|
-
trap - EXIT INT TERM
|
|
622
|
-
printf '%s\n' "$recorded_cdp_url"
|
|
623
|
-
return 0
|
|
624
|
-
fi
|
|
625
|
-
browser_pool_write_state "$state_file" stale "$recorded_pid" "$recorded_port" "$recorded_cdp_url" "$recorded_user_data_dir" "" "$holder" "Recorded browser is not alive or CDP is unreachable"
|
|
626
|
-
fi
|
|
627
|
-
|
|
628
|
-
start_result=$(start_persistent_chrome) || {
|
|
629
|
-
browser_pool_write_state "$state_file" dirty "" "" "" "" "" "$holder" "Managed browser start failed"
|
|
630
|
-
rm -rf "$lock_dir"
|
|
631
|
-
trap - EXIT INT TERM
|
|
632
|
-
return 1
|
|
633
|
-
}
|
|
634
|
-
chrome_pid=$(printf '%s\n' "$start_result" | sed -n '1p')
|
|
635
|
-
chrome_port=$(printf '%s\n' "$start_result" | sed -n '2p')
|
|
636
|
-
cdp_url=$(printf '%s\n' "$start_result" | sed -n '3p')
|
|
637
|
-
user_data_dir=$(printf '%s\n' "$start_result" | sed -n '4p')
|
|
638
|
-
browser_pool_write_state "$state_file" leased "$chrome_pid" "$chrome_port" "$cdp_url" "$user_data_dir" "$lease_id" "$holder" ""
|
|
639
|
-
rm -rf "$lock_dir"
|
|
640
|
-
trap - EXIT INT TERM
|
|
641
|
-
printf '%s\n' "$cdp_url"
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
ensure_persistent_chrome() {
|
|
645
|
-
if cdp_url=$(browser_pool_acquire); then
|
|
646
|
-
printf '%s\n' "$cdp_url"
|
|
647
|
-
return 0
|
|
648
|
-
fi
|
|
649
|
-
echo "pibo browser-use: managed browser-pool acquire failed; refusing to start an unmanaged browser." >&2
|
|
650
|
-
exit 1
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
while [ "$#" -gt 0 ]; do
|
|
654
|
-
arg=$1
|
|
655
|
-
shift
|
|
656
|
-
|
|
657
|
-
case "$arg" in
|
|
658
|
-
"$fresh_flag")
|
|
659
|
-
fresh_profile=1
|
|
660
|
-
continue
|
|
661
|
-
;;
|
|
662
|
-
--help|-h)
|
|
663
|
-
show_help=1
|
|
664
|
-
;;
|
|
665
|
-
--headed)
|
|
666
|
-
headed=1
|
|
667
|
-
;;
|
|
668
|
-
--profile)
|
|
669
|
-
explicit_profile=1
|
|
670
|
-
append_arg "$arg"
|
|
671
|
-
continue
|
|
672
|
-
;;
|
|
673
|
-
--cdp-url)
|
|
674
|
-
explicit_profile=1
|
|
675
|
-
append_arg "$arg"
|
|
676
|
-
if [ "$#" -gt 0 ]; then
|
|
677
|
-
append_arg "$1"
|
|
678
|
-
shift
|
|
679
|
-
fi
|
|
680
|
-
continue
|
|
681
|
-
;;
|
|
682
|
-
--profile=*|--cdp-url=*|--connect|--mcp|--use-cloud|--cloud-profile-id|--cloud-profile-id=*)
|
|
683
|
-
explicit_profile=1
|
|
684
|
-
;;
|
|
685
|
-
--session)
|
|
686
|
-
append_arg "$arg"
|
|
687
|
-
if [ "$#" -gt 0 ]; then
|
|
688
|
-
session=$1
|
|
689
|
-
append_arg "$1"
|
|
690
|
-
shift
|
|
691
|
-
fi
|
|
692
|
-
continue
|
|
693
|
-
;;
|
|
694
|
-
--session=*)
|
|
695
|
-
session=\${arg#--session=}
|
|
696
|
-
;;
|
|
697
|
-
--pibo-ensure-chrome)
|
|
698
|
-
pibo_ensure_chrome=1
|
|
699
|
-
continue
|
|
700
|
-
;;
|
|
701
|
-
-*)
|
|
702
|
-
;;
|
|
703
|
-
open|click|type|input|scroll|back|screenshot|state|switch|close-tab|keys|select|upload|eval|extract|hover|dblclick|rightclick|cookies|wait|get|python)
|
|
704
|
-
starts_browser=1
|
|
705
|
-
;;
|
|
706
|
-
close|sessions|install|init|setup|doctor|cloud|profile)
|
|
707
|
-
;;
|
|
708
|
-
esac
|
|
709
|
-
|
|
710
|
-
append_arg "$arg"
|
|
711
|
-
done
|
|
712
|
-
|
|
713
|
-
if [ "$pibo_ensure_chrome" -eq 1 ]; then
|
|
714
|
-
cdp_url=$(ensure_persistent_chrome)
|
|
715
|
-
printf '%s\\n' "$cdp_url"
|
|
716
|
-
exit 0
|
|
717
|
-
fi
|
|
718
|
-
|
|
719
|
-
if [ "$show_help" -eq 1 ]; then
|
|
720
|
-
{
|
|
721
|
-
echo "Pibo browser-use wrapper:"
|
|
722
|
-
echo " New browser-use daemon sessions default to persistent Chrome profile \\"$default_profile\\" via CDP."
|
|
723
|
-
echo " Use $fresh_flag to start a fresh temporary browser profile."
|
|
724
|
-
echo " Use --pibo-ensure-chrome to only start Chrome and print the CDP URL."
|
|
725
|
-
echo ""
|
|
726
|
-
} >&2
|
|
727
|
-
fi
|
|
728
|
-
|
|
729
|
-
set --
|
|
730
|
-
if [ -n "$sanitized_args" ]; then
|
|
731
|
-
while IFS= read -r item; do
|
|
732
|
-
set -- "$@" "$item"
|
|
733
|
-
done <<EOF
|
|
734
|
-
$sanitized_args
|
|
735
|
-
EOF
|
|
736
|
-
fi
|
|
737
|
-
|
|
738
|
-
# Prepend --session if the user did not pass it explicitly and a non-default session is set.
|
|
739
|
-
has_session_arg=0
|
|
740
|
-
for a in "$@"; do
|
|
741
|
-
case "$a" in
|
|
742
|
-
--session|--session=*) has_session_arg=1; break ;;
|
|
743
|
-
esac
|
|
744
|
-
done
|
|
745
|
-
if [ "$has_session_arg" -eq 0 ] && [ "$session" != "default" ]; then
|
|
746
|
-
set -- --session "$session" "$@"
|
|
747
|
-
fi
|
|
748
|
-
|
|
749
|
-
session_pid_file=\${BROWSER_USE_HOME:-$HOME/.browser-use}/$session.pid
|
|
750
|
-
session_alive=0
|
|
751
|
-
if [ -s "$session_pid_file" ]; then
|
|
752
|
-
session_pid=$(cat "$session_pid_file" 2>/dev/null || true)
|
|
753
|
-
if [ -n "$session_pid" ] && kill -0 "$session_pid" 2>/dev/null; then
|
|
754
|
-
session_alive=1
|
|
755
|
-
else
|
|
756
|
-
# Clean up stale session pid file
|
|
757
|
-
rm -f "$session_pid_file" 2>/dev/null || true
|
|
758
|
-
fi
|
|
759
|
-
fi
|
|
760
|
-
|
|
761
|
-
if [ "$starts_browser" -eq 1 ] && [ "$explicit_profile" -eq 0 ] && [ "$fresh_profile" -eq 0 ]; then
|
|
762
|
-
if [ "$session_alive" -eq 1 ]; then
|
|
763
|
-
"$real_browser_use" --session "$session" close >/dev/null 2>&1 || true
|
|
764
|
-
sleep 0.2
|
|
765
|
-
fi
|
|
766
|
-
cdp_url=$(ensure_persistent_chrome)
|
|
767
|
-
exec "$real_browser_use" --cdp-url "$cdp_url" "$@"
|
|
768
|
-
fi
|
|
769
|
-
|
|
770
|
-
exec "$real_browser_use" "$@"
|
|
9
|
+
return `#!/bin/sh
|
|
10
|
+
set -eu
|
|
11
|
+
|
|
12
|
+
real_browser_use=${shellQuote(realExecutablePath)}
|
|
13
|
+
default_profile=\${PIBO_BROWSER_USE_DEFAULT_PROFILE:-${shellQuote(BROWSER_USE_DEFAULT_PROFILE)}}
|
|
14
|
+
fresh_flag=${shellQuote(BROWSER_USE_FRESH_PROFILE_FLAG)}
|
|
15
|
+
python_cmd=\${PIBO_BROWSER_USE_PYTHON:-$(dirname "$real_browser_use")/python}
|
|
16
|
+
if [ ! -x "$python_cmd" ]; then
|
|
17
|
+
python_cmd=$(command -v python3 || command -v python || printf '%s' "$python_cmd")
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
session=\${PIBO_BROWSER_USE_SESSION:-default}
|
|
21
|
+
explicit_profile=0
|
|
22
|
+
fresh_profile=0
|
|
23
|
+
starts_browser=0
|
|
24
|
+
show_help=0
|
|
25
|
+
headed=0
|
|
26
|
+
pibo_ensure_chrome=0
|
|
27
|
+
sanitized_args=
|
|
28
|
+
|
|
29
|
+
append_arg() {
|
|
30
|
+
if [ "$#" -eq 0 ]; then
|
|
31
|
+
return
|
|
32
|
+
fi
|
|
33
|
+
if [ -z "$sanitized_args" ]; then
|
|
34
|
+
sanitized_args=$(printf '%s\\n' "$1")
|
|
35
|
+
else
|
|
36
|
+
sanitized_args=$(printf '%s\\n%s\\n' "$sanitized_args" "$1")
|
|
37
|
+
fi
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
find_chrome() {
|
|
41
|
+
if [ -n "\${PIBO_BROWSER_USE_CHROME:-}" ]; then
|
|
42
|
+
printf '%s\\n' "$PIBO_BROWSER_USE_CHROME"
|
|
43
|
+
return
|
|
44
|
+
fi
|
|
45
|
+
for candidate in google-chrome chrome chromium chromium-browser /opt/google/chrome/chrome; do
|
|
46
|
+
if command -v "$candidate" >/dev/null 2>&1; then
|
|
47
|
+
command -v "$candidate"
|
|
48
|
+
return
|
|
49
|
+
fi
|
|
50
|
+
if [ -x "$candidate" ]; then
|
|
51
|
+
printf '%s\\n' "$candidate"
|
|
52
|
+
return
|
|
53
|
+
fi
|
|
54
|
+
done
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
resolve_profile_directory() {
|
|
58
|
+
user_data_dir=$1
|
|
59
|
+
profile_name=$2
|
|
60
|
+
|
|
61
|
+
if [ -n "\${PIBO_BROWSER_USE_CHROME_PROFILE_DIRECTORY:-}" ]; then
|
|
62
|
+
printf '%s\\n' "$PIBO_BROWSER_USE_CHROME_PROFILE_DIRECTORY"
|
|
63
|
+
return
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
"$python_cmd" - "$user_data_dir" "$profile_name" <<'PY'
|
|
67
|
+
import json
|
|
68
|
+
import sys
|
|
69
|
+
from pathlib import Path
|
|
70
|
+
|
|
71
|
+
user_data_dir = Path(sys.argv[1])
|
|
72
|
+
profile_name = sys.argv[2]
|
|
73
|
+
local_state = user_data_dir / "Local State"
|
|
74
|
+
try:
|
|
75
|
+
data = json.loads(local_state.read_text())
|
|
76
|
+
except Exception:
|
|
77
|
+
print(profile_name if (user_data_dir / profile_name).is_dir() else "Default")
|
|
78
|
+
raise SystemExit
|
|
79
|
+
|
|
80
|
+
info_cache = data.get("profile", {}).get("info_cache", {})
|
|
81
|
+
if profile_name in info_cache:
|
|
82
|
+
print(profile_name)
|
|
83
|
+
raise SystemExit
|
|
84
|
+
|
|
85
|
+
profile_name_lower = profile_name.lower()
|
|
86
|
+
for directory, info in info_cache.items():
|
|
87
|
+
if str(info.get("name", directory)).lower() == profile_name_lower:
|
|
88
|
+
print(directory)
|
|
89
|
+
raise SystemExit
|
|
90
|
+
|
|
91
|
+
for directory in info_cache:
|
|
92
|
+
if directory.lower() == profile_name_lower:
|
|
93
|
+
print(directory)
|
|
94
|
+
raise SystemExit
|
|
95
|
+
|
|
96
|
+
print(profile_name if (user_data_dir / profile_name).is_dir() else "Default")
|
|
97
|
+
PY
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
find_free_port() {
|
|
101
|
+
"$python_cmd" - <<'PY'
|
|
102
|
+
import socket
|
|
103
|
+
|
|
104
|
+
with socket.socket() as sock:
|
|
105
|
+
sock.bind(("127.0.0.1", 0))
|
|
106
|
+
print(sock.getsockname()[1])
|
|
107
|
+
PY
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
pibo_cdp_is_ready() {
|
|
111
|
+
"$python_cmd" - "$1" <<'PY'
|
|
112
|
+
import socket
|
|
113
|
+
import sys
|
|
114
|
+
|
|
115
|
+
port = int(sys.argv[1])
|
|
116
|
+
try:
|
|
117
|
+
with socket.create_connection(("127.0.0.1", port), timeout=0.2) as sock:
|
|
118
|
+
sock.sendall(b"GET /json/version HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nConnection: close\\r\\n\\r\\n")
|
|
119
|
+
data = sock.recv(128)
|
|
120
|
+
raise SystemExit(0 if b"200" in data else 1)
|
|
121
|
+
except Exception:
|
|
122
|
+
raise SystemExit(1)
|
|
123
|
+
PY
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
find_chrome_pids_for_profile() {
|
|
127
|
+
user_data_dir=$1
|
|
128
|
+
"$python_cmd" - "$user_data_dir" <<'PYFINDCHROME'
|
|
129
|
+
import os
|
|
130
|
+
import shlex
|
|
131
|
+
import subprocess
|
|
132
|
+
import sys
|
|
133
|
+
|
|
134
|
+
managed_dir = os.path.normpath(os.path.abspath(sys.argv[1]))
|
|
135
|
+
browser_names = {"chrome", "google-chrome", "chromium", "chromium-browser"}
|
|
136
|
+
|
|
137
|
+
def normalize_path(value):
|
|
138
|
+
return os.path.normpath(os.path.abspath(value))
|
|
139
|
+
|
|
140
|
+
def has_managed_user_data_dir(args):
|
|
141
|
+
for index, arg in enumerate(args):
|
|
142
|
+
if arg.startswith("--user-data-dir="):
|
|
143
|
+
return normalize_path(arg.split("=", 1)[1]) == managed_dir
|
|
144
|
+
if arg == "--user-data-dir" and index + 1 < len(args):
|
|
145
|
+
return normalize_path(args[index + 1]) == managed_dir
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
output = subprocess.check_output(["ps", "-eo", "pid=,args="], text=True, stderr=subprocess.DEVNULL)
|
|
150
|
+
except Exception:
|
|
151
|
+
raise SystemExit(0)
|
|
152
|
+
|
|
153
|
+
for line in output.splitlines():
|
|
154
|
+
line = line.strip()
|
|
155
|
+
if not line:
|
|
156
|
+
continue
|
|
157
|
+
pid_text, _, command = line.partition(" ")
|
|
158
|
+
if not pid_text.isdigit() or not command:
|
|
159
|
+
continue
|
|
160
|
+
try:
|
|
161
|
+
args = shlex.split(command)
|
|
162
|
+
except ValueError:
|
|
163
|
+
continue
|
|
164
|
+
if not args:
|
|
165
|
+
continue
|
|
166
|
+
command_name = os.path.basename(args[0]).lower()
|
|
167
|
+
if command_name not in browser_names:
|
|
168
|
+
continue
|
|
169
|
+
if has_managed_user_data_dir(args):
|
|
170
|
+
print(pid_text)
|
|
171
|
+
PYFINDCHROME
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
find_recorded_browser_targets_for_profile() {
|
|
175
|
+
user_data_dir=$1
|
|
176
|
+
pid_file=$2
|
|
177
|
+
pool_state_file=$3
|
|
178
|
+
"$python_cmd" - "$user_data_dir" "$pid_file" "$pool_state_file" <<'PYRECORDED'
|
|
179
|
+
import json
|
|
180
|
+
import os
|
|
181
|
+
import sys
|
|
182
|
+
from pathlib import Path
|
|
183
|
+
|
|
184
|
+
managed_dir = os.path.normpath(os.path.abspath(sys.argv[1]))
|
|
185
|
+
pid_file = Path(sys.argv[2])
|
|
186
|
+
pool_state_file = Path(sys.argv[3])
|
|
187
|
+
|
|
188
|
+
def normalize_path(value):
|
|
189
|
+
return os.path.normpath(os.path.abspath(value))
|
|
190
|
+
|
|
191
|
+
def print_target(source, pid, pgid=""):
|
|
192
|
+
try:
|
|
193
|
+
pid_text = str(int(str(pid).strip()))
|
|
194
|
+
except Exception:
|
|
195
|
+
return
|
|
196
|
+
if int(pid_text) <= 0:
|
|
197
|
+
return
|
|
198
|
+
pgid_text = ""
|
|
199
|
+
if pgid not in (None, ""):
|
|
200
|
+
try:
|
|
201
|
+
pgid_text = str(int(str(pgid).strip()))
|
|
202
|
+
except Exception:
|
|
203
|
+
pgid_text = ""
|
|
204
|
+
print(f"{source}\t{pid_text}\t{pgid_text}")
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
data = json.loads(pool_state_file.read_text())
|
|
208
|
+
if normalize_path(str(data.get("userDataDir", ""))) == managed_dir:
|
|
209
|
+
print_target("pool-state", data.get("pid"), data.get("processGroupId"))
|
|
210
|
+
except FileNotFoundError:
|
|
211
|
+
pass
|
|
212
|
+
except Exception:
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
print_target("cdp-pid-file", pid_file.read_text())
|
|
217
|
+
except FileNotFoundError:
|
|
218
|
+
pass
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
PYRECORDED
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
process_group_for_pid() {
|
|
225
|
+
pid=$1
|
|
226
|
+
ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' '
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
process_is_running() {
|
|
230
|
+
pid=$1
|
|
231
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
232
|
+
return 1
|
|
233
|
+
fi
|
|
234
|
+
state=$(ps -o stat= -p "$pid" 2>/dev/null | tr -d ' ' | cut -c1)
|
|
235
|
+
[ "$state" != "Z" ]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
wait_for_pid_exit() {
|
|
239
|
+
pid=$1
|
|
240
|
+
attempts=0
|
|
241
|
+
while [ "$attempts" -lt "\${PIBO_BROWSER_POOL_TERM_WAIT_ATTEMPTS:-10}" ]; do
|
|
242
|
+
if ! process_is_running "$pid"; then
|
|
243
|
+
return 0
|
|
244
|
+
fi
|
|
245
|
+
attempts=$((attempts + 1))
|
|
246
|
+
sleep "\${PIBO_BROWSER_POOL_TERM_WAIT_SECONDS:-0.1}"
|
|
247
|
+
done
|
|
248
|
+
return 1
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
terminate_managed_browser_target() {
|
|
252
|
+
source=$1
|
|
253
|
+
pid=$2
|
|
254
|
+
recorded_pgid=$3
|
|
255
|
+
if [ -z "$pid" ] || ! process_is_running "$pid"; then
|
|
256
|
+
return 0
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
actual_pgid=$(process_group_for_pid "$pid")
|
|
260
|
+
target_group=
|
|
261
|
+
if [ -n "$recorded_pgid" ] && [ "$actual_pgid" = "$recorded_pgid" ]; then
|
|
262
|
+
target_group=$recorded_pgid
|
|
263
|
+
elif [ -n "$actual_pgid" ] && [ "$actual_pgid" = "$pid" ]; then
|
|
264
|
+
target_group=$actual_pgid
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
if [ -n "$target_group" ]; then
|
|
268
|
+
echo "pibo browser-use: terminating stale Chrome process group $target_group for managed profile ($source pid $pid)..." >&2
|
|
269
|
+
kill -TERM "-$target_group" 2>/dev/null || true
|
|
270
|
+
if ! wait_for_pid_exit "$pid"; then
|
|
271
|
+
kill -KILL "-$target_group" 2>/dev/null || true
|
|
272
|
+
wait_for_pid_exit "$pid" || true
|
|
273
|
+
fi
|
|
274
|
+
return 0
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
echo "pibo browser-use: terminating stale Chrome process $pid for managed profile ($source)..." >&2
|
|
278
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
279
|
+
if ! wait_for_pid_exit "$pid"; then
|
|
280
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
281
|
+
wait_for_pid_exit "$pid" || true
|
|
282
|
+
fi
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
pid_file_process_is_dead() {
|
|
286
|
+
pid_file=$1
|
|
287
|
+
if [ ! -s "$pid_file" ]; then
|
|
288
|
+
return 0
|
|
289
|
+
fi
|
|
290
|
+
recorded_pid=$(cat "$pid_file" 2>/dev/null || true)
|
|
291
|
+
if [ -z "$recorded_pid" ]; then
|
|
292
|
+
return 0
|
|
293
|
+
fi
|
|
294
|
+
! process_is_running "$recorded_pid"
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
kill_stale_chrome_for_profile() {
|
|
298
|
+
user_data_dir=$1
|
|
299
|
+
safe_session=$(printf '%s' "$session" | tr -c 'A-Za-z0-9_.-' '_')
|
|
300
|
+
state_dir=\${BROWSER_USE_HOME:-$HOME/.browser-use}/pibo-cdp
|
|
301
|
+
pid_file=$state_dir/$safe_session.pid
|
|
302
|
+
port_file=$state_dir/$safe_session.port
|
|
303
|
+
paths=$(browser_pool_paths)
|
|
304
|
+
pool_state_file=$(printf '%s\n' "$paths" | sed -n '1p')
|
|
305
|
+
|
|
306
|
+
# Prefer recorded pool/CDP pid metadata before falling back to exact profile matching.
|
|
307
|
+
recorded_targets=$(find_recorded_browser_targets_for_profile "$user_data_dir" "$pid_file" "$pool_state_file")
|
|
308
|
+
if [ -n "$recorded_targets" ]; then
|
|
309
|
+
while IFS='\t' read -r source recorded_pid recorded_pgid; do
|
|
310
|
+
terminate_managed_browser_target "$source" "$recorded_pid" "$recorded_pgid"
|
|
311
|
+
done <<EOFRECORDED
|
|
312
|
+
$recorded_targets
|
|
313
|
+
EOFRECORDED
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
# Then terminate any Chrome/Chromium process using this exact managed profile directory.
|
|
317
|
+
stale_pids=$(find_chrome_pids_for_profile "$user_data_dir")
|
|
318
|
+
for stale_pid in $stale_pids; do
|
|
319
|
+
terminate_managed_browser_target "profile-match" "$stale_pid" ""
|
|
320
|
+
done
|
|
321
|
+
|
|
322
|
+
# Remove stale lock files if the managed browser is confirmed gone.
|
|
323
|
+
sleep 0.2
|
|
324
|
+
if [ -z "$(find_chrome_pids_for_profile "$user_data_dir")" ]; then
|
|
325
|
+
for stale_file in SingletonLock SingletonCookie SingletonSocket; do
|
|
326
|
+
if [ -f "$user_data_dir/$stale_file" ]; then
|
|
327
|
+
rm -f "$user_data_dir/$stale_file" 2>/dev/null || true
|
|
328
|
+
fi
|
|
329
|
+
done
|
|
330
|
+
fi
|
|
331
|
+
|
|
332
|
+
# Remove stale CDP pid/port files only after their recorded process is gone.
|
|
333
|
+
if pid_file_process_is_dead "$pid_file"; then
|
|
334
|
+
rm -f "$pid_file" "$port_file" 2>/dev/null || true
|
|
335
|
+
fi
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
persistent_chrome_url_if_alive() {
|
|
339
|
+
safe_session=$(printf '%s' "$session" | tr -c 'A-Za-z0-9_.-' '_')
|
|
340
|
+
state_dir=\${BROWSER_USE_HOME:-$HOME/.browser-use}/pibo-cdp
|
|
341
|
+
pid_file=$state_dir/$safe_session.pid
|
|
342
|
+
port_file=$state_dir/$safe_session.port
|
|
343
|
+
|
|
344
|
+
if [ -s "$pid_file" ] && [ -s "$port_file" ]; then
|
|
345
|
+
chrome_pid=$(cat "$pid_file" 2>/dev/null || true)
|
|
346
|
+
chrome_port=$(cat "$port_file" 2>/dev/null || true)
|
|
347
|
+
if [ -n "$chrome_pid" ] && [ -n "$chrome_port" ] && kill -0 "$chrome_pid" 2>/dev/null && pibo_cdp_is_ready "$chrome_port"; then
|
|
348
|
+
printf 'http://127.0.0.1:%s\\n' "$chrome_port"
|
|
349
|
+
return 0
|
|
350
|
+
fi
|
|
351
|
+
# Clean up stale state files when process is dead or CDP unreachable
|
|
352
|
+
rm -f "$pid_file" "$port_file" 2>/dev/null || true
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
return 1
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
start_persistent_chrome() {
|
|
359
|
+
safe_session=$(printf '%s' "$session" | tr -c 'A-Za-z0-9_.-' '_')
|
|
360
|
+
state_dir=\${BROWSER_USE_HOME:-$HOME/.browser-use}/pibo-cdp
|
|
361
|
+
mkdir -p "$state_dir"
|
|
362
|
+
pid_file=$state_dir/$safe_session.pid
|
|
363
|
+
port_file=$state_dir/$safe_session.port
|
|
364
|
+
log_file=$state_dir/$safe_session.chrome.log
|
|
365
|
+
|
|
366
|
+
chrome_bin=$(find_chrome)
|
|
367
|
+
if [ -z "$chrome_bin" ]; then
|
|
368
|
+
echo "pibo browser-use: could not find Chrome; managed browser-pool acquire cannot start a browser. Install Chrome/Chromium or use $fresh_flag for Browser Use managed Chromium." >&2
|
|
369
|
+
return 1
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
user_data_dir=\${PIBO_BROWSER_USE_CHROME_USER_DATA_DIR:-}
|
|
373
|
+
if [ -z "$user_data_dir" ]; then
|
|
374
|
+
user_data_dir=\${BROWSER_USE_HOME:-$HOME/.browser-use}/chrome-profiles/$default_profile
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
mkdir -p "$user_data_dir"
|
|
378
|
+
|
|
379
|
+
retry=0
|
|
380
|
+
while [ "$retry" -lt 2 ]; do
|
|
381
|
+
kill_stale_chrome_for_profile "$user_data_dir"
|
|
382
|
+
profile_directory=$(resolve_profile_directory "$user_data_dir" "$default_profile")
|
|
383
|
+
chrome_port=$(find_free_port)
|
|
384
|
+
headless_arg=
|
|
385
|
+
if [ "$headed" -eq 0 ]; then
|
|
386
|
+
headless_arg=--headless=new
|
|
387
|
+
fi
|
|
388
|
+
rm -f "$log_file"
|
|
389
|
+
if command -v setsid >/dev/null 2>&1; then
|
|
390
|
+
setsid "$chrome_bin" \
|
|
391
|
+
--user-data-dir="$user_data_dir" \
|
|
392
|
+
--profile-directory="$profile_directory" \
|
|
393
|
+
--remote-debugging-port="$chrome_port" \
|
|
394
|
+
--no-first-run \
|
|
395
|
+
--no-default-browser-check \
|
|
396
|
+
--disable-default-apps \
|
|
397
|
+
--no-sandbox \
|
|
398
|
+
--disable-setuid-sandbox \
|
|
399
|
+
--disable-dev-shm-usage \
|
|
400
|
+
$headless_arg \
|
|
401
|
+
about:blank >"$log_file" 2>&1 &
|
|
402
|
+
else
|
|
403
|
+
nohup "$chrome_bin" \
|
|
404
|
+
--user-data-dir="$user_data_dir" \
|
|
405
|
+
--profile-directory="$profile_directory" \
|
|
406
|
+
--remote-debugging-port="$chrome_port" \
|
|
407
|
+
--no-first-run \
|
|
408
|
+
--no-default-browser-check \
|
|
409
|
+
--disable-default-apps \
|
|
410
|
+
--no-sandbox \
|
|
411
|
+
--disable-setuid-sandbox \
|
|
412
|
+
--disable-dev-shm-usage \
|
|
413
|
+
$headless_arg \
|
|
414
|
+
about:blank >"$log_file" 2>&1 &
|
|
415
|
+
fi
|
|
416
|
+
chrome_pid=$!
|
|
417
|
+
|
|
418
|
+
ready=0
|
|
419
|
+
if [ "\${PIBO_BROWSER_USE_SKIP_CDP_WAIT:-}" = "1" ]; then
|
|
420
|
+
ready=1
|
|
421
|
+
fi
|
|
422
|
+
attempt=0
|
|
423
|
+
while [ "$ready" -ne 1 ] && [ "$attempt" -lt 50 ]; do
|
|
424
|
+
if pibo_cdp_is_ready "$chrome_port"; then
|
|
425
|
+
ready=1
|
|
426
|
+
break
|
|
427
|
+
fi
|
|
428
|
+
if ! kill -0 "$chrome_pid" 2>/dev/null; then
|
|
429
|
+
break
|
|
430
|
+
fi
|
|
431
|
+
attempt=$((attempt + 1))
|
|
432
|
+
sleep 0.1
|
|
433
|
+
done
|
|
434
|
+
|
|
435
|
+
if [ "$ready" -eq 1 ]; then
|
|
436
|
+
printf '%s\n' "$chrome_pid" > "$pid_file"
|
|
437
|
+
printf '%s\n' "$chrome_port" > "$port_file"
|
|
438
|
+
printf 'pibo browser-use: started Chrome profile "%s" (%s/%s) on CDP port %s.\n' "$default_profile" "$user_data_dir" "$profile_directory" "$chrome_port" >&2
|
|
439
|
+
printf '%s\n%s\nhttp://127.0.0.1:%s\n%s\n' "$chrome_pid" "$chrome_port" "$chrome_port" "$user_data_dir"
|
|
440
|
+
return 0
|
|
441
|
+
fi
|
|
442
|
+
|
|
443
|
+
echo "pibo browser-use: Chrome did not expose CDP on port $chrome_port (attempt $retry)." >&2
|
|
444
|
+
sed -n '1,80p' "$log_file" >&2 2>/dev/null || true
|
|
445
|
+
if grep -q "SingletonLock" "$log_file" 2>/dev/null; then
|
|
446
|
+
echo "pibo browser-use: retrying after lock cleanup..." >&2
|
|
447
|
+
fi
|
|
448
|
+
retry=$((retry + 1))
|
|
449
|
+
sleep 0.5
|
|
450
|
+
done
|
|
451
|
+
|
|
452
|
+
echo "pibo browser-use: Chrome failed to start after retries." >&2
|
|
453
|
+
return 1
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
browser_pool_safe_segment() {
|
|
457
|
+
printf '%s' "$1" | tr -c 'A-Za-z0-9_.-' '_'
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
browser_pool_worker_id() {
|
|
461
|
+
printf '%s\n' "\${PIBO_BROWSER_POOL_WORKER_ID:-\${PIBO_COMPUTE_WORKER_ID:-\${HOSTNAME:-local}}}"
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
browser_pool_id() {
|
|
465
|
+
printf '%s\n' "\${PIBO_BROWSER_POOL_ID:-default}"
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
browser_pool_root() {
|
|
469
|
+
printf '%s\n' "\${PIBO_BROWSER_POOL_ROOT:-\${BROWSER_USE_HOME:-$HOME/.browser-use}/pibo-browser-pool}"
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
browser_pool_paths() {
|
|
473
|
+
worker=$(browser_pool_safe_segment "$(browser_pool_worker_id)")
|
|
474
|
+
pool=$(browser_pool_safe_segment "$(browser_pool_id)")
|
|
475
|
+
base=$(browser_pool_root)/browser-pools/$worker/$pool
|
|
476
|
+
printf '%s\n%s\n' "$base/state.json" "$base/state.lock"
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
browser_pool_read_summary() {
|
|
480
|
+
state_file=$1
|
|
481
|
+
"$python_cmd" - "$state_file" <<'PYSUMMARY'
|
|
482
|
+
import json
|
|
483
|
+
import sys
|
|
484
|
+
from pathlib import Path
|
|
485
|
+
path = Path(sys.argv[1])
|
|
486
|
+
try:
|
|
487
|
+
data = json.loads(path.read_text())
|
|
488
|
+
except FileNotFoundError:
|
|
489
|
+
print("missing")
|
|
490
|
+
raise SystemExit(0)
|
|
491
|
+
except Exception as exc:
|
|
492
|
+
print("malformed")
|
|
493
|
+
print(str(exc))
|
|
494
|
+
raise SystemExit(0)
|
|
495
|
+
for key in ("state", "pid", "cdpPort", "cdpUrl", "userDataDir", "activeLeaseId", "activeLeaseCount", "idleExpiresAt", "holder"):
|
|
496
|
+
value = data.get(key, "")
|
|
497
|
+
print(value if value is not None else "")
|
|
498
|
+
PYSUMMARY
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
browser_pool_lease_is_busy() {
|
|
502
|
+
active_lease=$1
|
|
503
|
+
requested_lease=$2
|
|
504
|
+
idle_expires_at=$3
|
|
505
|
+
if [ -z "$active_lease" ] || [ "$active_lease" = "$requested_lease" ]; then
|
|
506
|
+
return 1
|
|
507
|
+
fi
|
|
508
|
+
"$python_cmd" - "$idle_expires_at" <<'PYBUSY'
|
|
509
|
+
import sys
|
|
510
|
+
from datetime import datetime, timezone
|
|
511
|
+
value = sys.argv[1]
|
|
512
|
+
if not value:
|
|
513
|
+
raise SystemExit(0)
|
|
514
|
+
try:
|
|
515
|
+
expires_at = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
516
|
+
except Exception:
|
|
517
|
+
raise SystemExit(0)
|
|
518
|
+
if expires_at.tzinfo is None:
|
|
519
|
+
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
|
520
|
+
raise SystemExit(1 if expires_at <= datetime.now(timezone.utc) else 0)
|
|
521
|
+
PYBUSY
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
browser_pool_write_state() {
|
|
525
|
+
state_file=$1
|
|
526
|
+
lifecycle_state=$2
|
|
527
|
+
chrome_pid=$3
|
|
528
|
+
chrome_port=$4
|
|
529
|
+
cdp_url=$5
|
|
530
|
+
user_data_dir=$6
|
|
531
|
+
active_lease_id=$7
|
|
532
|
+
holder=$8
|
|
533
|
+
last_error=$9
|
|
534
|
+
"$python_cmd" - "$state_file" "$(browser_pool_worker_id)" "$(browser_pool_id)" "\${PIBO_BROWSER_POOL_MAX_PROCESSES:-1}" "$lifecycle_state" "$chrome_pid" "$chrome_port" "$cdp_url" "$user_data_dir" "$active_lease_id" "$holder" "$last_error" <<'PYSTATE'
|
|
535
|
+
import json
|
|
536
|
+
import os
|
|
537
|
+
import sys
|
|
538
|
+
from datetime import datetime, timedelta, timezone
|
|
539
|
+
from pathlib import Path
|
|
540
|
+
state_file = Path(sys.argv[1])
|
|
541
|
+
worker_id, pool_id, max_processes, lifecycle_state = sys.argv[2:6]
|
|
542
|
+
pid, port, cdp_url, user_data_dir, lease_id, holder, last_error = sys.argv[6:13]
|
|
543
|
+
now = datetime.now(timezone.utc)
|
|
544
|
+
record = {
|
|
545
|
+
"workerId": worker_id,
|
|
546
|
+
"poolId": pool_id,
|
|
547
|
+
"maxBrowserProcesses": int(max_processes or "1"),
|
|
548
|
+
"state": lifecycle_state,
|
|
549
|
+
"lastUsedAt": now.isoformat(timespec="milliseconds").replace("+00:00", "Z"),
|
|
550
|
+
}
|
|
551
|
+
if pid:
|
|
552
|
+
record["pid"] = int(pid)
|
|
553
|
+
record["processGroupId"] = int(pid)
|
|
554
|
+
if port:
|
|
555
|
+
record["cdpPort"] = int(port)
|
|
556
|
+
if cdp_url:
|
|
557
|
+
record["cdpUrl"] = cdp_url
|
|
558
|
+
if user_data_dir:
|
|
559
|
+
record["userDataDir"] = user_data_dir
|
|
560
|
+
if lease_id:
|
|
561
|
+
record["activeLeaseId"] = lease_id
|
|
562
|
+
record["activeLeaseCount"] = 1
|
|
563
|
+
record["cleanupStatus"] = "not-attempted"
|
|
564
|
+
record["idleExpiresAt"] = (now + timedelta(milliseconds=int(os.environ.get("PIBO_BROWSER_POOL_IDLE_TIMEOUT_MS", "600000")))).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
565
|
+
else:
|
|
566
|
+
record["activeLeaseCount"] = 0
|
|
567
|
+
if holder:
|
|
568
|
+
record["holder"] = holder
|
|
569
|
+
if last_error:
|
|
570
|
+
record["lastError"] = last_error
|
|
571
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
572
|
+
tmp = state_file.with_name(f"{state_file.name}.{os.getpid()}.tmp")
|
|
573
|
+
tmp.write_text(json.dumps(record, indent=2) + "\\n")
|
|
574
|
+
tmp.replace(state_file)
|
|
575
|
+
PYSTATE
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
browser_pool_acquire() {
|
|
579
|
+
paths=$(browser_pool_paths)
|
|
580
|
+
state_file=$(printf '%s\n' "$paths" | sed -n '1p')
|
|
581
|
+
lock_dir=$(printf '%s\n' "$paths" | sed -n '2p')
|
|
582
|
+
mkdir -p "$(dirname "$state_file")"
|
|
583
|
+
safe_session=$(browser_pool_safe_segment "$session")
|
|
584
|
+
lease_id=\${PIBO_BROWSER_POOL_LEASE_ID:-browser-use:$safe_session}
|
|
585
|
+
holder=\${PIBO_BROWSER_POOL_HOLDER:-browser-use:$session}
|
|
586
|
+
attempts=0
|
|
587
|
+
while ! mkdir "$lock_dir" 2>/dev/null; do
|
|
588
|
+
attempts=$((attempts + 1))
|
|
589
|
+
if [ "$attempts" -ge "\${PIBO_BROWSER_POOL_LOCK_ATTEMPTS:-100}" ]; then
|
|
590
|
+
echo "pibo browser-use: managed browser-pool acquire timed out waiting for $lock_dir" >&2
|
|
591
|
+
return 1
|
|
592
|
+
fi
|
|
593
|
+
sleep 0.05
|
|
594
|
+
done
|
|
595
|
+
trap 'rm -rf "$lock_dir"' EXIT INT TERM
|
|
596
|
+
|
|
597
|
+
summary=$(browser_pool_read_summary "$state_file")
|
|
598
|
+
summary_state=$(printf '%s\n' "$summary" | sed -n '1p')
|
|
599
|
+
if [ "$summary_state" = "malformed" ]; then
|
|
600
|
+
malformed_reason=$(printf '%s\n' "$summary" | sed -n '2p')
|
|
601
|
+
browser_pool_write_state "$state_file" dirty "" "" "" "" "" "$holder" "Malformed browser pool state: $malformed_reason"
|
|
602
|
+
elif [ "$summary_state" != "missing" ]; then
|
|
603
|
+
recorded_state=$summary_state
|
|
604
|
+
recorded_pid=$(printf '%s\n' "$summary" | sed -n '2p')
|
|
605
|
+
recorded_port=$(printf '%s\n' "$summary" | sed -n '3p')
|
|
606
|
+
recorded_cdp_url=$(printf '%s\n' "$summary" | sed -n '4p')
|
|
607
|
+
recorded_user_data_dir=$(printf '%s\n' "$summary" | sed -n '5p')
|
|
608
|
+
recorded_active_lease_id=$(printf '%s\n' "$summary" | sed -n '6p')
|
|
609
|
+
recorded_idle_expires_at=$(printf '%s\n' "$summary" | sed -n '8p')
|
|
610
|
+
recorded_holder=$(printf '%s\n' "$summary" | sed -n '9p')
|
|
611
|
+
if [ "$recorded_state" = "leased" ] && browser_pool_lease_is_busy "$recorded_active_lease_id" "$lease_id" "$recorded_idle_expires_at" && [ -n "$recorded_pid" ] && [ -n "$recorded_port" ] && [ -n "$recorded_cdp_url" ] && kill -0 "$recorded_pid" 2>/dev/null && pibo_cdp_is_ready "$recorded_port"; then
|
|
612
|
+
busy_reason="pool-exhausted: browser pool $(browser_pool_id) is already leased by $recorded_active_lease_id until \${recorded_idle_expires_at:-unknown}"
|
|
613
|
+
echo "pibo browser-use: $busy_reason" >&2
|
|
614
|
+
rm -rf "$lock_dir"
|
|
615
|
+
trap - EXIT INT TERM
|
|
616
|
+
return 1
|
|
617
|
+
fi
|
|
618
|
+
if [ "$recorded_state" != "dirty" ] && [ -n "$recorded_pid" ] && [ -n "$recorded_port" ] && [ -n "$recorded_cdp_url" ] && kill -0 "$recorded_pid" 2>/dev/null && pibo_cdp_is_ready "$recorded_port"; then
|
|
619
|
+
browser_pool_write_state "$state_file" leased "$recorded_pid" "$recorded_port" "$recorded_cdp_url" "$recorded_user_data_dir" "$lease_id" "$holder" ""
|
|
620
|
+
rm -rf "$lock_dir"
|
|
621
|
+
trap - EXIT INT TERM
|
|
622
|
+
printf '%s\n' "$recorded_cdp_url"
|
|
623
|
+
return 0
|
|
624
|
+
fi
|
|
625
|
+
browser_pool_write_state "$state_file" stale "$recorded_pid" "$recorded_port" "$recorded_cdp_url" "$recorded_user_data_dir" "" "$holder" "Recorded browser is not alive or CDP is unreachable"
|
|
626
|
+
fi
|
|
627
|
+
|
|
628
|
+
start_result=$(start_persistent_chrome) || {
|
|
629
|
+
browser_pool_write_state "$state_file" dirty "" "" "" "" "" "$holder" "Managed browser start failed"
|
|
630
|
+
rm -rf "$lock_dir"
|
|
631
|
+
trap - EXIT INT TERM
|
|
632
|
+
return 1
|
|
633
|
+
}
|
|
634
|
+
chrome_pid=$(printf '%s\n' "$start_result" | sed -n '1p')
|
|
635
|
+
chrome_port=$(printf '%s\n' "$start_result" | sed -n '2p')
|
|
636
|
+
cdp_url=$(printf '%s\n' "$start_result" | sed -n '3p')
|
|
637
|
+
user_data_dir=$(printf '%s\n' "$start_result" | sed -n '4p')
|
|
638
|
+
browser_pool_write_state "$state_file" leased "$chrome_pid" "$chrome_port" "$cdp_url" "$user_data_dir" "$lease_id" "$holder" ""
|
|
639
|
+
rm -rf "$lock_dir"
|
|
640
|
+
trap - EXIT INT TERM
|
|
641
|
+
printf '%s\n' "$cdp_url"
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
ensure_persistent_chrome() {
|
|
645
|
+
if cdp_url=$(browser_pool_acquire); then
|
|
646
|
+
printf '%s\n' "$cdp_url"
|
|
647
|
+
return 0
|
|
648
|
+
fi
|
|
649
|
+
echo "pibo browser-use: managed browser-pool acquire failed; refusing to start an unmanaged browser." >&2
|
|
650
|
+
exit 1
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
while [ "$#" -gt 0 ]; do
|
|
654
|
+
arg=$1
|
|
655
|
+
shift
|
|
656
|
+
|
|
657
|
+
case "$arg" in
|
|
658
|
+
"$fresh_flag")
|
|
659
|
+
fresh_profile=1
|
|
660
|
+
continue
|
|
661
|
+
;;
|
|
662
|
+
--help|-h)
|
|
663
|
+
show_help=1
|
|
664
|
+
;;
|
|
665
|
+
--headed)
|
|
666
|
+
headed=1
|
|
667
|
+
;;
|
|
668
|
+
--profile)
|
|
669
|
+
explicit_profile=1
|
|
670
|
+
append_arg "$arg"
|
|
671
|
+
continue
|
|
672
|
+
;;
|
|
673
|
+
--cdp-url)
|
|
674
|
+
explicit_profile=1
|
|
675
|
+
append_arg "$arg"
|
|
676
|
+
if [ "$#" -gt 0 ]; then
|
|
677
|
+
append_arg "$1"
|
|
678
|
+
shift
|
|
679
|
+
fi
|
|
680
|
+
continue
|
|
681
|
+
;;
|
|
682
|
+
--profile=*|--cdp-url=*|--connect|--mcp|--use-cloud|--cloud-profile-id|--cloud-profile-id=*)
|
|
683
|
+
explicit_profile=1
|
|
684
|
+
;;
|
|
685
|
+
--session)
|
|
686
|
+
append_arg "$arg"
|
|
687
|
+
if [ "$#" -gt 0 ]; then
|
|
688
|
+
session=$1
|
|
689
|
+
append_arg "$1"
|
|
690
|
+
shift
|
|
691
|
+
fi
|
|
692
|
+
continue
|
|
693
|
+
;;
|
|
694
|
+
--session=*)
|
|
695
|
+
session=\${arg#--session=}
|
|
696
|
+
;;
|
|
697
|
+
--pibo-ensure-chrome)
|
|
698
|
+
pibo_ensure_chrome=1
|
|
699
|
+
continue
|
|
700
|
+
;;
|
|
701
|
+
-*)
|
|
702
|
+
;;
|
|
703
|
+
open|click|type|input|scroll|back|screenshot|state|switch|close-tab|keys|select|upload|eval|extract|hover|dblclick|rightclick|cookies|wait|get|python)
|
|
704
|
+
starts_browser=1
|
|
705
|
+
;;
|
|
706
|
+
close|sessions|install|init|setup|doctor|cloud|profile)
|
|
707
|
+
;;
|
|
708
|
+
esac
|
|
709
|
+
|
|
710
|
+
append_arg "$arg"
|
|
711
|
+
done
|
|
712
|
+
|
|
713
|
+
if [ "$pibo_ensure_chrome" -eq 1 ]; then
|
|
714
|
+
cdp_url=$(ensure_persistent_chrome)
|
|
715
|
+
printf '%s\\n' "$cdp_url"
|
|
716
|
+
exit 0
|
|
717
|
+
fi
|
|
718
|
+
|
|
719
|
+
if [ "$show_help" -eq 1 ]; then
|
|
720
|
+
{
|
|
721
|
+
echo "Pibo browser-use wrapper:"
|
|
722
|
+
echo " New browser-use daemon sessions default to persistent Chrome profile \\"$default_profile\\" via CDP."
|
|
723
|
+
echo " Use $fresh_flag to start a fresh temporary browser profile."
|
|
724
|
+
echo " Use --pibo-ensure-chrome to only start Chrome and print the CDP URL."
|
|
725
|
+
echo ""
|
|
726
|
+
} >&2
|
|
727
|
+
fi
|
|
728
|
+
|
|
729
|
+
set --
|
|
730
|
+
if [ -n "$sanitized_args" ]; then
|
|
731
|
+
while IFS= read -r item; do
|
|
732
|
+
set -- "$@" "$item"
|
|
733
|
+
done <<EOF
|
|
734
|
+
$sanitized_args
|
|
735
|
+
EOF
|
|
736
|
+
fi
|
|
737
|
+
|
|
738
|
+
# Prepend --session if the user did not pass it explicitly and a non-default session is set.
|
|
739
|
+
has_session_arg=0
|
|
740
|
+
for a in "$@"; do
|
|
741
|
+
case "$a" in
|
|
742
|
+
--session|--session=*) has_session_arg=1; break ;;
|
|
743
|
+
esac
|
|
744
|
+
done
|
|
745
|
+
if [ "$has_session_arg" -eq 0 ] && [ "$session" != "default" ]; then
|
|
746
|
+
set -- --session "$session" "$@"
|
|
747
|
+
fi
|
|
748
|
+
|
|
749
|
+
session_pid_file=\${BROWSER_USE_HOME:-$HOME/.browser-use}/$session.pid
|
|
750
|
+
session_alive=0
|
|
751
|
+
if [ -s "$session_pid_file" ]; then
|
|
752
|
+
session_pid=$(cat "$session_pid_file" 2>/dev/null || true)
|
|
753
|
+
if [ -n "$session_pid" ] && kill -0 "$session_pid" 2>/dev/null; then
|
|
754
|
+
session_alive=1
|
|
755
|
+
else
|
|
756
|
+
# Clean up stale session pid file
|
|
757
|
+
rm -f "$session_pid_file" 2>/dev/null || true
|
|
758
|
+
fi
|
|
759
|
+
fi
|
|
760
|
+
|
|
761
|
+
if [ "$starts_browser" -eq 1 ] && [ "$explicit_profile" -eq 0 ] && [ "$fresh_profile" -eq 0 ]; then
|
|
762
|
+
if [ "$session_alive" -eq 1 ]; then
|
|
763
|
+
"$real_browser_use" --session "$session" close >/dev/null 2>&1 || true
|
|
764
|
+
sleep 0.2
|
|
765
|
+
fi
|
|
766
|
+
cdp_url=$(ensure_persistent_chrome)
|
|
767
|
+
exec "$real_browser_use" --cdp-url "$cdp_url" "$@"
|
|
768
|
+
fi
|
|
769
|
+
|
|
770
|
+
exec "$real_browser_use" "$@"
|
|
771
771
|
`;
|
|
772
772
|
}
|
|
773
773
|
export function ensureBrowserUseWrapper(status) {
|