@crvouga/dev-fleet 0.2.0 → 0.4.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/README.md +50 -16
- package/ansible/github/git.yml +66 -0
- package/ansible/macos/manager.yml +3 -0
- package/ansible/macos/policy.json +11 -1
- package/ansible/macos/t3-policy.yml +49 -0
- package/ansible/monitoring/compose.yml +10 -0
- package/ansible/monitoring/container-watchdog.py +118 -0
- package/ansible/monitoring/hub-darwin.yml +146 -0
- package/ansible/net/remote-probe.sh +5 -0
- package/ansible/ubuntu/baseline.yml +2 -0
- package/ansible/ubuntu/disk-gc.py +25 -3
- package/ansible/ubuntu/disk-guard.py +24 -5
- package/ansible/ubuntu/policy.json +51 -1
- package/ansible/ubuntu/t3-desktop.yml +19 -0
- package/ansible/ubuntu/t3-health.sh +93 -22
- package/ansible/ubuntu/t3-idle.py +42 -0
- package/ansible/ubuntu/t3-policy.yml +4 -11
- package/ansible/ubuntu/tasks/browser.yml +56 -0
- package/ansible/ubuntu/tasks/docker.yml +52 -0
- package/ansible/ubuntu/tasks/recovery.yml +60 -1
- package/ansible/ubuntu/tasks/resources.yml +2 -2
- package/ansible/ubuntu/tasks/services.yml +13 -0
- package/ansible/ubuntu/tasks/t3-restart.yml +37 -0
- package/ansible/ubuntu/wip-guard.py +121 -0
- package/bootstrap/doctor.ts +32 -6
- package/bootstrap/fleet-remote-bootstrap.sh +31 -4
- package/bootstrap/native-t3.ts +22 -11
- package/bootstrap/setup.ts +23 -8
- package/bootstrap/worker-tools.ts +118 -0
- package/cliproxy/README.md +4 -4
- package/cliproxy/compose.yaml +4 -0
- package/cliproxy/scripts/accounts.py +216 -0
- package/cliproxy/scripts/checkpoint.py +30 -2
- package/cliproxy/scripts/cli.py +1 -1
- package/cliproxy/scripts/clientkeys.py +99 -0
- package/cliproxy/scripts/lease.py +80 -0
- package/cliproxy/scripts/ops +137 -25
- package/cliproxy/scripts/restore-verify +15 -0
- package/docs/accounts.md +154 -0
- package/docs/authentication.md +90 -0
- package/docs/declared.md +5 -1
- package/docs/durability.md +26 -0
- package/docs/machine-setup.md +26 -15
- package/docs/operations.md +87 -31
- package/docs/runbooks.md +380 -0
- package/docs/state.md +13 -7
- package/docs/topology.md +2 -0
- package/docs/ubuntu-sync.md +35 -28
- package/examples/fleet.json +19 -1
- package/llms.txt +3 -0
- package/mise.toml +10 -1
- package/package.json +2 -2
- package/schema/check-ids.json +522 -0
- package/schema/fleet.schema.json +244 -1
- package/schema/output/check.schema.json +23 -0
- package/schema/output/error.schema.json +14 -0
- package/schema/output/fleet-report.schema.json +79 -0
- package/schema/output/fleet-sync.schema.json +112 -0
- package/schema/output/report.schema.json +38 -0
- package/src/accounts/checks.ts +220 -0
- package/src/accounts/context.ts +76 -0
- package/src/accounts/propagate.ts +62 -0
- package/src/accounts/roster.ts +85 -0
- package/src/accounts/selection.ts +67 -0
- package/src/accounts/sessions.ts +120 -0
- package/src/agents/account-relay.ts +176 -26
- package/src/agents/launch.ts +84 -5
- package/src/agents/native-verify.ts +128 -2
- package/src/auth-checks.ts +3 -0
- package/src/cli/agent-guide.ts +5 -3
- package/src/cli/fleet-accounts.ts +253 -0
- package/src/cli/fleet-report.ts +126 -0
- package/src/cli/fleet-sync-accounts.ts +58 -0
- package/src/cli/fleet-sync-manager.ts +31 -4
- package/src/cli/fleet-sync-output.ts +3 -2
- package/src/cli/fleet-sync-plan.ts +200 -0
- package/src/cli/fleet-sync-remote.ts +23 -14
- package/src/cli/fleet-sync-schedule.ts +142 -35
- package/src/cli/fleet-sync.ts +126 -8
- package/src/cli/fleet-watch.ts +198 -0
- package/src/cli/fleet.ts +4 -0
- package/src/cli/register-fleet.ts +86 -0
- package/src/cli/registry.ts +26 -0
- package/src/cli/schedule-logs.ts +36 -0
- package/src/clock.ts +63 -0
- package/src/definition.ts +226 -7
- package/src/git-identity.ts +160 -0
- package/src/ide/definition.ts +86 -0
- package/src/ide/ide.ts +43 -0
- package/src/ide/index.ts +5 -0
- package/src/ide/orchestrate.ts +50 -0
- package/src/ide/t3.ts +111 -0
- package/src/machine-health.ts +51 -4
- package/src/manager-health.ts +205 -0
- package/src/manager-sync.ts +13 -17
- package/src/monitoring/hub-backup.ts +63 -17
- package/src/monitoring/manager-hub.ts +44 -2
- package/src/node-converge-linux.ts +44 -12
- package/src/notify.ts +239 -0
- package/src/platform/darwin.ts +0 -13
- package/src/platform/fallback.ts +0 -8
- package/src/platform/linux.ts +0 -10
- package/src/platform/t3-policy.ts +33 -6
- package/src/platform/t3-service.ts +104 -0
- package/src/platform/types.ts +0 -4
- package/src/platform/windows.ts +0 -14
- package/src/remote-bootstrap.ts +87 -25
- package/src/remote-entry.ts +15 -1
- package/src/remote-operation.ts +213 -0
- package/src/schedule-check.ts +40 -0
- package/src/secrets.ts +26 -8
- package/src/service-auth.ts +15 -5
- package/src/ssh.ts +10 -2
- package/src/state/history.ts +172 -0
- package/src/state/store.ts +11 -4
- package/src/t3/accounts.ts +7 -1
- package/src/t3/models.ts +133 -0
- package/src/t3/pairing.ts +31 -0
- package/src/t3/profile.ts +203 -0
- package/src/t3/version.ts +119 -0
- package/src/t3-linked-check.ts +1 -1
- package/src/tailscale.ts +51 -0
- package/src/toolchain-checks.ts +8 -3
- package/src/transport.ts +3 -2
- package/src/worker-capability-checks.ts +167 -0
- package/src/worker-capacity.ts +19 -7
- package/src/worker-host-checks.ts +68 -13
- package/src/worker-policy.ts +33 -0
- package/src/worker-readiness.ts +10 -1
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
Managed by personal fleet-sync (tasks/resources.yml) as
|
|
5
5
|
personal-fleet-disk-guard.service. Every interval it measures free space on the
|
|
6
|
-
watched filesystems and the bytes each leaf cgroup under user.slice wrote. When
|
|
7
|
-
any watched filesystem is below the floor
|
|
6
|
+
watched filesystems (bytes and inodes) and the bytes each leaf cgroup under user.slice wrote. When
|
|
7
|
+
any watched filesystem is below the floor of free bytes or of free inodes, it
|
|
8
|
+
kills only the single cgroup that
|
|
8
9
|
wrote the most during the last interval, and only if that is at least the write
|
|
9
10
|
threshold. It never deletes files, never touches system services and never
|
|
10
11
|
kills idle work. Free space must be recovered by a person.
|
|
11
12
|
|
|
12
|
-
Usage: disk-guard.py --path / --path /home/me [--floor-mib N]
|
|
13
|
-
[--interval S] [--cgroup-root DIR] [--dry-run]
|
|
13
|
+
Usage: disk-guard.py --path / --path /home/me [--floor-mib N]
|
|
14
|
+
[--inode-floor-percent N] [--write-mib N] [--interval S] [--cgroup-root DIR] [--dry-run]
|
|
14
15
|
"""
|
|
15
16
|
import argparse
|
|
16
17
|
import json
|
|
@@ -61,6 +62,21 @@ def free_bytes(path):
|
|
|
61
62
|
return st.f_bavail * st.f_frsize
|
|
62
63
|
|
|
63
64
|
|
|
65
|
+
def free_inodes_percent(path):
|
|
66
|
+
"""Free inodes as a percentage; None on a filesystem without a fixed count."""
|
|
67
|
+
st = os.statvfs(path)
|
|
68
|
+
if st.f_files <= 0:
|
|
69
|
+
return None
|
|
70
|
+
return st.f_favail * 100 / st.f_files
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def low_paths(free, inodes, floor, inode_floor):
|
|
74
|
+
"""The watched paths below either floor: bytes, or free inodes in percent."""
|
|
75
|
+
return [path for path in free
|
|
76
|
+
if free[path] < floor
|
|
77
|
+
or (inodes.get(path) is not None and inodes[path] < inode_floor)]
|
|
78
|
+
|
|
79
|
+
|
|
64
80
|
def log(**event):
|
|
65
81
|
print(json.dumps(event, sort_keys=True), flush=True)
|
|
66
82
|
|
|
@@ -69,6 +85,7 @@ def main(argv):
|
|
|
69
85
|
parser = argparse.ArgumentParser()
|
|
70
86
|
parser.add_argument('--path', action='append', required=True)
|
|
71
87
|
parser.add_argument('--floor-mib', type=int, default=3072)
|
|
88
|
+
parser.add_argument('--inode-floor-percent', type=float, default=2)
|
|
72
89
|
parser.add_argument('--write-mib', type=int, default=32)
|
|
73
90
|
parser.add_argument('--interval', type=float, default=5.0)
|
|
74
91
|
parser.add_argument('--cgroup-root', default='/sys/fs/cgroup/user.slice')
|
|
@@ -76,6 +93,7 @@ def main(argv):
|
|
|
76
93
|
args = parser.parse_args(argv)
|
|
77
94
|
floor, threshold = args.floor_mib * MIB, args.write_mib * MIB
|
|
78
95
|
log(event='started', paths=args.path, floor_mib=args.floor_mib,
|
|
96
|
+
inode_floor_percent=args.inode_floor_percent,
|
|
79
97
|
write_mib=args.write_mib, interval=args.interval, dry_run=args.dry_run)
|
|
80
98
|
previous = leaf_writes(args.cgroup_root)
|
|
81
99
|
while True:
|
|
@@ -83,7 +101,8 @@ def main(argv):
|
|
|
83
101
|
try:
|
|
84
102
|
current = leaf_writes(args.cgroup_root)
|
|
85
103
|
free = {path: free_bytes(path) for path in args.path}
|
|
86
|
-
|
|
104
|
+
inodes = {path: free_inodes_percent(path) for path in args.path}
|
|
105
|
+
low = low_paths(free, inodes, floor, args.inode_floor_percent)
|
|
87
106
|
victim = choose_victim(previous, current, bool(low), threshold)
|
|
88
107
|
previous = current
|
|
89
108
|
if victim is None:
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"aptLockTimeoutSeconds": 120,
|
|
27
27
|
"minimumFreeDiskMiB": 4096,
|
|
28
28
|
"minimumFreeInodes": 10000,
|
|
29
|
+
"t3RestartMaxDeferralHours": 24,
|
|
29
30
|
"alwaysOnServices": ["ssh", "tailscaled", "docker", "beszel-agent"],
|
|
30
31
|
"resourceSafety": {
|
|
31
32
|
"swapFileMiB": 8192,
|
|
@@ -47,6 +48,55 @@
|
|
|
47
48
|
"diskGcPressureDockerAgeHours": 24,
|
|
48
49
|
"diskGcPressureCaches": [".bun/install/cache"],
|
|
49
50
|
"dockerLogMaxSize": "50m",
|
|
50
|
-
"dockerLogMaxFiles": "3"
|
|
51
|
+
"dockerLogMaxFiles": "3",
|
|
52
|
+
"inodeWarnFreePercent": 10,
|
|
53
|
+
"inodeFailFreePercent": 2
|
|
54
|
+
},
|
|
55
|
+
"clock": {
|
|
56
|
+
"warnOffsetSeconds": 2,
|
|
57
|
+
"failOffsetSeconds": 30
|
|
58
|
+
},
|
|
59
|
+
"rebootPendingFailDays": 14,
|
|
60
|
+
"dockerSmokeTest": {
|
|
61
|
+
"image": "busybox:1.37.0@sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec",
|
|
62
|
+
"timeoutSeconds": 60
|
|
63
|
+
},
|
|
64
|
+
"browser": {
|
|
65
|
+
"packages": {
|
|
66
|
+
"fonts-liberation": "1:2.1.5-3build1",
|
|
67
|
+
"libasound2t64": "1.2.15.3-1ubuntu1.1",
|
|
68
|
+
"libatk-bridge2.0-0t64": "2.60.4-0ubuntu0.1",
|
|
69
|
+
"libatk1.0-0t64": "2.60.4-0ubuntu0.1",
|
|
70
|
+
"libatspi2.0-0t64": "2.60.4-0ubuntu0.1",
|
|
71
|
+
"libcairo2": "1.18.4-3",
|
|
72
|
+
"libcups2t64": "2.4.16-1ubuntu1.3",
|
|
73
|
+
"libdbus-1-3": "1.16.2-2ubuntu4",
|
|
74
|
+
"libdrm2": "2.4.131-1",
|
|
75
|
+
"libgbm1": "26.0.8-1ubuntu0.3",
|
|
76
|
+
"libglib2.0-0t64": "2.88.0-1ubuntu0.1",
|
|
77
|
+
"libnspr4": "2:4.38.2-1ubuntu1",
|
|
78
|
+
"libnss3": "2:3.120-1ubuntu2.2",
|
|
79
|
+
"libpango-1.0-0": "1.57.0-1",
|
|
80
|
+
"libx11-6": "2:1.8.13-1ubuntu0.1",
|
|
81
|
+
"libxcb1": "1.17.0-2ubuntu1",
|
|
82
|
+
"libxcomposite1": "1:0.4.6-1build1",
|
|
83
|
+
"libxdamage1": "1:1.1.7-1",
|
|
84
|
+
"libxext6": "2:1.3.4-1build3",
|
|
85
|
+
"libxfixes3": "1:6.0.0-2build2",
|
|
86
|
+
"libxkbcommon0": "1.13.1-1",
|
|
87
|
+
"libxrandr2": "2:1.5.4-1build1"
|
|
88
|
+
},
|
|
89
|
+
"headlessShell": {
|
|
90
|
+
"version": "158.0.8127.18",
|
|
91
|
+
"amd64": {
|
|
92
|
+
"archive": "linux64/chrome-headless-shell-linux64",
|
|
93
|
+
"sha256": "18352d22cb0cdf520cbff2ad8c41391f0e02a72886cb53b0fde4bdfc4bd075d6"
|
|
94
|
+
},
|
|
95
|
+
"arm64": {
|
|
96
|
+
"archive": "linux-arm64/chrome-headless-shell-linux-arm64",
|
|
97
|
+
"sha256": "b136462898353d94c7fab50f9cdaa6d8f0d4c190153916936aa0a03353a26bf2"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"launchTimeoutSeconds": 60
|
|
51
101
|
}
|
|
52
102
|
}
|
|
@@ -53,6 +53,16 @@
|
|
|
53
53
|
register: t3_desktop_installed
|
|
54
54
|
changed_when: false
|
|
55
55
|
failed_when: false
|
|
56
|
+
- name: Check whether the desktop app is running before an update
|
|
57
|
+
become: true
|
|
58
|
+
become_user: '{{ development_user }}'
|
|
59
|
+
environment:
|
|
60
|
+
XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
|
|
61
|
+
ansible.builtin.command:
|
|
62
|
+
argv: [systemctl, --user, is-active, --quiet, t3code-desktop.service]
|
|
63
|
+
register: t3_desktop_was_active
|
|
64
|
+
changed_when: false
|
|
65
|
+
failed_when: false
|
|
56
66
|
- name: Unpack the pinned app when its version differs
|
|
57
67
|
when: t3_desktop_installed.stdout | default('') != t3_desktop_name
|
|
58
68
|
block:
|
|
@@ -94,6 +104,7 @@
|
|
|
94
104
|
- -T
|
|
95
105
|
- '/opt/.t3code-staging{{ t3_desktop_dir }}'
|
|
96
106
|
- '{{ t3_desktop_dir }}'
|
|
107
|
+
register: t3_desktop_updated
|
|
97
108
|
changed_when: true
|
|
98
109
|
- name: Add the launcher entry and icon
|
|
99
110
|
ansible.builtin.copy:
|
|
@@ -218,3 +229,11 @@
|
|
|
218
229
|
name: t3code-desktop.service
|
|
219
230
|
state: started
|
|
220
231
|
failed_when: false
|
|
232
|
+
# A running app keeps the files of its release: restart it into the one
|
|
233
|
+
# just installed, only while it runs no agent turn. Otherwise
|
|
234
|
+
# personal-worker-recovery restarts it at the first idle check, and
|
|
235
|
+
# t3:running-version reports T3_RESTART_PENDING until then.
|
|
236
|
+
- ansible.builtin.import_tasks: tasks/t3-restart.yml
|
|
237
|
+
when:
|
|
238
|
+
- t3_desktop_was_active.rc == 0
|
|
239
|
+
- t3_desktop_updated is changed
|
|
@@ -1,33 +1,104 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# Keep the worker T3 desktop app (UI and server) up
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
2
|
+
# Keep the worker T3 desktop app (UI and server) up, responsive and on the
|
|
3
|
+
# installed release. Run every minute by personal-worker-recovery.timer
|
|
4
|
+
# (managed by fleet-sync, tasks/recovery.yml), from the stable copy the role
|
|
5
|
+
# installs beside t3-idle.py.
|
|
6
|
+
# - The former headless server is stopped and disabled: one T3 per worker.
|
|
7
|
+
# - No graphical session: T3_NO_GRAPHICAL_SESSION; after three checks in a row
|
|
8
|
+
# the display manager is restarted so auto-login starts one, three times at
|
|
9
|
+
# most until a session is seen again.
|
|
10
|
+
# - Stopped or failed: reset its start limit and start it.
|
|
11
|
+
# - Running but its local API has not answered for three checks in a row:
|
|
12
|
+
# restart it, since a hung server serves no turns.
|
|
13
|
+
# - Running another release than the installed one, or a policy restart that
|
|
14
|
+
# was deferred: restart it once it runs no agent turn (T3_RESTART_PENDING).
|
|
6
15
|
set -u
|
|
7
16
|
url=http://127.0.0.1:3773/.well-known/t3/environment
|
|
8
|
-
|
|
17
|
+
unit=t3code-desktop.service
|
|
18
|
+
runtime="${XDG_RUNTIME_DIR:?}"
|
|
19
|
+
failures="$runtime/personal-t3-health-failures"
|
|
20
|
+
no_session="$runtime/personal-t3-no-session"
|
|
21
|
+
display_restarts="$runtime/personal-t3-display-restarts"
|
|
22
|
+
pending="$runtime/personal-t3-restart-pending"
|
|
23
|
+
restarted="$runtime/personal-t3-restarted-for"
|
|
24
|
+
installed="${T3_DESKTOP_DIR:-/opt/T3 Code (Nightly)}/.personal-fleet-version"
|
|
25
|
+
here=$(dirname "$0")
|
|
9
26
|
limit=3
|
|
10
27
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
echo "
|
|
15
|
-
|
|
28
|
+
count() {
|
|
29
|
+
n=$(($(cat "$1" 2>/dev/null || echo 0) + 1))
|
|
30
|
+
echo "$n" > "$1"
|
|
31
|
+
echo "$n"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if systemctl --user is-active --quiet t3code.service ||
|
|
35
|
+
systemctl --user is-enabled --quiet t3code.service; then
|
|
36
|
+
echo "Retiring the former headless T3 server."
|
|
37
|
+
if ! systemctl --user disable --now t3code.service; then
|
|
38
|
+
echo "T3_LEGACY_SERVER_RUNNING"
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if ! systemctl --user is-active --quiet graphical-session.target; then
|
|
44
|
+
rm -f "$failures"
|
|
45
|
+
seen=$(count "$no_session")
|
|
46
|
+
echo "T3_NO_GRAPHICAL_SESSION ($seen/$limit)."
|
|
47
|
+
if [ "$seen" -ge "$limit" ]; then
|
|
48
|
+
rm -f "$no_session"
|
|
49
|
+
restarts=$(count "$display_restarts")
|
|
50
|
+
if [ "$restarts" -le "$limit" ]; then
|
|
51
|
+
echo "Restarting the display manager so auto-login starts a session ($restarts/$limit)."
|
|
52
|
+
sudo -n systemctl restart display-manager.service
|
|
53
|
+
else
|
|
54
|
+
echo "The display manager was restarted $limit times; a person must log in."
|
|
55
|
+
fi
|
|
56
|
+
fi
|
|
57
|
+
exit 1
|
|
16
58
|
fi
|
|
17
|
-
|
|
18
|
-
|
|
59
|
+
rm -f "$no_session" "$display_restarts"
|
|
60
|
+
|
|
61
|
+
if ! systemctl --user is-active --quiet "$unit"; then
|
|
62
|
+
rm -f "$failures"
|
|
19
63
|
echo "T3 is not running; starting it."
|
|
20
|
-
|
|
64
|
+
systemctl --user reset-failed "$unit" 2>/dev/null
|
|
65
|
+
exec systemctl --user start "$unit"
|
|
21
66
|
fi
|
|
22
|
-
if curl --fail --silent --max-time 10
|
|
23
|
-
|
|
67
|
+
if ! environment=$(curl --fail --silent --max-time 10 "$url"); then
|
|
68
|
+
failed=$(count "$failures")
|
|
69
|
+
echo "T3 local API did not answer ($failed/$limit)."
|
|
70
|
+
if [ "$failed" -ge "$limit" ]; then
|
|
71
|
+
rm -f "$failures"
|
|
72
|
+
echo "T3 is unresponsive; restarting it."
|
|
73
|
+
systemctl --user reset-failed "$unit" 2>/dev/null
|
|
74
|
+
exec systemctl --user restart "$unit"
|
|
75
|
+
fi
|
|
24
76
|
exit 0
|
|
25
77
|
fi
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
78
|
+
rm -f "$failures"
|
|
79
|
+
|
|
80
|
+
running=$(printf '%s' "$environment" | sed -n 's/.*"serverVersion" *: *"\([^"]*\)".*/\1/p')
|
|
81
|
+
release=$(sed -n 's/^T3-Code-\(.*\)-[a-z0-9]*\.deb$/\1/p' "$installed" 2>/dev/null)
|
|
82
|
+
reason=
|
|
83
|
+
[ -f "$pending" ] && reason="the staged policy"
|
|
84
|
+
if [ -n "$release" ] && [ "$running" != "$release" ]; then
|
|
85
|
+
# One restart per release: a server that still reports another version
|
|
86
|
+
# after it is never restarted in a loop.
|
|
87
|
+
if [ "$(cat "$restarted" 2>/dev/null)" = "$release" ]; then
|
|
88
|
+
echo "T3_RUNNING_VERSION_MISMATCH: restarted for $release, still reports ${running:-nothing}."
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
reason="release $release"
|
|
92
|
+
else
|
|
93
|
+
rm -f "$restarted"
|
|
94
|
+
fi
|
|
95
|
+
[ -z "$reason" ] && exit 0
|
|
96
|
+
if python3 -I -B "$here/t3-idle.py" --state "${T3CODE_HOME:-$HOME/.t3}/userdata/statev2.sqlite"; then
|
|
97
|
+
rm -f "$pending"
|
|
98
|
+
[ "$reason" = "release $release" ] && echo "$release" > "$restarted"
|
|
99
|
+
echo "T3 runs no agent turn; restarting it to load $reason."
|
|
100
|
+
systemctl --user reset-failed "$unit" 2>/dev/null
|
|
101
|
+
exec systemctl --user restart "$unit"
|
|
33
102
|
fi
|
|
103
|
+
echo "T3_RESTART_PENDING: agent turns are running; $reason loads at the first idle check."
|
|
104
|
+
exit 0
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Say whether the worker's T3 runs an agent turn: exit 0 when it runs none,
|
|
2
|
+
1 when it runs one and 2 when it cannot tell.
|
|
3
|
+
|
|
4
|
+
T3 offers no command that lists the running turns of every project, so this
|
|
5
|
+
reads the runs T3 records in its own state database, read-only. Anything it
|
|
6
|
+
cannot read counts as busy: a restart never ends a turn on a guess.
|
|
7
|
+
|
|
8
|
+
Usage: t3-idle.py --state ~/.t3/userdata/statev2.sqlite
|
|
9
|
+
"""
|
|
10
|
+
import argparse
|
|
11
|
+
import sqlite3
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
IDLE, BUSY, UNKNOWN = 0, 1, 2
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def running_turns(state):
|
|
18
|
+
connection = sqlite3.connect(f'file:{state}?mode=ro', uri=True, timeout=5)
|
|
19
|
+
try:
|
|
20
|
+
(count,) = connection.execute(
|
|
21
|
+
"SELECT count(*) FROM orchestration_v2_projection_runs WHERE status = 'running'"
|
|
22
|
+
).fetchone()
|
|
23
|
+
return count
|
|
24
|
+
finally:
|
|
25
|
+
connection.close()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
parser = argparse.ArgumentParser()
|
|
30
|
+
parser.add_argument('--state', required=True)
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
try:
|
|
33
|
+
count = running_turns(args.state)
|
|
34
|
+
except sqlite3.Error:
|
|
35
|
+
print('T3_IDLE_UNKNOWN')
|
|
36
|
+
return UNKNOWN
|
|
37
|
+
print(f'{count} running turn(s)')
|
|
38
|
+
return BUSY if count else IDLE
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == '__main__':
|
|
42
|
+
sys.exit(main())
|
|
@@ -41,8 +41,9 @@
|
|
|
41
41
|
mode: '0644'
|
|
42
42
|
backup: true
|
|
43
43
|
# T3 reads its policy once, at startup. A running app that has not loaded
|
|
44
|
-
# the staged policy is restarted
|
|
45
|
-
#
|
|
44
|
+
# the staged policy is restarted so every node offers the same providers,
|
|
45
|
+
# but only while it runs no agent turn (tasks/t3-restart.yml): a busy app
|
|
46
|
+
# keeps its turns and loads the policy at the first idle check.
|
|
46
47
|
- name: Check whether the desktop app is running
|
|
47
48
|
become: true
|
|
48
49
|
become_user: '{{ development_user }}'
|
|
@@ -62,15 +63,7 @@
|
|
|
62
63
|
changed_when: false
|
|
63
64
|
failed_when: false
|
|
64
65
|
when: t3_desktop_active.rc == 0
|
|
65
|
-
-
|
|
66
|
-
become: true
|
|
67
|
-
become_user: '{{ development_user }}'
|
|
68
|
-
environment:
|
|
69
|
-
XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
|
|
70
|
-
ansible.builtin.systemd_service:
|
|
71
|
-
scope: user
|
|
72
|
-
name: t3code-desktop.service
|
|
73
|
-
state: restarted
|
|
66
|
+
- ansible.builtin.import_tasks: tasks/t3-restart.yml
|
|
74
67
|
when:
|
|
75
68
|
- t3_desktop_active.rc == 0
|
|
76
69
|
- t3_policy_loaded.stdout is not search('loaded this policy\.\s*$')
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Headless Chromium for browser tests (Playwright, Storybook, end-to-end
|
|
3
|
+
# suites): the system libraries it loads, at the snapshot's exact versions,
|
|
4
|
+
# and one pinned Chrome Headless Shell build. Applied to workers that select
|
|
5
|
+
# the web tool profile; the launch below backs worker:browser.
|
|
6
|
+
- name: Install the exact libraries headless Chromium loads
|
|
7
|
+
ansible.builtin.apt:
|
|
8
|
+
name: '{{ item.key }}={{ item.value }}'
|
|
9
|
+
state: present
|
|
10
|
+
lock_timeout: '{{ aptLockTimeoutSeconds }}'
|
|
11
|
+
allow_downgrade: false
|
|
12
|
+
allow_unauthenticated: false
|
|
13
|
+
fail_on_autoremove: true
|
|
14
|
+
policy_rc_d: 101
|
|
15
|
+
environment:
|
|
16
|
+
APT_CONFIG: /etc/personal-fleet/apt.conf
|
|
17
|
+
loop: '{{ browser.packages | dict2items }}'
|
|
18
|
+
- name: Name the headless shell build of this architecture
|
|
19
|
+
ansible.builtin.set_fact:
|
|
20
|
+
browser_build: "{{ browser.headlessShell['arm64' if ansible_facts.architecture == 'aarch64' else 'amd64'] }}"
|
|
21
|
+
browser_root: '/opt/personal-fleet/browser/{{ browser.headlessShell.version }}'
|
|
22
|
+
- name: Create the package cache and the browser directory
|
|
23
|
+
ansible.builtin.file:
|
|
24
|
+
path: '{{ item }}'
|
|
25
|
+
state: directory
|
|
26
|
+
owner: root
|
|
27
|
+
group: root
|
|
28
|
+
mode: '0755'
|
|
29
|
+
loop:
|
|
30
|
+
- /var/cache/personal-fleet
|
|
31
|
+
- '{{ browser_root }}'
|
|
32
|
+
- name: Download the pinned headless shell
|
|
33
|
+
ansible.builtin.get_url:
|
|
34
|
+
url: 'https://cdn.playwright.dev/builds/cft/{{ browser.headlessShell.version }}/{{ browser_build.archive }}.zip'
|
|
35
|
+
dest: '/var/cache/personal-fleet/chrome-headless-shell-{{ browser.headlessShell.version }}-{{ browser_build.archive | basename }}.zip'
|
|
36
|
+
checksum: 'sha256:{{ browser_build.sha256 }}'
|
|
37
|
+
mode: '0644'
|
|
38
|
+
timeout: 300
|
|
39
|
+
- name: Unpack the verified headless shell
|
|
40
|
+
ansible.builtin.unarchive:
|
|
41
|
+
src: '/var/cache/personal-fleet/chrome-headless-shell-{{ browser.headlessShell.version }}-{{ browser_build.archive | basename }}.zip'
|
|
42
|
+
dest: '{{ browser_root }}'
|
|
43
|
+
remote_src: true
|
|
44
|
+
creates: '{{ browser_root }}/{{ browser_build.archive | basename }}/chrome-headless-shell'
|
|
45
|
+
- name: Launch headless as the developer, load a blank page and exit (BROWSER_LAUNCH_FAILED)
|
|
46
|
+
ansible.builtin.command:
|
|
47
|
+
argv:
|
|
48
|
+
- '{{ browser_root }}/{{ browser_build.archive | basename }}/chrome-headless-shell'
|
|
49
|
+
- --headless
|
|
50
|
+
- --no-sandbox
|
|
51
|
+
- --disable-gpu
|
|
52
|
+
- --dump-dom
|
|
53
|
+
- about:blank
|
|
54
|
+
become_user: '{{ development_user }}'
|
|
55
|
+
timeout: '{{ browser.launchTimeoutSeconds }}'
|
|
56
|
+
changed_when: false
|
|
@@ -29,3 +29,55 @@
|
|
|
29
29
|
- [docker, compose, version]
|
|
30
30
|
- [docker, buildx, version]
|
|
31
31
|
changed_when: false
|
|
32
|
+
# The engine answering is not proof that an agent can use it: run a pinned
|
|
33
|
+
# image and build a one-line image as the developer, in a fresh session that
|
|
34
|
+
# reads the docker group from scratch. The image is cached here so the same
|
|
35
|
+
# test works offline in worker:docker-run.
|
|
36
|
+
- name: Look for the cached smoke-test image
|
|
37
|
+
ansible.builtin.command:
|
|
38
|
+
argv:
|
|
39
|
+
[
|
|
40
|
+
docker,
|
|
41
|
+
image,
|
|
42
|
+
inspect,
|
|
43
|
+
--format,
|
|
44
|
+
'{{ "{{.Id}}" }}',
|
|
45
|
+
'{{ dockerSmokeTest.image }}',
|
|
46
|
+
]
|
|
47
|
+
become_user: '{{ development_user }}'
|
|
48
|
+
check_mode: false
|
|
49
|
+
changed_when: false
|
|
50
|
+
failed_when: false
|
|
51
|
+
register: docker_smoke_image
|
|
52
|
+
- name: Cache the pinned smoke-test image
|
|
53
|
+
ansible.builtin.command:
|
|
54
|
+
argv: [docker, pull, --quiet, '{{ dockerSmokeTest.image }}']
|
|
55
|
+
become_user: '{{ development_user }}'
|
|
56
|
+
when: docker_smoke_image.rc | default(0) != 0
|
|
57
|
+
changed_when: true
|
|
58
|
+
- name: Run a container as the developer, offline (DOCKER_RUN_FAILED)
|
|
59
|
+
ansible.builtin.command:
|
|
60
|
+
argv:
|
|
61
|
+
[
|
|
62
|
+
docker,
|
|
63
|
+
run,
|
|
64
|
+
--rm,
|
|
65
|
+
--network,
|
|
66
|
+
none,
|
|
67
|
+
--pull,
|
|
68
|
+
never,
|
|
69
|
+
'{{ dockerSmokeTest.image }}',
|
|
70
|
+
'true',
|
|
71
|
+
]
|
|
72
|
+
become_user: '{{ development_user }}'
|
|
73
|
+
timeout: '{{ dockerSmokeTest.timeoutSeconds }}'
|
|
74
|
+
changed_when: false
|
|
75
|
+
- name: Build a one-line image as the developer, offline (DOCKER_BUILD_FAILED)
|
|
76
|
+
ansible.builtin.command:
|
|
77
|
+
argv: [docker, build, --quiet, --pull=false, --network, none, '-']
|
|
78
|
+
stdin: |
|
|
79
|
+
FROM {{ dockerSmokeTest.image }}
|
|
80
|
+
RUN true
|
|
81
|
+
become_user: '{{ development_user }}'
|
|
82
|
+
timeout: '{{ dockerSmokeTest.timeoutSeconds }}'
|
|
83
|
+
changed_when: false
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
---
|
|
2
|
+
# The unit runs a copy installed outside every revision: a synced revision is
|
|
3
|
+
# pruned two syncs later, and the unit must not name a file that is gone.
|
|
4
|
+
- name: Create the directory of the installed recovery scripts
|
|
5
|
+
become: true
|
|
6
|
+
ansible.builtin.file:
|
|
7
|
+
path: /usr/local/lib/personal-fleet
|
|
8
|
+
state: directory
|
|
9
|
+
owner: root
|
|
10
|
+
group: root
|
|
11
|
+
mode: '0755'
|
|
12
|
+
- name: Install the T3 recovery scripts outside the revision
|
|
13
|
+
become: true
|
|
14
|
+
ansible.builtin.copy:
|
|
15
|
+
src: '{{ playbook_dir }}/{{ item }}'
|
|
16
|
+
dest: '/usr/local/lib/personal-fleet/{{ item }}'
|
|
17
|
+
owner: root
|
|
18
|
+
group: root
|
|
19
|
+
mode: '0755'
|
|
20
|
+
loop:
|
|
21
|
+
- t3-health.sh
|
|
22
|
+
- t3-idle.py
|
|
2
23
|
- name: Declare bounded recovery of the worker T3 desktop app
|
|
3
24
|
ansible.builtin.copy:
|
|
4
25
|
dest: '{{ development_home }}/.config/systemd/user/personal-worker-recovery.service'
|
|
@@ -10,7 +31,7 @@
|
|
|
10
31
|
Description=Keep the worker T3 desktop app running and responsive
|
|
11
32
|
[Service]
|
|
12
33
|
Type=oneshot
|
|
13
|
-
ExecStart=/bin/sh
|
|
34
|
+
ExecStart=/bin/sh /usr/local/lib/personal-fleet/t3-health.sh
|
|
14
35
|
TimeoutStartSec=90
|
|
15
36
|
- name: Declare persistent service recovery checks
|
|
16
37
|
ansible.builtin.copy:
|
|
@@ -37,3 +58,41 @@
|
|
|
37
58
|
daemon_reload: true
|
|
38
59
|
enabled: true
|
|
39
60
|
state: started
|
|
61
|
+
- name: Declare the work-in-progress guard of agent worktrees
|
|
62
|
+
ansible.builtin.copy:
|
|
63
|
+
dest: '{{ development_home }}/.config/systemd/user/personal-worker-wip-guard.service'
|
|
64
|
+
owner: '{{ development_user }}'
|
|
65
|
+
mode: '0600'
|
|
66
|
+
content: |
|
|
67
|
+
# Managed by personal fleet-sync: ansible/ubuntu/wip-guard.py
|
|
68
|
+
[Unit]
|
|
69
|
+
Description=Push work in progress of agent worktrees to recovery refs
|
|
70
|
+
[Service]
|
|
71
|
+
Type=oneshot
|
|
72
|
+
ExecStart=/usr/bin/python3 {{ development_source }}/ansible/ubuntu/wip-guard.py --root {{ development_home }}/.t3/worktrees --node %l
|
|
73
|
+
TimeoutStartSec=600
|
|
74
|
+
- name: Declare how often work in progress is saved
|
|
75
|
+
ansible.builtin.copy:
|
|
76
|
+
dest: '{{ development_home }}/.config/systemd/user/personal-worker-wip-guard.timer'
|
|
77
|
+
owner: '{{ development_user }}'
|
|
78
|
+
mode: '0600'
|
|
79
|
+
content: |
|
|
80
|
+
[Unit]
|
|
81
|
+
Description=Save work in progress of agent worktrees every 15 minutes
|
|
82
|
+
[Timer]
|
|
83
|
+
OnBootSec=5min
|
|
84
|
+
OnUnitActiveSec=15min
|
|
85
|
+
Persistent=true
|
|
86
|
+
[Install]
|
|
87
|
+
WantedBy=timers.target
|
|
88
|
+
- name: Enable the work-in-progress guard
|
|
89
|
+
become: true
|
|
90
|
+
become_user: '{{ development_user }}'
|
|
91
|
+
environment:
|
|
92
|
+
XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
|
|
93
|
+
ansible.builtin.systemd_service:
|
|
94
|
+
scope: user
|
|
95
|
+
name: personal-worker-wip-guard.timer
|
|
96
|
+
daemon_reload: true
|
|
97
|
+
enabled: true
|
|
98
|
+
state: started
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
StartLimitIntervalSec=0
|
|
225
225
|
[Service]
|
|
226
226
|
Type=simple
|
|
227
|
-
ExecStart=/usr/bin/python3 -I -B {{ disk_guard_source.stat.path }} --path / --path {{ development_home }} --floor-mib {{ resourceSafety.diskGuardFloorMiB }} --write-mib {{ resourceSafety.diskGuardWriteMiB }} --interval {{ resourceSafety.diskGuardIntervalSeconds }}
|
|
227
|
+
ExecStart=/usr/bin/python3 -I -B {{ disk_guard_source.stat.path }} --path / --path {{ development_home }} --floor-mib {{ resourceSafety.diskGuardFloorMiB }} --inode-floor-percent {{ resourceSafety.inodeFailFreePercent }} --write-mib {{ resourceSafety.diskGuardWriteMiB }} --interval {{ resourceSafety.diskGuardIntervalSeconds }}
|
|
228
228
|
Restart=always
|
|
229
229
|
RestartSec=5
|
|
230
230
|
OOMScoreAdjust=-1000
|
|
@@ -276,7 +276,7 @@
|
|
|
276
276
|
After=docker.service
|
|
277
277
|
[Service]
|
|
278
278
|
Type=oneshot
|
|
279
|
-
ExecStart=/usr/bin/python3 -I -B {{ disk_gc_source.stat.path }} --home {{ development_home }} --path / --path {{ development_home }} --idle-days {{ resourceSafety.diskGcIdleDays }} --pressure-mib {{ resourceSafety.diskGcPressureMiB }} --pressure-idle-hours {{ resourceSafety.diskGcPressureIdleHours }} --docker-age-hours {{ resourceSafety.diskGcDockerAgeHours }} --pressure-docker-age-hours {{ resourceSafety.diskGcPressureDockerAgeHours }}{% for cache in resourceSafety.diskGcPressureCaches %} --pressure-cache {{ cache }}{% endfor %}
|
|
279
|
+
ExecStart=/usr/bin/python3 -I -B {{ disk_gc_source.stat.path }} --home {{ development_home }} --path / --path {{ development_home }} --idle-days {{ resourceSafety.diskGcIdleDays }} --pressure-mib {{ resourceSafety.diskGcPressureMiB }} --pressure-inode-percent {{ resourceSafety.inodeWarnFreePercent }} --pressure-idle-hours {{ resourceSafety.diskGcPressureIdleHours }} --docker-age-hours {{ resourceSafety.diskGcDockerAgeHours }} --pressure-docker-age-hours {{ resourceSafety.diskGcPressureDockerAgeHours }}{% for cache in resourceSafety.diskGcPressureCaches %} --pressure-cache {{ cache }}{% endfor %}
|
|
280
280
|
|
|
281
281
|
TimeoutStartSec=2h
|
|
282
282
|
Nice=19
|
|
@@ -29,3 +29,16 @@
|
|
|
29
29
|
ansible.builtin.systemd_service:
|
|
30
30
|
daemon_reload: true
|
|
31
31
|
when: recovery_dropins.changed
|
|
32
|
+
# A skewed clock breaks TLS, tokens, apt signatures and Vault logins with
|
|
33
|
+
# errors that never mention time. Ubuntu ships one time service (chrony on
|
|
34
|
+
# newer releases, systemd-timesyncd before); whichever is installed stays
|
|
35
|
+
# enabled. A container takes its clock from its host. Measured as worker:clock.
|
|
36
|
+
- name: Inspect the time services this worker has
|
|
37
|
+
ansible.builtin.service_facts:
|
|
38
|
+
- name: Keep the clock synchronised with the time service Ubuntu ships
|
|
39
|
+
ansible.builtin.systemd_service:
|
|
40
|
+
name: '{{ item }}'
|
|
41
|
+
enabled: true
|
|
42
|
+
state: started
|
|
43
|
+
loop: "{{ (['chrony.service', 'systemd-timesyncd.service'] | select('in', ansible_facts.services) | list)[:1] }}"
|
|
44
|
+
when: ansible_facts.virtualization_type not in ['docker', 'container', 'podman', 'lxc', 'systemd-nspawn']
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Restart a running T3 only while it runs no agent turn: a restart ends every
|
|
3
|
+
# running turn. A busy T3 keeps its turns; the restart is recorded as pending
|
|
4
|
+
# (with the time it was first deferred) and personal-worker-recovery applies it
|
|
5
|
+
# at the first idle check (t3-health.sh). t3_restart_busy restarts it anyway.
|
|
6
|
+
- name: Ask T3 whether an agent turn is running
|
|
7
|
+
become: true
|
|
8
|
+
become_user: '{{ development_user }}'
|
|
9
|
+
ansible.builtin.command:
|
|
10
|
+
argv:
|
|
11
|
+
- python3
|
|
12
|
+
- -I
|
|
13
|
+
- -B
|
|
14
|
+
- '{{ playbook_dir }}/t3-idle.py'
|
|
15
|
+
- --state
|
|
16
|
+
- '{{ development_home }}/.t3/userdata/statev2.sqlite'
|
|
17
|
+
register: t3_idle
|
|
18
|
+
changed_when: false
|
|
19
|
+
failed_when: false
|
|
20
|
+
- name: Restart T3 while it runs no agent turn
|
|
21
|
+
become: true
|
|
22
|
+
become_user: '{{ development_user }}'
|
|
23
|
+
environment:
|
|
24
|
+
XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
|
|
25
|
+
ansible.builtin.systemd_service:
|
|
26
|
+
scope: user
|
|
27
|
+
name: t3code-desktop.service
|
|
28
|
+
state: restarted
|
|
29
|
+
when: t3_idle is not skipped and (t3_idle.rc == 0 or t3_restart_busy | default(false) | bool)
|
|
30
|
+
- name: Record the restart as pending while agent turns run
|
|
31
|
+
ansible.builtin.copy:
|
|
32
|
+
dest: '/run/user/{{ development_uid }}/personal-t3-restart-pending'
|
|
33
|
+
content: "{{ now(utc=true).timestamp() | int }}\n"
|
|
34
|
+
force: false
|
|
35
|
+
owner: '{{ development_user }}'
|
|
36
|
+
mode: '0600'
|
|
37
|
+
when: t3_idle is not skipped and t3_idle.rc != 0 and not (t3_restart_busy | default(false) | bool)
|