pi-background 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/extensions/background.ts +44 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-background
|
|
2
2
|
|
|
3
|
-
Lets [
|
|
3
|
+
Lets [Pi](https://pi.dev) run long commands in the background instead of waiting on them. Pi starts the job, carries on or hands back to you, and gets a message when the job finishes or prints something it's waiting for.
|
|
4
4
|
|
|
5
5
|
<picture>
|
|
6
6
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Pringled/pi-extensions/main/assets/images/pi-background-dark.gif">
|
|
@@ -13,12 +13,13 @@ pi install npm:pi-background
|
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
|
-
- Long commands like downloads, builds, test suites and training runs go to the background, and
|
|
16
|
+
- Long commands like downloads, builds, test suites and training runs go to the background, and Pi hears back with the exit code and the end of the output when they finish.
|
|
17
17
|
- While a job runs, a line above the input shows it with its elapsed time and latest output, such as `⏵ bg1 python3 train.py · 4m12s · epoch 3/10 loss 0.41`.
|
|
18
|
-
-
|
|
19
|
-
-
|
|
18
|
+
- Pi can wait for a specific line, such as a dev server's `listening on` or a training run's `epoch 3`, without checking again and again.
|
|
19
|
+
- Jobs started together are reported together: when Pi starts several jobs at once, such as benchmarks to compare, it's woken once when the last one finishes instead of after each. You can keep chatting meanwhile, and a job that runs much longer doesn't hold the others back for more than 2 minutes.
|
|
20
|
+
- Regular commands time out after 2 minutes, unless Pi asks for longer (up to 10), so Pi never hangs on a stuck command. Commands that start their own background process with `&` are left alone.
|
|
20
21
|
|
|
21
|
-
You don't need to do anything differently:
|
|
22
|
+
You don't need to do anything differently: Pi decides when a job belongs in the background. You can also ask for it, for example "run the tests in the background and tell me when they're done".
|
|
22
23
|
|
|
23
24
|
## Settings
|
|
24
25
|
|
|
@@ -32,7 +33,7 @@ The running jobs line has three levels. Set one in `~/.pi/agent/settings.json`,
|
|
|
32
33
|
|
|
33
34
|
## Tools
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
Pi gets three tools for this: `bg_run` starts a job, `bg_status` shows how it's doing and its latest output, and `bg_kill` stops it. Jobs run with the same environment and sandbox as Pi's own shell commands, and stop when you close Pi.
|
|
36
37
|
|
|
37
38
|
## License
|
|
38
39
|
|
package/extensions/background.ts
CHANGED
|
@@ -12,6 +12,8 @@ const BASH_MAX_TIMEOUT = 600;
|
|
|
12
12
|
const WAKE_DEBOUNCE_MS = 1000;
|
|
13
13
|
const MAX_WAKES = 10;
|
|
14
14
|
const MAX_SHOWN_JOBS = 3;
|
|
15
|
+
// How long finished results wait for jobs started alongside them before Pi is woken anyway
|
|
16
|
+
const GROUP_HOLD_MS = 2 * 60_000;
|
|
15
17
|
|
|
16
18
|
interface Job {
|
|
17
19
|
id: string;
|
|
@@ -24,6 +26,10 @@ interface Job {
|
|
|
24
26
|
ended?: number;
|
|
25
27
|
// The latest output line, shown while the job runs
|
|
26
28
|
lastLine?: string;
|
|
29
|
+
// The turn that started the job; jobs from the same turn are reported together
|
|
30
|
+
group: number;
|
|
31
|
+
// Started with wake_on, so it may run indefinitely and doesn't hold back the others
|
|
32
|
+
watched: boolean;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
function tail(path: string, lines: number): string {
|
|
@@ -79,8 +85,40 @@ export default function (pi: ExtensionAPI) {
|
|
|
79
85
|
const ops = createLocalBashOperations();
|
|
80
86
|
let next = 1;
|
|
81
87
|
|
|
82
|
-
const wake = (content: string, details: { headline: string; last: string; error: boolean }) =>
|
|
83
|
-
pi.sendMessage({ customType: "background-job", content, display: true, details }, { triggerTurn
|
|
88
|
+
const wake = (content: string, details: { headline: string; last: string; error: boolean }, triggerTurn = true) =>
|
|
89
|
+
pi.sendMessage({ customType: "background-job", content, display: true, details }, { triggerTurn, deliverAs: "followUp" });
|
|
90
|
+
|
|
91
|
+
// Jobs started in the same turn usually belong together, like several benchmarks to compare. While some of them
|
|
92
|
+
// still run, a successful one's result is added to the conversation without waking Pi, so it answers once at the
|
|
93
|
+
// end instead of after each job. A job that runs on too long doesn't hold the others back for more than GROUP_HOLD_MS.
|
|
94
|
+
let turn = 0;
|
|
95
|
+
pi.on("turn_start", async () => {
|
|
96
|
+
turn++;
|
|
97
|
+
});
|
|
98
|
+
const holds = new Map<number, NodeJS.Timeout>();
|
|
99
|
+
const runningInGroup = (job: Job) =>
|
|
100
|
+
[...jobs.values()].filter((other) => other !== job && other.group === job.group && other.status === "running" && !other.watched);
|
|
101
|
+
const report = (job: Job, content: string, details: { headline: string; last: string; error: boolean }) => {
|
|
102
|
+
const others = job.watched ? [] : runningInGroup(job);
|
|
103
|
+
// A failure wakes Pi straight away, so it can react before the rest of the group finishes
|
|
104
|
+
if (others.length === 0 || details.error) {
|
|
105
|
+
clearTimeout(holds.get(job.group));
|
|
106
|
+
holds.delete(job.group);
|
|
107
|
+
return wake(content, details);
|
|
108
|
+
}
|
|
109
|
+
wake(`${content}\n(Still running from the same turn: ${others.map((other) => other.id).join(", ")}; you'll be woken when they finish.)`, details, false);
|
|
110
|
+
if (holds.has(job.group)) return;
|
|
111
|
+
holds.set(job.group, setTimeout(() => {
|
|
112
|
+
holds.delete(job.group);
|
|
113
|
+
const still = runningInGroup(job);
|
|
114
|
+
if (still.length === 0) return;
|
|
115
|
+
wake(`Background jobs from the same turn are still running after ${GROUP_HOLD_MS / 60_000} minutes: ${still.map((other) => other.id).join(", ")}. The results of the ones that finished are above; you'll get a message when each of these finishes.`, {
|
|
116
|
+
headline: `${still.map((other) => other.id).join(", ")} still running`,
|
|
117
|
+
last: "",
|
|
118
|
+
error: false,
|
|
119
|
+
});
|
|
120
|
+
}, GROUP_HOLD_MS));
|
|
121
|
+
};
|
|
84
122
|
|
|
85
123
|
// Wakes the agent with output lines matching pattern, batched per second and capped per job
|
|
86
124
|
const watcher = (job: Job, pattern: RegExp) => {
|
|
@@ -209,7 +247,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
209
247
|
mkdirSync(dir, { recursive: true });
|
|
210
248
|
const log = join(dir, `${process.pid}-${id}.log`);
|
|
211
249
|
const out = createWriteStream(log);
|
|
212
|
-
const job: Job = { id, command: params.command, log, started: Date.now(), controller: new AbortController(), status: "running" };
|
|
250
|
+
const job: Job = { id, command: params.command, log, started: Date.now(), controller: new AbortController(), status: "running", group: turn, watched: Boolean(pattern) };
|
|
213
251
|
jobs.set(id, job);
|
|
214
252
|
|
|
215
253
|
const watch = pattern ? watcher(job, pattern) : undefined;
|
|
@@ -237,7 +275,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
237
275
|
out.end(() => {
|
|
238
276
|
// A kill from session shutdown has no session left to notify
|
|
239
277
|
if (job.status === "killed" && !jobs.has(id)) return;
|
|
240
|
-
|
|
278
|
+
report(job, `Background job finished: ${summary(job)}\nLast lines:\n${tail(log, TAIL_LINES)}`, {
|
|
241
279
|
headline: `${id} ${job.status === "exited" ? `exited ${job.exitCode}` : job.status} · ${elapsed(job)}`,
|
|
242
280
|
last: tail(log, 1),
|
|
243
281
|
error: job.status !== "exited" || job.exitCode !== 0,
|
|
@@ -289,6 +327,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
289
327
|
});
|
|
290
328
|
|
|
291
329
|
pi.on("session_shutdown", async () => {
|
|
330
|
+
for (const timer of holds.values()) clearTimeout(timer);
|
|
331
|
+
holds.clear();
|
|
292
332
|
for (const job of jobs.values()) {
|
|
293
333
|
if (job.status === "running") job.controller.abort();
|
|
294
334
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-background",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Thomas van Dongen",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"README.md",
|
|
14
14
|
"LICENSE"
|
|
15
15
|
],
|
|
16
|
-
"description": "Lets
|
|
16
|
+
"description": "Lets Pi run long commands in the background and wakes it when they finish, so it never blocks or hangs",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"pi-package",
|
|
19
19
|
"pi",
|