deel-local-cli 1.10.0 → 1.13.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.ko.md +1167 -1166
- package/README.md +1224 -1223
- package/bin/deel.js +1 -1
- package/package.json +2 -2
- package/src/acp/map.js +367 -362
- package/src/acp/serve.js +810 -769
- package/src/agent/budget.js +156 -153
- package/src/agent/commit.js +534 -511
- package/src/agent/compact.js +158 -31
- package/src/agent/effort.js +308 -197
- package/src/agent/evolve.js +263 -213
- package/src/agent/filemem.js +16 -2
- package/src/agent/loop.js +1728 -1516
- package/src/agent/memory.js +156 -152
- package/src/agent/mention.js +210 -210
- package/src/agent/recall.js +222 -209
- package/src/agent/session.js +1056 -948
- package/src/backend/adapter.js +1355 -798
- package/src/backend/cachemark.js +223 -0
- package/src/backend/detect.js +327 -288
- package/src/backend/http.js +460 -458
- package/src/backend/mcp.js +407 -395
- package/src/backend/price.js +260 -197
- package/src/backend/probe.js +487 -473
- package/src/backend/quota.js +274 -157
- package/src/backend/retry.js +30 -6
- package/src/backend/tokens.js +59 -0
- package/src/backend/toolfit.js +352 -325
- package/src/backend/wire.js +715 -0
- package/src/commands.js +3184 -2998
- package/src/config.js +283 -244
- package/src/i18n/en.js +545 -520
- package/src/i18n/ja.js +498 -473
- package/src/i18n/ko.js +592 -567
- package/src/i18n/zh.js +498 -473
- package/src/oneshot.js +625 -549
- package/src/pack/tar.js +154 -132
- package/src/pack/zip.js +235 -217
- package/src/plugins/manage.js +416 -336
- package/src/providers/bedrock.js +17 -0
- package/src/repl.js +2616 -2515
- package/src/safety/audit.js +148 -129
- package/src/safety/guard.js +641 -526
- package/src/safety/network.js +200 -157
- package/src/tools/fastgrep.js +229 -214
- package/src/tools/fsutil.js +291 -250
- package/src/tools/index.js +2560 -2523
- package/src/tools/jobs.js +876 -795
- package/src/tools/label.js +52 -0
- package/src/tools/spawn.js +213 -99
- package/src/tools/verify.js +358 -357
- package/src/tools/webfetch.js +418 -393
- package/src/ui/export.js +233 -217
- package/src/ui/pick.js +115 -115
- package/src/ui/status.js +617 -610
package/src/tools/jobs.js
CHANGED
|
@@ -1,795 +1,876 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* 뒤에서 계속 도는 명령.
|
|
3
|
-
*
|
|
4
|
-
* 왜 필요한가:
|
|
5
|
-
* Bash 는 명령이 **끝나야** 결과를 준다. 그래서 끝나지 않는 것을 못 돌린다 —
|
|
6
|
-
* `npm run dev`, `python -m http.server`, `vite`, `npm run watch` 가 전부 그렇다.
|
|
7
|
-
* 전에는 이걸 시키면 120초를 기다렸다가 시간 초과로 죽였다. 화면에는
|
|
8
|
-
* `시간 초과로 중단됨` 만 남는다. 모델은 서버를 못 띄운 것으로 알고 포기하거나,
|
|
9
|
-
* 더 나쁘게는 timeout 을 늘려서 다시 부른다 — 그러면 그 턴이 통째로 멈춘다.
|
|
10
|
-
*
|
|
11
|
-
* 만든 것을 **띄워서 확인**하는 것이 바이브코딩의 마지막 한 칸이다.
|
|
12
|
-
* Verify 가 "문법은 맞다" 까지 봐 주지만, 진짜로 뜨는지는 띄워 봐야 안다.
|
|
13
|
-
*
|
|
14
|
-
* 무엇을 하나:
|
|
15
|
-
* 명령을 띄우고 **바로 돌아온다.** 출력은 여기에 쌓아 두고, 모델이 Jobs 로
|
|
16
|
-
* 읽어 간다. 뜨자마자 죽는 경우(포트가 이미 물려 있다 등)가 흔해서, 띄운 직후
|
|
17
|
-
* 잠깐 기다렸다가 그 사이 나온 것을 같이 준다 — 그 몇 줄이 대부분의 답이다.
|
|
18
|
-
*
|
|
19
|
-
* 지키는 선:
|
|
20
|
-
* · 안전 검사는 Bash 와 **똑같이** 거친다. 여기가 뒷문이 되면 안 된다.
|
|
21
|
-
* (checkCommand·checkPaths 는 부르는 쪽에서 이미 거친다 — index.js 참고)
|
|
22
|
-
* · deel 이 끝나면 전부 죽인다. 남겨 두면 사람이 모르는 프로세스가 계속 돈다.
|
|
23
|
-
* 그건 '내가 안 띄운 서버가 포트를 물고 있는' 상태고, 원인을 못 찾는다.
|
|
24
|
-
* · 출력은 상한이 있다. watch 하나가 몇 시간 돌면 몇 GB 가 된다.
|
|
25
|
-
*/
|
|
26
|
-
import { spawn, execFileSync } from 'node:child_process';
|
|
27
|
-
import { decode as decodeBytes, consoleCodepage } from './encoding.js';
|
|
28
|
-
import { 셸명령 } from './shell.js';
|
|
29
|
-
import { 열쇠뺀환경 } from '../backend/mcp.js';
|
|
30
|
-
import { 말, 세말 } from '../i18n/index.js';
|
|
31
|
-
|
|
32
|
-
/* 결과 한 줄을 잇는다 — 빈 조각은 버린다(tools/index.js 의 이어 와 같은 것). */
|
|
33
|
-
const 이어 = (...조각들) => 조각들.filter((x) => x != null && String(x) !== '').join(' · ');
|
|
34
|
-
|
|
35
|
-
// 한 일감이 **들고 있을** 출력 상한. 넘으면 앞을 버리고 뒤를 남긴다 —
|
|
36
|
-
// 오래 도는 것에서 필요한 건 언제나 **방금** 나온 쪽이다.
|
|
37
|
-
const 출력상한 = 256 * 1024;
|
|
38
|
-
/*
|
|
39
|
-
* 한 번에 **모델에게 건넬** 상한. 위엣것과 다른 값이어야 한다.
|
|
40
|
-
*
|
|
41
|
-
* 둘을 같은 값으로 두면 watch 하나가 넘칠 때마다 Jobs 읽기 한 번이 256KB 를
|
|
42
|
-
* 창에 쏟는다. 8k 모델이면 그 한 번으로 창이 끝나고, 큰 창에서도 대화가
|
|
43
|
-
* 통째로 밀려 나간다. 게다가 넘친 직후에는 커서가 0 으로 돌아가 있어서
|
|
44
|
-
* (담기 참고) '새것만' 달라고 해도 통째로 온다 — 그러니 여기가 막혀 있어야 한다.
|
|
45
|
-
*
|
|
46
|
-
* Bash 의 background 쪽은 처음부터 4,000자로 줄이고 있었는데 이 길만 뚫려 있었다.
|
|
47
|
-
*/
|
|
48
|
-
const 건넬상한 = 4000;
|
|
49
|
-
// 동시에 띄워 둘 수 있는 개수. 이보다 많아지면 사람이 못 따라간다.
|
|
50
|
-
export const 최대일감 = 8;
|
|
51
|
-
/*
|
|
52
|
-
* **끝난** 일감을 몇 개까지 들고 있나.
|
|
53
|
-
*
|
|
54
|
-
* 바로 지우면 안 된다 — 마지막 출력을 읽으라고 남겨 두는 것이다. 그런데
|
|
55
|
-
* 안 지우면 짧은 명령 서른 개에 항목 서른 개가 쌓이고, 하나가 최대 256KB 를
|
|
56
|
-
* 들고 있으니 몇 MB 가 된다. 목록을 볼 때마다 그걸 전부 다시 푼다.
|
|
57
|
-
*
|
|
58
|
-
* 그래서 최근 것만 남기고 오래된 것부터 버린다. 도는 것은 여기 안 센다 —
|
|
59
|
-
* 그건 최대일감 이 따로 막는다.
|
|
60
|
-
*/
|
|
61
|
-
const 끝난것상한 = 8;
|
|
62
|
-
/*
|
|
63
|
-
* 죽인 뒤 남은 말을 얼마나 기다리나.
|
|
64
|
-
*
|
|
65
|
-
* 유닉스 쪽 SIGKILL 올려치기(800ms)보다 짧게 잡는다. 여기서 먼저 깨어나
|
|
66
|
-
* '아직 사나' 를 보고, 살아 있으면 그 자리에서 곧장 끊기 때문이다.
|
|
67
|
-
*/
|
|
68
|
-
const 죽는말기다림 = 700;
|
|
69
|
-
|
|
70
|
-
// 명령을 셸에 넘기는 방법은 tools/shell.js 한 군데서 정한다. Bash 도구와 같은 값이어야
|
|
71
|
-
// 한다 — 두 벌이 되면 한쪽만 고쳐지고, 윈도우에서 따옴표가 든 명령이 조용히 뭉개진다.
|
|
72
|
-
export { 셸명령 };
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* 어떻게 띄우나 — spawn 에 넘길 것들.
|
|
76
|
-
*
|
|
77
|
-
* `detached` 가 이 함수가 있는 이유다. 윈도우는 `taskkill /t` 가 프로세스
|
|
78
|
-
* 나무를 훑어 주지만 유닉스에는 그런 것이 없다. **띄울 때 무리(process group)를
|
|
79
|
-
* 만들어 두지 않으면 나중에 손자를 가리킬 방법이 아예 없다.**
|
|
80
|
-
*
|
|
81
|
-
* `npm run dev` 는 npm → node → vite 로 내려가고, 포트를 무는 것은 맨 아래다.
|
|
82
|
-
* sh 에 SIGTERM 만 보내면 그 아래가 그대로 남아서 — 사람 눈에 안 보이는 채로 —
|
|
83
|
-
* 계속 돈다. 나중에 고칠 수 없는 종류라 띄우는 순간에 정해 둔다.
|
|
84
|
-
*
|
|
85
|
-
* 윈도우에서는 반대로 무리를 안 만든다. detached 는 거기서 새 콘솔 창을
|
|
86
|
-
* 띄우는 쪽으로 작동해서, 조용히 돌아야 할 것이 화면에 튀어나온다.
|
|
87
|
-
*/
|
|
88
|
-
export function 띄우기옵션(cwd) {
|
|
89
|
-
const win = process.platform === 'win32';
|
|
90
|
-
return {
|
|
91
|
-
cwd,
|
|
92
|
-
// 열쇠만 빼고 나머지는 그대로 물려준다 (backend/mcp.js 열쇠뺀환경 머리말).
|
|
93
|
-
env: 열쇠뺀환경(),
|
|
94
|
-
windowsHide: true,
|
|
95
|
-
detached: !win,
|
|
96
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* 윈도우에서 pid 아래 나무를 통째로 끊는다.
|
|
102
|
-
*
|
|
103
|
-
* @returns {boolean} 나무를 **다 훑었나.** false 면 손자가 남아 있을 수 있으니,
|
|
104
|
-
* 부르는 쪽은 뿌리(cmd.exe)를 죽이지 말고 다시 와야 한다.
|
|
105
|
-
*
|
|
106
|
-
* ── 실패 두 가지를 갈라야 한다 ──────────────────────────────────────────
|
|
107
|
-
*
|
|
108
|
-
* · 「그런 프로세스 없다」 — 이미 죽은 것이다. 탈이 아니다.
|
|
109
|
-
* · **taskkill 이 제 상한에 걸려 잘렸다** — 나무를 다 못 훑었다.
|
|
110
|
-
*
|
|
111
|
-
* 뒤엣것을 앞엣것처럼 넘기고 뿌리를 죽여 버리면 손자가 영영 못 찾는 채로
|
|
112
|
-
* 남는다. 뿌리가 죽은 뒤에는 `taskkill /pid` 가 「프로세스를 못 찾았습니다」 로
|
|
113
|
-
* 끝나서 다시 부를 수조차 없다.
|
|
114
|
-
*
|
|
115
|
-
* ── 무엇으로 가르나: err.killed 가 아니다 ───────────────────────────────
|
|
116
|
-
*
|
|
117
|
-
* 처음에는 `!err?.killed` 로 갈랐다. **그게 늘 참이었다.** 비동기 execFile 의
|
|
118
|
-
* 콜백은 상한에 걸리면 err.killed 를 true 로 주는데, execFileSync 는 안 준다.
|
|
119
|
-
* 그래서 「나무를 다 못 훑었다」 가 한 번도 안 나왔고, 뿌리를 살려 두는 분기는
|
|
120
|
-
* 처음부터 닿지 않는 코드였다 — 이 판의 대표 기능이 통째로 헛돈 셈이다.
|
|
121
|
-
*
|
|
122
|
-
* 실제로 재 보고 갈랐다(윈도우 11 · Node 20):
|
|
123
|
-
*
|
|
124
|
-
* 상한에 걸림 killed=undefined signal=SIGTERM code=ETIMEDOUT status=null
|
|
125
|
-
* 없는 pid 를 죽임 killed=undefined signal=null code=undefined status=128
|
|
126
|
-
*
|
|
127
|
-
* 갈리는 자리는 **signal 과 code** 다. 아래 검사(test/jobs.test.js 의
|
|
128
|
-
* '★ taskkill 이 잘린 것과 이미 죽은 것을 가른다')가 이 값을 못박아 둔다.
|
|
129
|
-
*/
|
|
130
|
-
export function 나무끊기(pid, 상한 = 1500) {
|
|
131
|
-
if (process.platform !== 'win32' || !pid) return true;
|
|
132
|
-
try {
|
|
133
|
-
execFileSync('taskkill', ['/pid', String(pid), '/t', '/f'],
|
|
134
|
-
{ windowsHide: true, stdio: 'ignore', timeout: 상한 });
|
|
135
|
-
return true;
|
|
136
|
-
} catch (err) {
|
|
137
|
-
return !잘렸나(err);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** 이 오류가 「상한에 걸려 잘렸다」 인가. 아니면 그냥 0 이 아닌 값으로 끝난 것이다. */
|
|
142
|
-
export function 잘렸나(err) {
|
|
143
|
-
return err?.code === 'ETIMEDOUT' || err?.signal != null || err?.killed === true;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* 유닉스에서 pid 가 이끄는 **무리째** 죽인다.
|
|
148
|
-
*
|
|
149
|
-
* 음수 pid 는 '그 무리 전부' 라는 뜻이다 — 띄울 때 detached 로 무리를 만들어
|
|
150
|
-
* 둔 것과 짝이다. 이걸 안 하고 kid.kill() 만 하면 sh 나 npm 만 죽고 그 아래
|
|
151
|
-
* vite 가 포트를 문 채로 남는다. 무엇이 물고 있는지 못 찾는, 제일 나쁜 자리다.
|
|
152
|
-
*
|
|
153
|
-
* 먼저 곱게 말하고(TERM), 안 들으면 끊는다(KILL). 곧장 KILL 로 가면 dev 서버가
|
|
154
|
-
* 임시 파일이나 소켓을 치울 틈이 없다.
|
|
155
|
-
*
|
|
156
|
-
* **띄울 때 무리를 안 만들었으면 여기서 할 수 있는 것이 없다.** 나중에 무리를
|
|
157
|
-
* 만들어 줄 방법은 없다 — 그래서 이 판단이 띄우는 자리와 짝을 이뤄야 한다.
|
|
158
|
-
* (Bash 도구가 한동안 이 짝을 안 맞춰서, 윈도우만 고쳐지고 유닉스는 그대로였다.)
|
|
159
|
-
*/
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
this
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
j
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1
|
+
/*
|
|
2
|
+
* 뒤에서 계속 도는 명령.
|
|
3
|
+
*
|
|
4
|
+
* 왜 필요한가:
|
|
5
|
+
* Bash 는 명령이 **끝나야** 결과를 준다. 그래서 끝나지 않는 것을 못 돌린다 —
|
|
6
|
+
* `npm run dev`, `python -m http.server`, `vite`, `npm run watch` 가 전부 그렇다.
|
|
7
|
+
* 전에는 이걸 시키면 120초를 기다렸다가 시간 초과로 죽였다. 화면에는
|
|
8
|
+
* `시간 초과로 중단됨` 만 남는다. 모델은 서버를 못 띄운 것으로 알고 포기하거나,
|
|
9
|
+
* 더 나쁘게는 timeout 을 늘려서 다시 부른다 — 그러면 그 턴이 통째로 멈춘다.
|
|
10
|
+
*
|
|
11
|
+
* 만든 것을 **띄워서 확인**하는 것이 바이브코딩의 마지막 한 칸이다.
|
|
12
|
+
* Verify 가 "문법은 맞다" 까지 봐 주지만, 진짜로 뜨는지는 띄워 봐야 안다.
|
|
13
|
+
*
|
|
14
|
+
* 무엇을 하나:
|
|
15
|
+
* 명령을 띄우고 **바로 돌아온다.** 출력은 여기에 쌓아 두고, 모델이 Jobs 로
|
|
16
|
+
* 읽어 간다. 뜨자마자 죽는 경우(포트가 이미 물려 있다 등)가 흔해서, 띄운 직후
|
|
17
|
+
* 잠깐 기다렸다가 그 사이 나온 것을 같이 준다 — 그 몇 줄이 대부분의 답이다.
|
|
18
|
+
*
|
|
19
|
+
* 지키는 선:
|
|
20
|
+
* · 안전 검사는 Bash 와 **똑같이** 거친다. 여기가 뒷문이 되면 안 된다.
|
|
21
|
+
* (checkCommand·checkPaths 는 부르는 쪽에서 이미 거친다 — index.js 참고)
|
|
22
|
+
* · deel 이 끝나면 전부 죽인다. 남겨 두면 사람이 모르는 프로세스가 계속 돈다.
|
|
23
|
+
* 그건 '내가 안 띄운 서버가 포트를 물고 있는' 상태고, 원인을 못 찾는다.
|
|
24
|
+
* · 출력은 상한이 있다. watch 하나가 몇 시간 돌면 몇 GB 가 된다.
|
|
25
|
+
*/
|
|
26
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
27
|
+
import { decode as decodeBytes, consoleCodepage } from './encoding.js';
|
|
28
|
+
import { 셸명령 } from './shell.js';
|
|
29
|
+
import { 열쇠뺀환경 } from '../backend/mcp.js';
|
|
30
|
+
import { 말, 세말 } from '../i18n/index.js';
|
|
31
|
+
|
|
32
|
+
/* 결과 한 줄을 잇는다 — 빈 조각은 버린다(tools/index.js 의 이어 와 같은 것). */
|
|
33
|
+
const 이어 = (...조각들) => 조각들.filter((x) => x != null && String(x) !== '').join(' · ');
|
|
34
|
+
|
|
35
|
+
// 한 일감이 **들고 있을** 출력 상한. 넘으면 앞을 버리고 뒤를 남긴다 —
|
|
36
|
+
// 오래 도는 것에서 필요한 건 언제나 **방금** 나온 쪽이다.
|
|
37
|
+
const 출력상한 = 256 * 1024;
|
|
38
|
+
/*
|
|
39
|
+
* 한 번에 **모델에게 건넬** 상한. 위엣것과 다른 값이어야 한다.
|
|
40
|
+
*
|
|
41
|
+
* 둘을 같은 값으로 두면 watch 하나가 넘칠 때마다 Jobs 읽기 한 번이 256KB 를
|
|
42
|
+
* 창에 쏟는다. 8k 모델이면 그 한 번으로 창이 끝나고, 큰 창에서도 대화가
|
|
43
|
+
* 통째로 밀려 나간다. 게다가 넘친 직후에는 커서가 0 으로 돌아가 있어서
|
|
44
|
+
* (담기 참고) '새것만' 달라고 해도 통째로 온다 — 그러니 여기가 막혀 있어야 한다.
|
|
45
|
+
*
|
|
46
|
+
* Bash 의 background 쪽은 처음부터 4,000자로 줄이고 있었는데 이 길만 뚫려 있었다.
|
|
47
|
+
*/
|
|
48
|
+
const 건넬상한 = 4000;
|
|
49
|
+
// 동시에 띄워 둘 수 있는 개수. 이보다 많아지면 사람이 못 따라간다.
|
|
50
|
+
export const 최대일감 = 8;
|
|
51
|
+
/*
|
|
52
|
+
* **끝난** 일감을 몇 개까지 들고 있나.
|
|
53
|
+
*
|
|
54
|
+
* 바로 지우면 안 된다 — 마지막 출력을 읽으라고 남겨 두는 것이다. 그런데
|
|
55
|
+
* 안 지우면 짧은 명령 서른 개에 항목 서른 개가 쌓이고, 하나가 최대 256KB 를
|
|
56
|
+
* 들고 있으니 몇 MB 가 된다. 목록을 볼 때마다 그걸 전부 다시 푼다.
|
|
57
|
+
*
|
|
58
|
+
* 그래서 최근 것만 남기고 오래된 것부터 버린다. 도는 것은 여기 안 센다 —
|
|
59
|
+
* 그건 최대일감 이 따로 막는다.
|
|
60
|
+
*/
|
|
61
|
+
const 끝난것상한 = 8;
|
|
62
|
+
/*
|
|
63
|
+
* 죽인 뒤 남은 말을 얼마나 기다리나.
|
|
64
|
+
*
|
|
65
|
+
* 유닉스 쪽 SIGKILL 올려치기(800ms)보다 짧게 잡는다. 여기서 먼저 깨어나
|
|
66
|
+
* '아직 사나' 를 보고, 살아 있으면 그 자리에서 곧장 끊기 때문이다.
|
|
67
|
+
*/
|
|
68
|
+
const 죽는말기다림 = 700;
|
|
69
|
+
|
|
70
|
+
// 명령을 셸에 넘기는 방법은 tools/shell.js 한 군데서 정한다. Bash 도구와 같은 값이어야
|
|
71
|
+
// 한다 — 두 벌이 되면 한쪽만 고쳐지고, 윈도우에서 따옴표가 든 명령이 조용히 뭉개진다.
|
|
72
|
+
export { 셸명령 };
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 어떻게 띄우나 — spawn 에 넘길 것들.
|
|
76
|
+
*
|
|
77
|
+
* `detached` 가 이 함수가 있는 이유다. 윈도우는 `taskkill /t` 가 프로세스
|
|
78
|
+
* 나무를 훑어 주지만 유닉스에는 그런 것이 없다. **띄울 때 무리(process group)를
|
|
79
|
+
* 만들어 두지 않으면 나중에 손자를 가리킬 방법이 아예 없다.**
|
|
80
|
+
*
|
|
81
|
+
* `npm run dev` 는 npm → node → vite 로 내려가고, 포트를 무는 것은 맨 아래다.
|
|
82
|
+
* sh 에 SIGTERM 만 보내면 그 아래가 그대로 남아서 — 사람 눈에 안 보이는 채로 —
|
|
83
|
+
* 계속 돈다. 나중에 고칠 수 없는 종류라 띄우는 순간에 정해 둔다.
|
|
84
|
+
*
|
|
85
|
+
* 윈도우에서는 반대로 무리를 안 만든다. detached 는 거기서 새 콘솔 창을
|
|
86
|
+
* 띄우는 쪽으로 작동해서, 조용히 돌아야 할 것이 화면에 튀어나온다.
|
|
87
|
+
*/
|
|
88
|
+
export function 띄우기옵션(cwd) {
|
|
89
|
+
const win = process.platform === 'win32';
|
|
90
|
+
return {
|
|
91
|
+
cwd,
|
|
92
|
+
// 열쇠만 빼고 나머지는 그대로 물려준다 (backend/mcp.js 열쇠뺀환경 머리말).
|
|
93
|
+
env: 열쇠뺀환경(),
|
|
94
|
+
windowsHide: true,
|
|
95
|
+
detached: !win,
|
|
96
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 윈도우에서 pid 아래 나무를 통째로 끊는다.
|
|
102
|
+
*
|
|
103
|
+
* @returns {boolean} 나무를 **다 훑었나.** false 면 손자가 남아 있을 수 있으니,
|
|
104
|
+
* 부르는 쪽은 뿌리(cmd.exe)를 죽이지 말고 다시 와야 한다.
|
|
105
|
+
*
|
|
106
|
+
* ── 실패 두 가지를 갈라야 한다 ──────────────────────────────────────────
|
|
107
|
+
*
|
|
108
|
+
* · 「그런 프로세스 없다」 — 이미 죽은 것이다. 탈이 아니다.
|
|
109
|
+
* · **taskkill 이 제 상한에 걸려 잘렸다** — 나무를 다 못 훑었다.
|
|
110
|
+
*
|
|
111
|
+
* 뒤엣것을 앞엣것처럼 넘기고 뿌리를 죽여 버리면 손자가 영영 못 찾는 채로
|
|
112
|
+
* 남는다. 뿌리가 죽은 뒤에는 `taskkill /pid` 가 「프로세스를 못 찾았습니다」 로
|
|
113
|
+
* 끝나서 다시 부를 수조차 없다.
|
|
114
|
+
*
|
|
115
|
+
* ── 무엇으로 가르나: err.killed 가 아니다 ───────────────────────────────
|
|
116
|
+
*
|
|
117
|
+
* 처음에는 `!err?.killed` 로 갈랐다. **그게 늘 참이었다.** 비동기 execFile 의
|
|
118
|
+
* 콜백은 상한에 걸리면 err.killed 를 true 로 주는데, execFileSync 는 안 준다.
|
|
119
|
+
* 그래서 「나무를 다 못 훑었다」 가 한 번도 안 나왔고, 뿌리를 살려 두는 분기는
|
|
120
|
+
* 처음부터 닿지 않는 코드였다 — 이 판의 대표 기능이 통째로 헛돈 셈이다.
|
|
121
|
+
*
|
|
122
|
+
* 실제로 재 보고 갈랐다(윈도우 11 · Node 20):
|
|
123
|
+
*
|
|
124
|
+
* 상한에 걸림 killed=undefined signal=SIGTERM code=ETIMEDOUT status=null
|
|
125
|
+
* 없는 pid 를 죽임 killed=undefined signal=null code=undefined status=128
|
|
126
|
+
*
|
|
127
|
+
* 갈리는 자리는 **signal 과 code** 다. 아래 검사(test/jobs.test.js 의
|
|
128
|
+
* '★ taskkill 이 잘린 것과 이미 죽은 것을 가른다')가 이 값을 못박아 둔다.
|
|
129
|
+
*/
|
|
130
|
+
export function 나무끊기(pid, 상한 = 1500) {
|
|
131
|
+
if (process.platform !== 'win32' || !pid) return true;
|
|
132
|
+
try {
|
|
133
|
+
execFileSync('taskkill', ['/pid', String(pid), '/t', '/f'],
|
|
134
|
+
{ windowsHide: true, stdio: 'ignore', timeout: 상한 });
|
|
135
|
+
return true;
|
|
136
|
+
} catch (err) {
|
|
137
|
+
return !잘렸나(err);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 이 오류가 「상한에 걸려 잘렸다」 인가. 아니면 그냥 0 이 아닌 값으로 끝난 것이다. */
|
|
142
|
+
export function 잘렸나(err) {
|
|
143
|
+
return err?.code === 'ETIMEDOUT' || err?.signal != null || err?.killed === true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 유닉스에서 pid 가 이끄는 **무리째** 죽인다.
|
|
148
|
+
*
|
|
149
|
+
* 음수 pid 는 '그 무리 전부' 라는 뜻이다 — 띄울 때 detached 로 무리를 만들어
|
|
150
|
+
* 둔 것과 짝이다. 이걸 안 하고 kid.kill() 만 하면 sh 나 npm 만 죽고 그 아래
|
|
151
|
+
* vite 가 포트를 문 채로 남는다. 무엇이 물고 있는지 못 찾는, 제일 나쁜 자리다.
|
|
152
|
+
*
|
|
153
|
+
* 먼저 곱게 말하고(TERM), 안 들으면 끊는다(KILL). 곧장 KILL 로 가면 dev 서버가
|
|
154
|
+
* 임시 파일이나 소켓을 치울 틈이 없다.
|
|
155
|
+
*
|
|
156
|
+
* **띄울 때 무리를 안 만들었으면 여기서 할 수 있는 것이 없다.** 나중에 무리를
|
|
157
|
+
* 만들어 줄 방법은 없다 — 그래서 이 판단이 띄우는 자리와 짝을 이뤄야 한다.
|
|
158
|
+
* (Bash 도구가 한동안 이 짝을 안 맞춰서, 윈도우만 고쳐지고 유닉스는 그대로였다.)
|
|
159
|
+
*/
|
|
160
|
+
/**
|
|
161
|
+
* 그 무리가 아직 살아 있나. 죽었으면 false.
|
|
162
|
+
*
|
|
163
|
+
* 무리로 못 물어보면 **그 하나에게** 물어본다 — 위 신호주기() 와 같은 까닭이다.
|
|
164
|
+
* 무리가 없는 판에서 「무리가 없다 = 다 죽었다」 로 읽으면, 살아 있는 자식을
|
|
165
|
+
* 죽은 것으로 세고 그냥 지나간다.
|
|
166
|
+
*/
|
|
167
|
+
function 무리살아있나(pid) {
|
|
168
|
+
try { process.kill(-pid, 0); return true; } catch { /* 무리가 없다 */ }
|
|
169
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 동기로 잠깐 쉰다.
|
|
174
|
+
*
|
|
175
|
+
* `setTimeout` 이 아니라 이걸 쓰는 까닭은 아래 무리끊기 머리말에 있다 —
|
|
176
|
+
* 이 자리는 프로그램이 끝나는 길에서도 지나가고, 그 자리에서는 비동기
|
|
177
|
+
* 일감이 하나도 안 돈다.
|
|
178
|
+
*/
|
|
179
|
+
function 동기로쉬기(ms) {
|
|
180
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
|
|
181
|
+
catch { /* SharedArrayBuffer 가 막힌 판이면 그냥 지나간다 */ }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 신호를 보낸다. **무리째 못 보내면 그 하나에게라도 보낸다.**
|
|
186
|
+
*
|
|
187
|
+
* ── 왜 두 번 시도하나 ──────────────────────────────────────────────────
|
|
188
|
+
*
|
|
189
|
+
* `-pid` 는 「그 무리 전부」 라는 뜻이고, 그 무리는 띄울 때 `detached` 로
|
|
190
|
+
* 만들어 둔 것이다. 그런데 그게 늘 있으리라 믿으면 안 된다 —
|
|
191
|
+
*
|
|
192
|
+
* · `detached` 가 안 먹은 판 (컨테이너 안, 이상한 실행 환경)
|
|
193
|
+
* · 이미 무리 우두머리가 죽고 남은 놈이 딴 무리로 넘어간 자리
|
|
194
|
+
*
|
|
195
|
+
* 이런 자리에서 `kill(-pid, …)` 는 ESRCH 로 튕긴다. 그러면 여태는 그
|
|
196
|
+
* 자리에서 **아무것도 안 하고 돌아갔다.** 무리로 못 보냈다는 것이 「보낼
|
|
197
|
+
* 것이 없다」 는 뜻은 아닌데, 그렇게 다뤘다. 무리로 못 보내면 적어도 우리가
|
|
198
|
+
* 아는 그 하나(자식)에게는 보낸다 — 그게 손자를 못 잡더라도, 아무것도 안
|
|
199
|
+
* 하는 것보다는 낫다.
|
|
200
|
+
*
|
|
201
|
+
* @returns {boolean} 하나라도 닿았으면 true
|
|
202
|
+
*/
|
|
203
|
+
function 신호주기(pid, 신호) {
|
|
204
|
+
let 닿았나 = false;
|
|
205
|
+
try { process.kill(-pid, 신호); 닿았나 = true; } catch { /* 무리가 없다 */ }
|
|
206
|
+
if (!닿았나) {
|
|
207
|
+
try { process.kill(pid, 신호); 닿았나 = true; } catch { /* 그마저 죽었다 */ }
|
|
208
|
+
}
|
|
209
|
+
return 닿았나;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function 무리끊기(pid, { 곧장 = false, 늦게 = 800 } = {}) {
|
|
213
|
+
if (process.platform === 'win32' || !pid) return;
|
|
214
|
+
if (곧장) {
|
|
215
|
+
신호주기(pid, 'SIGKILL');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
신호주기(pid, 'SIGTERM');
|
|
219
|
+
|
|
220
|
+
/*
|
|
221
|
+
* ── 올려치기를 **동기로** 한다 ────────────────────────────────────────
|
|
222
|
+
*
|
|
223
|
+
* 여기가 `setTimeout(…, 늦게).unref()` 였다. 두 가지가 어긋났다.
|
|
224
|
+
*
|
|
225
|
+
* 1. `unref()` 한 타이머는 **프로세스를 안 붙잡는다.** 시간 초과로
|
|
226
|
+
* 끊고 곧바로 끝나는 길에서는 그 타이머가 아예 안 뜬다. 즉 SIGTERM
|
|
227
|
+
* 을 무시하는 손자(제 신호 처리기를 단 dev 서버·watch 프로세스)는
|
|
228
|
+
* 그대로 살아남는다. 「죽이라고 말은 했는데 안 죽는」 자리다.
|
|
229
|
+
* 2. 부르는 쪽은 이 함수가 **돌아오면 끝난 줄로 안다.** 실제로는 아직
|
|
230
|
+
* TERM 만 보낸 상태라, 바로 뒤에서 「진짜 멈췄나」 를 보면 살아 있다.
|
|
231
|
+
* 이 비대칭이 윈도우와 유닉스 사이에도 있었다 — 윈도우 쪽은
|
|
232
|
+
* taskkill 을 동기로 기다린다(나무죽이기 머리말).
|
|
233
|
+
*
|
|
234
|
+
* 그래서 곱게 말한 뒤 **짧게 나눠 지켜보다가**, 그래도 살아 있으면 그
|
|
235
|
+
* 자리에서 끊는다. 얌전히 끝나는 무리는 첫 20ms 에 사라지므로 보통은
|
|
236
|
+
* 거의 안 기다린다. 안 죽는 놈에게만 `늦게` 만큼 쓴다.
|
|
237
|
+
*/
|
|
238
|
+
const 조각 = 20;
|
|
239
|
+
for (let 쓴시간 = 0; 쓴시간 < 늦게; 쓴시간 += 조각) {
|
|
240
|
+
if (!무리살아있나(pid)) return;
|
|
241
|
+
동기로쉬기(조각);
|
|
242
|
+
}
|
|
243
|
+
/*
|
|
244
|
+
* 마지막 한 방은 **묻지 않고** 보낸다.
|
|
245
|
+
*
|
|
246
|
+
* 여기가 `if (무리살아있나(pid))` 였다. 그 물음은 틀릴 수 있다 — 권한이
|
|
247
|
+
* 없으면(EPERM) 살아 있는데도 「없다」 가 나오고, 그러면 안 죽이고 지나간다.
|
|
248
|
+
* 이미 죽은 것에 SIGKILL 을 보내는 값은 예외 하나뿐이고 그건 여기서 삼킨다.
|
|
249
|
+
* 안 죽은 것을 살려 보내는 값은 포트를 문 프로세스가 남는 것이다. 값이 다르다.
|
|
250
|
+
*/
|
|
251
|
+
신호주기(pid, 'SIGKILL');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 프로세스 나무를 통째로 끝낸다. 자식만 죽이면 손자가 남아 포트를 계속 문다.
|
|
256
|
+
*
|
|
257
|
+
* 윈도우에서 taskkill 을 **기다린다.** 전에는 execFile(비동기)로 불렀는데,
|
|
258
|
+
* 그러면 두 가지가 조용히 어긋난다.
|
|
259
|
+
*
|
|
260
|
+
* 1. 바로 아래 kid.kill() 이 먼저 돌아 cmd 를 죽인다. 나무의 뿌리가 없어진
|
|
261
|
+
* 뒤에 taskkill 이 뜨므로 손자 — `npm run dev` 의 진짜 서버 — 를 못 찾고,
|
|
262
|
+
* 그놈은 그대로 남아 포트를 문다. 죽이라고 말은 했는데 안 죽는다.
|
|
263
|
+
* 2. process 의 'exit' 에서 부를 때는 **아예 안 돈다.** 그 자리에서는
|
|
264
|
+
* 비동기 일감이 하나도 실행되지 않는다. 끄려고 걸어 둔 마지막 그물이
|
|
265
|
+
* 정작 제일 흔한 끝맺음(그냥 종료)에서 통째로 비어 있었던 셈이다.
|
|
266
|
+
*
|
|
267
|
+
* 그래서 동기로 부른다. 여기서 멈추는 몇십 ms 로 '안 죽은 서버' 를 없앤다.
|
|
268
|
+
* test/jobs.test.js 의 '프로세스가 진짜 멈춘다' 가 이 자리를 지킨다.
|
|
269
|
+
*/
|
|
270
|
+
function 나무죽이기(kid, { 파이프끊기: 끊을까 = true, 곧장 = false, 상한 = 1500, 뿌리살리기 = true } = {}) {
|
|
271
|
+
if (!kid) return true;
|
|
272
|
+
let 나무끊었나 = true;
|
|
273
|
+
if (process.platform === 'win32' && kid.pid) {
|
|
274
|
+
/*
|
|
275
|
+
* 상한은 부르는 쪽이 정한다. 여기는 끝날 때도 지나가는 자리라
|
|
276
|
+
* (process 'exit'), 일감마다 몇 초씩 기다리면 deel 이 멈춘 것처럼 보인다.
|
|
277
|
+
* 그래서 기본은 짧다. 사람이 「끝내」 라고 시킨 자리에서는 길게 준다 —
|
|
278
|
+
* 거기서는 몇백 ms 늦는 것보다 안 죽는 것이 훨씬 나쁘다.
|
|
279
|
+
*/
|
|
280
|
+
나무끊었나 = 나무끊기(kid.pid, 상한);
|
|
281
|
+
/*
|
|
282
|
+
* 나무를 다 못 훑었으면 뿌리를 살려 둔다. 부르는 쪽이 다시 온다.
|
|
283
|
+
*
|
|
284
|
+
* 딱 한 자리만 예외다 — 더 이상 다시 올 수 없는 자리(프로그램이 끝나는
|
|
285
|
+
* 길)에서는 뿌리라도 죽인다. 둘 다 살려 두는 것보다는 낫다.
|
|
286
|
+
*/
|
|
287
|
+
if (!나무끊었나 && 뿌리살리기) return false;
|
|
288
|
+
} else if (kid.pid) {
|
|
289
|
+
무리끊기(kid.pid, { 곧장 });
|
|
290
|
+
}
|
|
291
|
+
try { kid.kill(); } catch { /* 이미 죽음 */ }
|
|
292
|
+
if (끊을까) 파이프끊기(kid);
|
|
293
|
+
try { kid.unref(); } catch { /* 없으면 그만 */ }
|
|
294
|
+
return 나무끊었나;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 파이프를 놓는다.
|
|
299
|
+
*
|
|
300
|
+
* 아이가 죽어도 stdout·stderr 를 붙들고 있으면 그 손잡이가 이벤트 루프를
|
|
301
|
+
* 붙잡는다. unref() 는 프로세스 손잡이만 놓지 파이프는 안 놓는다.
|
|
302
|
+
* 이걸 빠뜨리면 deel 이 할 일을 다 하고도 안 끝난다 — 사람 눈에는 멈춘 것이다.
|
|
303
|
+
*
|
|
304
|
+
* 다만 **죽이자마자 끊으면 안 된다.** 그 순간 파이프에는 아직 안 읽힌 것이
|
|
305
|
+
* 남아 있고, 죽기 직전에 나온 몇 줄이 대개 제일 중요하다(서버가 뻗으며 남긴
|
|
306
|
+
* 스택 트레이스가 거기 있다). 그래서 끝내기() 는 잠깐 기다렸다가 이걸 부른다.
|
|
307
|
+
*/
|
|
308
|
+
function 파이프끊기(kid) {
|
|
309
|
+
try { kid?.stdout?.destroy(); } catch { /* 이미 닫힘 */ }
|
|
310
|
+
try { kid?.stderr?.destroy(); } catch { /* 이미 닫힘 */ }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** 아직 살아 있나. 종료코드도 시그널도 없으면 안 죽은 것이다. */
|
|
314
|
+
function 아직사나(kid) {
|
|
315
|
+
return !!kid && kid.exitCode == null && kid.signalCode == null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 죽인 뒤, 남은 말이 다 나오기를 잠깐 기다린다.
|
|
320
|
+
*
|
|
321
|
+
* 'close' 는 프로세스가 끝나고 **파이프까지 다 비워졌을 때** 온다. 그래서
|
|
322
|
+
* 이걸 기다리면 죽는 순간 뱉은 것까지 손에 들어온다.
|
|
323
|
+
*
|
|
324
|
+
* 무한정 기다리지는 않는다 — 안 죽는 놈 하나가 그 턴을 통째로 잡아먹으면
|
|
325
|
+
* 안 된다. 그리고 여기 시계는 unref 하지 않는다. 이 기다림이 지금 이벤트
|
|
326
|
+
* 루프가 살아 있을 유일한 이유일 수 있어서, 놓아 버리면 그대로 끝난다.
|
|
327
|
+
*/
|
|
328
|
+
function 죽는말기다리기(kid, ms) {
|
|
329
|
+
return new Promise((끝) => {
|
|
330
|
+
if (!kid || ms <= 0) return 끝();
|
|
331
|
+
let 됐나 = false;
|
|
332
|
+
const 마치기 = () => { if (됐나) return; 됐나 = true; clearTimeout(시계); 끝(); };
|
|
333
|
+
const 시계 = setTimeout(마치기, ms);
|
|
334
|
+
kid.once('close', 마치기);
|
|
335
|
+
kid.once('error', 마치기);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
class 일감 {
|
|
340
|
+
constructor(번호, 명령, 설명) {
|
|
341
|
+
this.번호 = 번호;
|
|
342
|
+
this.명령 = 명령;
|
|
343
|
+
this.설명 = 설명 ?? null;
|
|
344
|
+
this.띄운때 = Date.now();
|
|
345
|
+
this.상태 = '도는중'; // 도는중 | 끝남 | 죽임
|
|
346
|
+
this.종료코드 = null;
|
|
347
|
+
this.시그널 = null;
|
|
348
|
+
this.조각들 = [];
|
|
349
|
+
this.바이트 = 0;
|
|
350
|
+
// 이 일감이 앞을 버린 적이 **있다**. 통째로 달라고 할 때의 사실이라,
|
|
351
|
+
// 한 번 서면 안 내려간다 — 지금 들고 있는 것이 처음부터가 아니라는 뜻이다.
|
|
352
|
+
this.앞잘림 = false;
|
|
353
|
+
// 지난번 읽어 간 뒤로 버렸다. **이번에 건네는 토막**에 대한 사실이라,
|
|
354
|
+
// 한 번 말하면 내린다. 이 둘을 한 칸으로 합치면 안 된다 (읽기() 참고).
|
|
355
|
+
this.막버렸다 = false;
|
|
356
|
+
this.읽은글자 = 0;
|
|
357
|
+
// 한 번 정하면 안 바꾼다. 바뀌면 읽은 자리가 조용히 어긋난다 (전체글 참고).
|
|
358
|
+
this.인코딩 = null;
|
|
359
|
+
// 담을 때마다 는다. 푼 글을 다시 써도 되는지를 이 숫자로 가른다 —
|
|
360
|
+
// 바이트 수로 가르면 버린 만큼 다시 담겼을 때 같은 값이 되어 못 알아챈다.
|
|
361
|
+
this.판 = 0;
|
|
362
|
+
this.푼것 = null;
|
|
363
|
+
this.kid = null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
담기(buf) {
|
|
367
|
+
if (!buf?.length) return;
|
|
368
|
+
this.조각들.push(Buffer.from(buf));
|
|
369
|
+
this.바이트 += buf.length;
|
|
370
|
+
this.판++;
|
|
371
|
+
if (this.바이트 <= 출력상한) return;
|
|
372
|
+
/*
|
|
373
|
+
* 앞을 버린다.
|
|
374
|
+
*
|
|
375
|
+
* 버리면 이미 읽은 글자 수가 뜻을 잃는다 — 앞이 사라졌으니 같은 자리가
|
|
376
|
+
* 아니다. 억지로 맞추려 들면 조용히 어긋난 글을 주게 되므로, 커서를
|
|
377
|
+
* 0 으로 돌리고 **앞이 잘렸다고 말한다.** 겹쳐 보이는 편이 낫다.
|
|
378
|
+
*/
|
|
379
|
+
while (this.바이트 > 출력상한 && this.조각들.length > 1) {
|
|
380
|
+
this.바이트 -= this.조각들.shift().length;
|
|
381
|
+
}
|
|
382
|
+
this.앞잘림 = true;
|
|
383
|
+
this.막버렸다 = true;
|
|
384
|
+
this.읽은글자 = 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/*
|
|
388
|
+
* 쌓인 것을 글로 푼다.
|
|
389
|
+
*
|
|
390
|
+
* 조각마다 풀면 여러 바이트짜리 글자가 조각 경계에서 쪼개져 깨진다.
|
|
391
|
+
* 통째로 이어 붙여 한 번에 푼다.
|
|
392
|
+
*
|
|
393
|
+
* **인코딩은 한 번 정하면 안 바꾼다.** 이게 중요하다 — 읽은 자리(읽은글자)를
|
|
394
|
+
* 글자 수로 세고 있는데, 인코딩이 중간에 바뀌면 같은 앞부분이 다른 글자 수로
|
|
395
|
+
* 풀린다. 그러면 커서가 가리키던 자리가 조용히 어긋나서, 이미 준 글을 다시
|
|
396
|
+
* 주거나 새 글을 건너뛴다. 오류도 안 나고 로그에도 안 남는다.
|
|
397
|
+
*
|
|
398
|
+
* 실제로 그럴 수 있다. 인코딩 판정은 **버퍼 전체를 보는 어림짐작**이라,
|
|
399
|
+
* UTF-8 로 잘 풀리던 출력에 UTF-8 이 아닌 바이트가 하나 섞이는 순간
|
|
400
|
+
* 통째로 CP949 로 다시 읽힌다 — 한글 윈도우 콘솔에서 흔한 일이다.
|
|
401
|
+
*/
|
|
402
|
+
전체글() {
|
|
403
|
+
if (!this.조각들.length) return '';
|
|
404
|
+
/*
|
|
405
|
+
* 안 바뀌었으면 지난번 것을 그대로 준다.
|
|
406
|
+
*
|
|
407
|
+
* `목록()` 이 일감마다 이걸 부른다 — 안 읽은 글자 수를 세려고. 그 한 번이
|
|
408
|
+
* 최대 256KB 를 이어 붙이고 통째로 다시 푸는 일이라, 끝난 것 여덟 개를
|
|
409
|
+
* 들고 있으면 목록 한 번에 2MB 를 푼다. 모델이 Jobs 를 부를 때마다.
|
|
410
|
+
*/
|
|
411
|
+
if (this.푼것 && this.푼것.판 === this.판) return this.푼것.글;
|
|
412
|
+
const 다 = Buffer.concat(this.조각들);
|
|
413
|
+
if (this.인코딩) {
|
|
414
|
+
try {
|
|
415
|
+
const 글 = new TextDecoder(this.인코딩, { fatal: false }).decode(다);
|
|
416
|
+
this.푼것 = { 판: this.판, 글 };
|
|
417
|
+
return 글;
|
|
418
|
+
} catch { /* 이 Node 가 모르는 이름이면 아래로 내려가 다시 잰다 */ }
|
|
419
|
+
}
|
|
420
|
+
const 콘솔 = consoleCodepage() === 65001 ? 'utf-8' : null;
|
|
421
|
+
try {
|
|
422
|
+
const r = decodeBytes(다, { fallback: 콘솔 });
|
|
423
|
+
// 처음 한 번만 못 박는다. 아직 아무것도 안 쌓였을 때 정하면
|
|
424
|
+
// 표본이 너무 적어 엉뚱하게 잡히므로, 실제로 글이 나온 뒤에 정한다.
|
|
425
|
+
if (r.text) this.인코딩 = r.encoding;
|
|
426
|
+
this.푼것 = { 판: this.판, 글: r.text };
|
|
427
|
+
return r.text;
|
|
428
|
+
} catch { return 다.toString('utf8'); }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* 지난번 읽은 뒤로 새로 나온 것만. 처음부터를 주면 통째로.
|
|
433
|
+
*
|
|
434
|
+
* `앞잘림` 은 **지금 건네는 이 글**에 대한 말이다. 묻는 방식에 따라 답이 다르다.
|
|
435
|
+
*
|
|
436
|
+
* 통째로 — '이 일감은 앞을 버린 적이 있다.' 지금 주는 것이 처음부터가
|
|
437
|
+
* 아니라는 뜻이라, 버린 적이 있는 한 계속 사실이다.
|
|
438
|
+
* 새것만 — '지금 주는 이 토막 **앞에 구멍이 있다.**' 버린 직후 한 번만
|
|
439
|
+
* 사실이고, 그 뒤에 나온 몇 줄은 멀쩡하다.
|
|
440
|
+
*
|
|
441
|
+
* 이 둘을 한 칸으로 합쳐 뒀더니, 한 번 넘친 일감은 **그 뒤로 영원히**
|
|
442
|
+
* "앞부분이 잘렸습니다" 를 달고 나왔다. 모델은 제가 받은 글에 앞이 빠졌다고
|
|
443
|
+
* 믿고 처음부터 다시 읽으러 가는데 — 로컬 모델에서 왕복 하나가 20~40초다 —
|
|
444
|
+
* 그렇게 받아 온 것에도 똑같이 그 말이 붙어 있다. 맴돌기 딱 좋은 자리였다.
|
|
445
|
+
* test/jobs.test.js 의 '온전한 새 출력에 잘렸다고 안 한다' 가 여기를 지킨다.
|
|
446
|
+
*/
|
|
447
|
+
읽기({ 처음부터 = false } = {}) {
|
|
448
|
+
const 글 = this.전체글();
|
|
449
|
+
const 새것 = 처음부터 ? 글 : 글.slice(Math.min(this.읽은글자, 글.length));
|
|
450
|
+
this.읽은글자 = 글.length;
|
|
451
|
+
const 잘림 = 처음부터 ? this.앞잘림 : this.막버렸다;
|
|
452
|
+
// 통째로 읽어 갔으면 지금 들고 있는 것을 다 본 것이라, '바로 앞에 구멍이
|
|
453
|
+
// 있다' 는 경고도 같이 내린다. 다음에 또 버리면 그때 다시 선다.
|
|
454
|
+
this.막버렸다 = false;
|
|
455
|
+
return { 글: 새것, 앞잘림: 잘림 };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
산햇수() { return Math.round((Date.now() - this.띄운때) / 1000); }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const 일감들 = new Map();
|
|
462
|
+
let 다음번호 = 1;
|
|
463
|
+
// 자리를 위해 지운 개수. **지웠으면 지웠다고 말하려고** 센다 —
|
|
464
|
+
// 안 말하면 모델은 목록에 보이는 것이 전부인 줄 안다.
|
|
465
|
+
let 치운것 = 0;
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* 끝난 일감이 쌓이는 것을 막는다.
|
|
469
|
+
*
|
|
470
|
+
* 오래된 것부터 버린다. 번호가 곧 띄운 순서라 그대로 쓴다. 방금 띄운 것을
|
|
471
|
+
* 못 읽게 되면 안 되므로 **최근 쪽을 남긴다.**
|
|
472
|
+
*/
|
|
473
|
+
function 끝난것치우기() {
|
|
474
|
+
const 끝난것 = [...일감들.values()].filter((j) => j.상태 !== '도는중').sort((a, b) => a.번호 - b.번호);
|
|
475
|
+
if (끝난것.length <= 끝난것상한) return;
|
|
476
|
+
for (const j of 끝난것.slice(0, 끝난것.length - 끝난것상한)) {
|
|
477
|
+
// 들고 있던 것도 같이 놓는다. 항목만 지우고 조각들을 안 놓으면 아무것도 안 준다.
|
|
478
|
+
j.조각들 = [];
|
|
479
|
+
j.푼것 = null;
|
|
480
|
+
일감들.delete(j.번호);
|
|
481
|
+
치운것++;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* 명령을 뒤에서 띄운다.
|
|
487
|
+
*
|
|
488
|
+
* @param 기다림 띄운 뒤 몇 ms 를 지켜보나. 그 사이에 죽으면 **띄우기 실패**로
|
|
489
|
+
* 돌려준다. 포트가 이미 물려 있는 경우가 제일 흔한데, 그때 "떴습니다" 라고
|
|
490
|
+
* 답하면 모델은 다음 단계로 넘어가고 사용자는 안 뜬 서버를 찾아다닌다.
|
|
491
|
+
*/
|
|
492
|
+
export async function 띄우기(명령, { cwd, 설명 = null, 기다림 = 1500 } = {}) {
|
|
493
|
+
const 도는것 = [...일감들.values()].filter((j) => j.상태 === '도는중');
|
|
494
|
+
if (도는것.length >= 최대일감) {
|
|
495
|
+
return { error: `뒤에서 도는 명령이 이미 ${도는것.length}개입니다. Jobs 로 안 쓰는 것을 끝내고 다시 하세요.` };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const j = new 일감(다음번호++, 명령, 설명);
|
|
499
|
+
const shell = 셸명령(명령);
|
|
500
|
+
let kid;
|
|
501
|
+
try {
|
|
502
|
+
kid = spawn(shell.file, shell.args, {
|
|
503
|
+
...띄우기옵션(cwd),
|
|
504
|
+
windowsVerbatimArguments: shell.verbatim,
|
|
505
|
+
});
|
|
506
|
+
} catch (err) {
|
|
507
|
+
return { error: `띄우지 못했습니다: ${String(err?.message ?? err)}` };
|
|
508
|
+
}
|
|
509
|
+
j.kid = kid;
|
|
510
|
+
일감들.set(j.번호, j);
|
|
511
|
+
|
|
512
|
+
kid.stdout?.on('data', (b) => j.담기(b));
|
|
513
|
+
kid.stderr?.on('data', (b) => j.담기(b));
|
|
514
|
+
kid.on('error', (err) => { j.담기(Buffer.from(`\n[띄우기 실패: ${err.message}]\n`)); j.상태 = '끝남'; j.종료코드 = -1; });
|
|
515
|
+
kid.on('close', (code, sig) => {
|
|
516
|
+
if (j.상태 === '죽임') return;
|
|
517
|
+
j.상태 = '끝남';
|
|
518
|
+
j.종료코드 = sig ? null : code;
|
|
519
|
+
j.시그널 = sig ?? null;
|
|
520
|
+
// 끝난 것이 쌓이지 않게. 도는 것은 최대일감 이 따로 막는다.
|
|
521
|
+
끝난것치우기();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// 뜨자마자 죽는지 잠깐 지켜본다. 그 몇 줄이 대부분의 답이다.
|
|
525
|
+
await 잠깐(j, 기다림);
|
|
526
|
+
|
|
527
|
+
const 본것 = j.읽기({ 처음부터: true });
|
|
528
|
+
if (j.상태 !== '도는중') {
|
|
529
|
+
일감들.delete(j.번호);
|
|
530
|
+
return {
|
|
531
|
+
떴나: false,
|
|
532
|
+
번호: j.번호,
|
|
533
|
+
종료코드: j.종료코드,
|
|
534
|
+
시그널: j.시그널,
|
|
535
|
+
출력: 본것.글,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
return { 떴나: true, 번호: j.번호, 출력: 본것.글 };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function 잠깐(j, ms) {
|
|
542
|
+
return new Promise((끝) => {
|
|
543
|
+
if (ms <= 0) return 끝();
|
|
544
|
+
let 됐나 = false;
|
|
545
|
+
const 마치기 = () => { if (됐나) return; 됐나 = true; clearTimeout(시계); 끝(); };
|
|
546
|
+
const 시계 = setTimeout(마치기, ms);
|
|
547
|
+
시계.unref?.();
|
|
548
|
+
j.kid?.once('close', 마치기);
|
|
549
|
+
j.kid?.once('error', 마치기);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export function 목록() {
|
|
554
|
+
return [...일감들.values()].map((j) => ({
|
|
555
|
+
번호: j.번호,
|
|
556
|
+
명령: j.명령,
|
|
557
|
+
설명: j.설명,
|
|
558
|
+
상태: j.상태,
|
|
559
|
+
종료코드: j.종료코드,
|
|
560
|
+
시그널: j.시그널,
|
|
561
|
+
초: j.산햇수(),
|
|
562
|
+
안읽은글자: Math.max(0, j.전체글().length - j.읽은글자),
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export function 하나(번호) { return 일감들.get(Number(번호)) ?? null; }
|
|
567
|
+
|
|
568
|
+
export function 읽기(번호, opts) {
|
|
569
|
+
const j = 하나(번호);
|
|
570
|
+
if (!j) return null;
|
|
571
|
+
const r = j.읽기(opts);
|
|
572
|
+
return { ...r, 상태: j.상태, 종료코드: j.종료코드, 시그널: j.시그널, 명령: j.명령, 초: j.산햇수() };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* 하나를 끝낸다.
|
|
577
|
+
*
|
|
578
|
+
* **기다렸다가 거둔다.** 죽이자마자 파이프를 끊으면 그 순간 아직 안 읽힌 것이
|
|
579
|
+
* 통째로 사라지는데, 하필 그게 제일 중요한 몇 줄이다 — 서버가 뻗으면서 남긴
|
|
580
|
+
* 스택 트레이스, 왜 죽었는지 적은 마지막 말. `마지막 출력:` 이라고 적어 놓고
|
|
581
|
+
* 정작 마지막을 안 주면 안 적느니만 못하다.
|
|
582
|
+
*/
|
|
583
|
+
export async function 끝내기(번호) {
|
|
584
|
+
const j = 하나(번호);
|
|
585
|
+
if (!j) return null;
|
|
586
|
+
if (j.상태 !== '도는중') { 일감들.delete(j.번호); return { 이미: true, 명령: j.명령 }; }
|
|
587
|
+
j.상태 = '죽임';
|
|
588
|
+
/*
|
|
589
|
+
* 사람이 시킨 자리라 넉넉히 기다린다.
|
|
590
|
+
*
|
|
591
|
+
* 검사를 통째로 돌릴 때처럼 컴퓨터가 바쁘면 taskkill 이 1.5초를 넘긴다.
|
|
592
|
+
* 그때 잘린 채로 넘어가면 손자가 남는데, 몇백 ms 빨리 돌아오는 것보다
|
|
593
|
+
* 안 죽은 서버 하나가 훨씬 비싸다.
|
|
594
|
+
*/
|
|
595
|
+
const 넉넉히 = 6000;
|
|
596
|
+
나무죽이기(j.kid, { 파이프끊기: false, 상한: 넉넉히 });
|
|
597
|
+
await 죽는말기다리기(j.kid, 죽는말기다림);
|
|
598
|
+
/*
|
|
599
|
+
* 아직 살아 있으면 여기서 끊는다.
|
|
600
|
+
*
|
|
601
|
+
* 곧 목록에서 지울 참인데, 지우고 나면 이놈을 가리킬 방법이 없다. 안 죽은
|
|
602
|
+
* 채로 지우면 영영 못 찾는 프로세스가 하나 남는다 — 이 기능이 없애려던
|
|
603
|
+
* 바로 그 상태다. 유닉스에서 SIGTERM 을 안 듣는 놈이 여기로 온다.
|
|
604
|
+
* 윈도우에서는 나무를 다 못 훑어 뿌리를 살려 둔 경우가 여기로 온다.
|
|
605
|
+
*
|
|
606
|
+
* **뿌리살리기를 끈다.** 이걸 안 끄면 윈도우에서 taskkill 이 또 잘렸을 때
|
|
607
|
+
* 나무죽이기 가 kid.kill() 에 닿지도 못하고 그냥 돌아온다 — 「곧장」 이
|
|
608
|
+
* 윈도우에서는 아무 뜻이 없었다는 말이다. 여기는 마지막 자리라 뿌리라도
|
|
609
|
+
* 죽여야 한다. (모두끝내기 는 이미 그렇게 하고 있었다.)
|
|
610
|
+
*/
|
|
611
|
+
if (아직사나(j.kid)) {
|
|
612
|
+
나무죽이기(j.kid, { 파이프끊기: false, 곧장: true, 상한: 넉넉히, 뿌리살리기: false });
|
|
613
|
+
/*
|
|
614
|
+
* 죽인 **직후에 살았나 물으면 안 된다.**
|
|
615
|
+
*
|
|
616
|
+
* 아직사나() 는 exitCode·signalCode 를 보는데, 그 값은 Node 가 종료를
|
|
617
|
+
* 실제로 **관찰한 뒤에** 채워진다. 동기 taskkill 이 돌아온 순간에는 아직
|
|
618
|
+
* 비어 있을 수 있다 — 그러면 멀쩡히 죽은 것을 「안 죽었다」 로 읽는다.
|
|
619
|
+
*
|
|
620
|
+
* 검사를 통째로 돌릴 때 이걸로 한 번 헛짚었다. 혼자 돌리면 초록이고
|
|
621
|
+
* 전체에서만 빨개졌다 — 컴퓨터가 바쁘면 관찰이 늦기 때문이다.
|
|
622
|
+
* 그러니 한 번 더 기다렸다가 묻는다.
|
|
623
|
+
*/
|
|
624
|
+
await 죽는말기다리기(j.kid, 죽는말기다림);
|
|
625
|
+
}
|
|
626
|
+
파이프끊기(j.kid);
|
|
627
|
+
const 남은 = j.읽기({});
|
|
628
|
+
/*
|
|
629
|
+
* ★ 그러고도 안 죽었으면 **그렇다고 말한다.**
|
|
630
|
+
*
|
|
631
|
+
* 전에는 성패를 안 보고 늘 성공을 돌려줬다. 그러면 모델과 사람은 「끝냈습니다」
|
|
632
|
+
* 를 받는데 서버는 살아서 포트를 물고 있고, 목록에서도 지워져 다시 부를
|
|
633
|
+
* 방법이 없다. 이 파일 머리말이 스스로 적어 둔 「안 죽은 채로 지우면 영영
|
|
634
|
+
* 못 찾는 프로세스가 하나 남는다」 가 그대로 성립하고 있었다.
|
|
635
|
+
*
|
|
636
|
+
* 안 죽었으면 목록에 남긴다. 그래야 사람이 한 번 더 시킬 수 있다.
|
|
637
|
+
*/
|
|
638
|
+
if (아직사나(j.kid)) {
|
|
639
|
+
j.상태 = '도는중'; // 지우지 않는다 — 다시 시킬 수 있어야 한다
|
|
640
|
+
return { 이미: false, 안죽음: true, 명령: j.명령, 초: j.산햇수(), 남은: 남은.글 };
|
|
641
|
+
}
|
|
642
|
+
일감들.delete(j.번호);
|
|
643
|
+
return { 이미: false, 명령: j.명령, 초: j.산햇수(), 남은: 남은.글 };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* 전부 끝낸다. deel 을 닫을 때 부른다.
|
|
648
|
+
*
|
|
649
|
+
* 이걸 빠뜨리면 사람이 안 띄운 서버가 계속 돈다. 다음에 켜서 `npm run dev` 를
|
|
650
|
+
* 하면 "포트가 이미 쓰이는 중" 이 뜨는데, 무엇이 물고 있는지 알 길이 없다.
|
|
651
|
+
*/
|
|
652
|
+
export function 모두끝내기() {
|
|
653
|
+
const n = [...일감들.values()].filter((j) => j.상태 === '도는중').length;
|
|
654
|
+
for (const j of 일감들.values()) {
|
|
655
|
+
if (j.상태 !== '도는중') continue;
|
|
656
|
+
j.상태 = '죽임';
|
|
657
|
+
/*
|
|
658
|
+
* 여기는 프로그램이 끝나는 길이라 **다시 올 기회가 없다.** 한 번 짧게
|
|
659
|
+
* 훑어 보고, 잘렸으면 한 번 더 길게 훑는다. 그러고도 안 되면 뿌리라도
|
|
660
|
+
* 죽인다 — 아무것도 안 죽이는 것보다 낫다.
|
|
661
|
+
*/
|
|
662
|
+
if (!나무죽이기(j.kid)) 나무죽이기(j.kid, { 상한: 4000, 뿌리살리기: false });
|
|
663
|
+
}
|
|
664
|
+
일감들.clear();
|
|
665
|
+
return n;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** 검사에서 판을 깨끗이 하려고 쓴다. */
|
|
669
|
+
export function 비우기() { 모두끝내기(); 다음번호 = 1; 치운것 = 0; }
|
|
670
|
+
|
|
671
|
+
// 어떤 길로 끝나든 남기지 않는다. repl·oneshot 이 부르는 것과 겹쳐도 무해하다.
|
|
672
|
+
process.once('exit', () => { try { 모두끝내기(); } catch { /* 끝나는 중이라 할 수 있는 게 없다 */ } });
|
|
673
|
+
|
|
674
|
+
/*
|
|
675
|
+
* ── 도구 ──────────────────────────────────────────────────────────────
|
|
676
|
+
*
|
|
677
|
+
* 왜 Bash 안에 인자로 안 넣고 도구를 따로 두나:
|
|
678
|
+
* Bash 에 `job`·`kill` 같은 인자를 얹으면 스키마가 "명령을 실행한다" 와
|
|
679
|
+
* "일감을 본다" 두 가지를 한꺼번에 말하게 된다. 작은 모델은 그 자리에서
|
|
680
|
+
* command 없이 Bash 를 부르거나, 반대로 읽으려다 명령을 또 실행한다.
|
|
681
|
+
* 하는 일이 다르면 도구를 나누는 편이 결국 싸다. 이 스키마는 아주 작다.
|
|
682
|
+
*/
|
|
683
|
+
/*
|
|
684
|
+
* 인자 이름 받이 — 한글 이름과 영문 이름을 **둘 다** 받는다.
|
|
685
|
+
*
|
|
686
|
+
* 모델은 한글 인자 이름을 자주 영어로 바꿔 보낸다. 추정이 아니라 이 저장소가
|
|
687
|
+
* 겪은 일이다 — Task 는 이미 `목적 ?? purpose`, `할일 ?? task`, `모드 ?? mode`
|
|
688
|
+
* 로 둘 다 받고 있다(agent/loop.js). 누군가 겪고 달아 둔 것이다.
|
|
689
|
+
*
|
|
690
|
+
* 여기에는 그게 없어서 `Jobs({job: 1, stop: true})` 가 **목록**을 돌려줬다.
|
|
691
|
+
* 모델은 끄라고 시켰고 성공처럼 보이는 답을 받았는데 서버는 그대로 포트를
|
|
692
|
+
* 문다 — 사람은 원인을 못 찾는다. 이 도구에서 제일 나쁜 실패다.
|
|
693
|
+
*
|
|
694
|
+
* 이름을 영문으로 **바꾸는** 것으로는 안 된다. 그러면 한글로 보내는 쪽이 같은
|
|
695
|
+
* 구멍에 빠지고, 안 맞춘 이름(id·number)은 여전히 샌다. 둘 다 받고,
|
|
696
|
+
* 못 알아들은 것은 못 알아들었다고 말한다.
|
|
697
|
+
*
|
|
698
|
+
* **목록은 여기 한 벌뿐이다.** 화면 이름표도 이걸 쓴다(repl.js·oneshot.js).
|
|
699
|
+
* 두 벌이 되면 한쪽만 고쳐지고, 그때부터 화면과 실제가 어긋난다.
|
|
700
|
+
*/
|
|
701
|
+
const 인자이름 = {
|
|
702
|
+
번호: ['job', 'job_id', 'jobId', 'id', 'number', '번호'],
|
|
703
|
+
끝내기: ['stop', 'kill', '끝내기'],
|
|
704
|
+
처음부터: ['from_start', 'fromStart', 'full', '처음부터'],
|
|
705
|
+
};
|
|
706
|
+
const 아는이름 = new Set(Object.values(인자이름).flat());
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* 준 인자에서 아는 것만 골라낸다.
|
|
710
|
+
*
|
|
711
|
+
* @returns {{번호: number|null, 끝내기: boolean, 처음부터: boolean, 준것: string[], 모르는것: string[]}}
|
|
712
|
+
*/
|
|
713
|
+
export function 일감인자(args) {
|
|
714
|
+
const 준것 = Object.keys(args ?? {});
|
|
715
|
+
const 하나씩 = (이름) => {
|
|
716
|
+
for (const n of 인자이름[이름]) if (args?.[n] != null) return args[n];
|
|
717
|
+
return null;
|
|
718
|
+
};
|
|
719
|
+
const 날번호 = 하나씩('번호');
|
|
720
|
+
return {
|
|
721
|
+
번호: 날번호 == null ? null : Number(날번호),
|
|
722
|
+
끝내기: !!하나씩('끝내기'),
|
|
723
|
+
처음부터: !!하나씩('처음부터'),
|
|
724
|
+
준것,
|
|
725
|
+
모르는것: 준것.filter((k) => !아는이름.has(k)),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* 모델에게 건넬 만큼만 남긴다 — **뒤**를 남긴다.
|
|
731
|
+
*
|
|
732
|
+
* 오래 도는 것에서 사람이 찾는 것은 언제나 방금 나온 쪽이다. 앞을 남기면
|
|
733
|
+
* 몇 시간 전 기동 로그를 주게 된다.
|
|
734
|
+
*
|
|
735
|
+
* 줄였으면 줄였다고 적는다. 안 적으면 모델은 그게 전부인 줄 알고,
|
|
736
|
+
* 안 보이는 줄에 답이 있을 때 엉뚱한 결론으로 간다.
|
|
737
|
+
*/
|
|
738
|
+
function 뒤만(글, 한도 = 건넬상한) {
|
|
739
|
+
const s = String(글 ?? '');
|
|
740
|
+
if (s.length <= 한도) return { 글: s, 줄임: false };
|
|
741
|
+
return {
|
|
742
|
+
글: `(앞 ${s.length - 한도}자는 길어서 줄였습니다 — 뒤쪽만 보여 줍니다)\n${s.slice(-한도)}`,
|
|
743
|
+
줄임: true,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** 끝난 일감을 한 마디로. 시그널로 죽으면 종료코드가 null 이라 따로 말한다. */
|
|
748
|
+
function 끝난말(r) {
|
|
749
|
+
if (r.시그널) return `${r.시그널} 로 죽음`;
|
|
750
|
+
return r.종료코드 === 0 ? '끝남' : `종료코드 ${r.종료코드}`;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export const JOBS_TOOL = {
|
|
754
|
+
schema: {
|
|
755
|
+
name: 'Jobs',
|
|
756
|
+
description: '뒤에서 도는 명령(Bash 의 background)을 보고·읽고·끝낸다.'
|
|
757
|
+
+ ' job 없이 부르면 목록. job 을 주면 그동안 새로 나온 출력.'
|
|
758
|
+
+ ' 서버를 띄웠으면 일이 끝날 때 반드시 stop 으로 정리해라.',
|
|
759
|
+
parameters: {
|
|
760
|
+
type: 'object',
|
|
761
|
+
properties: {
|
|
762
|
+
job: { type: 'number', description: '볼 일감 번호. 없으면 목록' },
|
|
763
|
+
stop: { type: 'boolean', description: 'true 면 그 일감을 끝낸다' },
|
|
764
|
+
from_start: { type: 'boolean', description: 'true 면 처음부터 다시 읽는다' },
|
|
765
|
+
},
|
|
766
|
+
required: [],
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
// 끝내기 는 죽는 순간 뱉은 말을 기다렸다 거두므로 비동기다 (끝내기 머리말 참고).
|
|
770
|
+
// runTool 이 await 로 부르니 도구가 비동기여도 된다 (tools/index.js).
|
|
771
|
+
async run(args) {
|
|
772
|
+
const 받은것 = 일감인자(args);
|
|
773
|
+
/*
|
|
774
|
+
* 준 것이 있는데 **하나도** 못 알아들었으면 목록으로 얼버무리면 안 된다.
|
|
775
|
+
*
|
|
776
|
+
* 목록을 돌려주면 그것도 성공한 답으로 보인다. 끄라고 시킨 모델은
|
|
777
|
+
* 껐다고 여기고 넘어가는데 서버는 그대로 돈다. 무엇을 받는지 알려 줘야
|
|
778
|
+
* 다음 걸음에서 고쳐 부른다.
|
|
779
|
+
*
|
|
780
|
+
* 인자가 **아예 없는** 것은 그냥 '목록 보기' 다 — 그건 오류가 아니다.
|
|
781
|
+
*/
|
|
782
|
+
if (받은것.준것.length && 받은것.모르는것.length === 받은것.준것.length) {
|
|
783
|
+
return {
|
|
784
|
+
error: `모르는 인자입니다: ${받은것.모르는것.join(', ')}.`
|
|
785
|
+
+ ' Jobs 는 번호(job) · 끝내기(stop) · 처음부터(from_start) 만 받습니다.'
|
|
786
|
+
+ ' 번호 없이 부르면 목록입니다.',
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
const 번호 = 받은것.번호;
|
|
790
|
+
// 숫자가 아닌 것을 받으면 NaN 이 되고, 그대로 두면 `NaN번 일감이 없습니다`
|
|
791
|
+
// 라는 말이 모델에게 나간다. 무엇이 잘못됐는지 안 알려 주는 오류다.
|
|
792
|
+
if (번호 != null && !Number.isFinite(번호)) {
|
|
793
|
+
return { error: `번호는 숫자여야 합니다 (받은 것: ${JSON.stringify(args?.번호)}). 번호 없이 Jobs 를 불러 목록을 보세요.` };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (번호 == null) {
|
|
797
|
+
/*
|
|
798
|
+
* 번호 없이 "끝내라" 고 하면 목록으로 얼버무리면 안 된다.
|
|
799
|
+
*
|
|
800
|
+
* 목록을 돌려주면 그건 성공한 답으로 보인다. 모델은 정리한 줄 알고
|
|
801
|
+
* "서버를 껐습니다" 라고 말하는데 서버는 그대로 돌고 있다.
|
|
802
|
+
* 도구가 시킨 일을 안 했으면 안 했다고 말해야 그 다음이 이어진다.
|
|
803
|
+
*/
|
|
804
|
+
if (받은것.끝내기) {
|
|
805
|
+
const 도는것 = 목록().filter((j) => j.상태 === '도는중');
|
|
806
|
+
return {
|
|
807
|
+
error: '끝낼 일감 번호를 주세요. 번호 없이 끝내기만 주면 아무것도 안 끝냅니다.'
|
|
808
|
+
+ (도는것.length
|
|
809
|
+
? ` 지금 도는 것: ${도는것.map((j) => `${j.번호}번(${j.명령})`).join(' · ')}`
|
|
810
|
+
: ' 지금 도는 것이 없습니다.'),
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
const ls = 목록();
|
|
814
|
+
if (!ls.length) return { content: '뒤에서 도는 명령이 없습니다.', summary: 세말('count', 0) };
|
|
815
|
+
const 줄들 = ls.map((j) => {
|
|
816
|
+
const 상 = j.상태 === '도는중' ? `도는중 ${j.초}초` : j.시그널 ? `${j.시그널} 로 죽음` : `끝남 (종료코드 ${j.종료코드})`;
|
|
817
|
+
const 새것 = j.안읽은글자 ? ` · 안 읽은 출력 ${j.안읽은글자}자` : '';
|
|
818
|
+
return ` ${j.번호}. ${j.명령} — ${상}${새것}`;
|
|
819
|
+
});
|
|
820
|
+
/*
|
|
821
|
+
* 자리를 위해 지운 것이 있으면 말한다.
|
|
822
|
+
*
|
|
823
|
+
* 안 말하면 모델은 여기 보이는 것이 이 세션에서 띄운 전부인 줄 안다.
|
|
824
|
+
* 아까 띄운 3번을 찾다가 없으니 "안 띄웠나 보다" 하고 다시 띄운다 —
|
|
825
|
+
* 그러면 포트가 물려서 안 뜨고, 왜 안 되는지도 모른다.
|
|
826
|
+
*/
|
|
827
|
+
if (치운것) 줄들.push(` (오래된 것 ${치운것}개는 자리를 위해 지웠습니다)`);
|
|
828
|
+
return { content: 줄들.join('\n'), summary: 세말('count', ls.length), 일감수: ls.length };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (받은것.끝내기) {
|
|
832
|
+
const r = await 끝내기(번호);
|
|
833
|
+
if (!r) return { error: `${번호}번 일감이 없습니다. 번호 없이 Jobs 를 불러 목록을 보세요.` };
|
|
834
|
+
if (r.이미) return { content: `${번호}번은 이미 끝나 있었습니다: ${r.명령}`, summary: 말('sum.jobEndedAlready') };
|
|
835
|
+
/*
|
|
836
|
+
* ★ 안 죽었으면 「끝냈습니다」 라고 하지 않는다.
|
|
837
|
+
*
|
|
838
|
+
* 여기서 거짓말하면 모델은 다음 단계로 가고 사람은 포트를 문 채로 남은
|
|
839
|
+
* 서버를 못 찾는다 — 이 기능이 없애려던 바로 그 상태다. 목록에도
|
|
840
|
+
* 남겨 뒀으니 한 번 더 시킬 수 있다고 알려 준다.
|
|
841
|
+
*/
|
|
842
|
+
if (r.안죽음) {
|
|
843
|
+
return {
|
|
844
|
+
error: `${번호}번이 안 끝났습니다 (${r.초}초 돌았습니다): ${r.명령}`
|
|
845
|
+
+ '\n죽이라고 시켰는데 아직 살아 있습니다. 목록에 그대로 두었으니 한 번 더 시켜 보세요.'
|
|
846
|
+
+ (r.남은 ? `\n\n마지막 출력:\n${뒤만(r.남은, 2000).글}` : ''),
|
|
847
|
+
summary: 말('sum.jobNotStopped'),
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
content: `${번호}번을 끝냈습니다 (${r.초}초 돌았습니다): ${r.명령}`
|
|
852
|
+
+ (r.남은 ? `\n\n마지막 출력:\n${뒤만(r.남은, 2000).글}` : ''),
|
|
853
|
+
summary: 이어(말('sum.jobStopped'), 말('unit.sec', { n: r.초 })),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const r = 읽기(번호, { 처음부터: 받은것.처음부터 });
|
|
858
|
+
if (!r) return { error: `${번호}번 일감이 없습니다. 번호 없이 Jobs 를 불러 목록을 보세요.` };
|
|
859
|
+
const 머리 = r.상태 === '도는중'
|
|
860
|
+
? `${번호}번 (도는중 ${r.초}초): ${r.명령}`
|
|
861
|
+
: `${번호}번 (${r.시그널 ? `${r.시그널} 로 죽음` : `끝남 · 종료코드 ${r.종료코드}`}): ${r.명령}`;
|
|
862
|
+
// 새 출력이 없다는 것도 사실이다. 빈 글을 주면 모델은 못 읽은 줄 안다.
|
|
863
|
+
const 잘라낸 = 뒤만(r.글);
|
|
864
|
+
const 몸 = r.글.trim()
|
|
865
|
+
? (r.앞잘림 ? '(앞부분은 너무 길어 잘렸습니다 — 뒤쪽만 남았습니다)\n' : '') + 잘라낸.글
|
|
866
|
+
: '(지난번 읽은 뒤로 새 출력이 없습니다)';
|
|
867
|
+
return {
|
|
868
|
+
content: `${머리}\n\n${몸}`,
|
|
869
|
+
summary: (r.상태 === '도는중' ? 이어(말('sum.jobRunning'), 말('unit.sec', { n: r.초 })) : 끝난말(r))
|
|
870
|
+
+ (잘라낸.줄임 ? ' · 일부만' : ''),
|
|
871
|
+
// 끝난 일감이 종료코드 0 이 아니면 실패로 물들인다 — Bash 와 같은 규칙이다.
|
|
872
|
+
// 시그널로 죽은 것도 실패다 — 그때 종료코드는 null 이라 !== 0 으로 잡힌다.
|
|
873
|
+
failed: r.상태 !== '도는중' && r.종료코드 !== 0,
|
|
874
|
+
};
|
|
875
|
+
},
|
|
876
|
+
};
|