dsh-plugin-container 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +341 -2
- package/cordis.patch.yml +3 -0
- package/docs/ai-chat.png +0 -0
- package/docs/ai-new-session.png +0 -0
- package/docs/apple-container.png +0 -0
- package/docs/attach.png +0 -0
- package/docs/docker-console.png +0 -0
- package/docs/docker-overview.png +0 -0
- package/docs/docker-volumes.png +0 -0
- package/docs/image-architecture.png +0 -0
- package/docs/package-install.png +0 -0
- package/docs/provider-switch.png +0 -0
- package/docs/run-container.png +0 -0
- package/index.js +138 -0
- package/lib/client.js +3499 -0
- package/package.json +62 -4
- package/src/client/api.js +94 -0
- package/src/client/attach.jsx +133 -0
- package/src/client/components.jsx +301 -0
- package/src/client/hooks.js +161 -0
- package/src/client/i18n.js +496 -0
- package/src/client/icons.jsx +219 -0
- package/src/client/index.jsx +76 -0
- package/src/client/page.jsx +755 -0
- package/src/client/run-container.jsx +187 -0
- package/src/client/runs.js +182 -0
- package/src/client/store.js +91 -0
- package/src/client/styles.js +363 -0
- package/src/client/tabs/compose.jsx +94 -0
- package/src/client/tabs/console.jsx +211 -0
- package/src/client/tabs/containers.jsx +143 -0
- package/src/client/tabs/images.jsx +127 -0
- package/src/client/tabs/networks.jsx +91 -0
- package/src/client/tabs/overview.jsx +266 -0
- package/src/client/tabs/volumes.jsx +76 -0
- package/src/host/apple-cli.js +307 -0
- package/src/host/apple.js +471 -0
- package/src/host/attach.js +243 -0
- package/src/host/commands.js +235 -0
- package/src/host/docker-cli.js +405 -0
- package/src/host/jobs.js +245 -0
- package/src/host/parsers.js +484 -0
- package/src/host/runtime.js +285 -0
- package/src/host/service.js +832 -0
- package/src/host/tools.js +198 -0
- package/src/host/util.js +97 -0
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side Docker service exposed to the browser over the Connection generic
|
|
3
|
+
* RPC channel `/docker`.
|
|
4
|
+
*
|
|
5
|
+
* Every method returns `{ok:true,value}` or `{ok:false,error}`. There is no
|
|
6
|
+
* shell, no model-facing tool, and no persistence: query caches and command
|
|
7
|
+
* records live in this plugin's memory and disappear on unload.
|
|
8
|
+
*/
|
|
9
|
+
import { homedir } from 'node:os'
|
|
10
|
+
import { statSync } from 'node:fs'
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
CommandError,
|
|
14
|
+
describeCommand,
|
|
15
|
+
detectDestructive,
|
|
16
|
+
normalizeContext,
|
|
17
|
+
normalizeSubcommand,
|
|
18
|
+
parseArguments,
|
|
19
|
+
parseCommand,
|
|
20
|
+
resolveCwd,
|
|
21
|
+
} from './commands.js'
|
|
22
|
+
import { AppleProvider } from './apple.js'
|
|
23
|
+
import { AttachManager } from './attach.js'
|
|
24
|
+
import { AppleCliError, AppleCommandError } from './apple-cli.js'
|
|
25
|
+
import { DockerCli, DockerCliError, DockerCommandError } from './docker-cli.js'
|
|
26
|
+
import { JobManager } from './jobs.js'
|
|
27
|
+
import {
|
|
28
|
+
firstLine,
|
|
29
|
+
mapDockerInfo,
|
|
30
|
+
mapDockerVersion,
|
|
31
|
+
normalizeContainer,
|
|
32
|
+
normalizeImage,
|
|
33
|
+
normalizeNetwork,
|
|
34
|
+
normalizeVolume,
|
|
35
|
+
parseComposeList,
|
|
36
|
+
parseContexts,
|
|
37
|
+
parseNdjson,
|
|
38
|
+
parseSystemDf,
|
|
39
|
+
summarizeStderr,
|
|
40
|
+
} from './parsers.js'
|
|
41
|
+
import { createRuntimeResolver } from './runtime.js'
|
|
42
|
+
import { codeOf, isRecord, messageOf, optionalInteger, requireString } from './util.js'
|
|
43
|
+
|
|
44
|
+
const STATUS_CACHE_MS = 1500
|
|
45
|
+
const CONTEXT_CACHE_MS = 10_000
|
|
46
|
+
const VALUE_CACHE_MS = 1500
|
|
47
|
+
|
|
48
|
+
/** @param {unknown} value */
|
|
49
|
+
function ok(value) {
|
|
50
|
+
return { ok: true, value }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @param {string} code @param {string} message @param {Record<string, unknown>} [details] */
|
|
54
|
+
function fail(code, message, details = {}) {
|
|
55
|
+
return { ok: false, error: { code, message, details } }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @param {unknown} error */
|
|
59
|
+
function failureFrom(error) {
|
|
60
|
+
if (error instanceof DockerCliError || error instanceof DockerCommandError || error instanceof AppleCliError || error instanceof AppleCommandError || error instanceof CommandError) {
|
|
61
|
+
return fail(error.code ?? 'docker/internal', error.message, {
|
|
62
|
+
...(error.details ?? {}),
|
|
63
|
+
...(error.stderr !== undefined ? { stderr: summarizeStderr(error.stderr) } : {}),
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
if (codeOf(error) !== undefined) return fail(codeOf(error), messageOf(error), error?.details ?? {})
|
|
67
|
+
return fail('docker/internal', messageOf(error))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {unknown} value */
|
|
71
|
+
function payloadOf(value) {
|
|
72
|
+
if (value === undefined || value === null) return {}
|
|
73
|
+
if (!isRecord(value)) throw new CommandError('请求参数必须是对象')
|
|
74
|
+
return value
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @param {unknown} value @param {string} field */
|
|
78
|
+
function optionalString(value, field) {
|
|
79
|
+
if (value === undefined || value === null) return undefined
|
|
80
|
+
return requireString(value, field)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @param {unknown} value */
|
|
84
|
+
function optionalBoolean(value) {
|
|
85
|
+
return value === true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @param {string} value */
|
|
89
|
+
function safeIdentifier(value) {
|
|
90
|
+
const id = requireString(value, 'id', { max: 256 })
|
|
91
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:/@-]{0,255}$/u.test(id)) throw new CommandError('资源 ID 不合法')
|
|
92
|
+
return id
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @param {unknown} value */
|
|
96
|
+
function safeImageRef(value) {
|
|
97
|
+
const reference = requireString(value, 'image', { max: 512 })
|
|
98
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:/@-]*$/u.test(reference)) throw new CommandError('镜像名不合法')
|
|
99
|
+
return reference
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @param {unknown} value @param {string} field */
|
|
103
|
+
function stringList(value, field) {
|
|
104
|
+
if (value === undefined || value === null) return []
|
|
105
|
+
if (!Array.isArray(value)) throw new CommandError(`${field} 必须是字符串数组`)
|
|
106
|
+
if (value.length > 64) throw new CommandError(`${field} 数量过多`)
|
|
107
|
+
return value.map((entry, index) => requireString(entry, `${field}[${String(index)}]`, { max: 512 }))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @param {string} value */
|
|
111
|
+
function safePort(value) {
|
|
112
|
+
if (!/^(?:(?:\d{1,3}\.){3}\d{1,3}:)?\d{1,5}:\d{1,5}(?:\/(?:tcp|udp))?$/u.test(value) && !/^\d{1,5}(?:\/(?:tcp|udp))?$/u.test(value)) {
|
|
113
|
+
throw new CommandError(`端口格式不合法: ${value}(示例 8080:80)`)
|
|
114
|
+
}
|
|
115
|
+
return value
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @param {string} value */
|
|
119
|
+
function safeEnv(value) {
|
|
120
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*=[^\r\n]*$/u.test(value)) throw new CommandError(`环境变量格式不合法: ${value}(示例 KEY=VALUE)`)
|
|
121
|
+
return value
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export class DockerService {
|
|
125
|
+
/**
|
|
126
|
+
* @param {{
|
|
127
|
+
* subprocess: any,
|
|
128
|
+
* logger?: any,
|
|
129
|
+
* config?: Record<string, any>,
|
|
130
|
+
* env?: NodeJS.ProcessEnv,
|
|
131
|
+
* home?: string,
|
|
132
|
+
* platform?: NodeJS.Platform,
|
|
133
|
+
* }} options
|
|
134
|
+
*/
|
|
135
|
+
constructor(options) {
|
|
136
|
+
this.logger = options.logger
|
|
137
|
+
this.config = options.config ?? {}
|
|
138
|
+
this.home = options.home ?? homedir()
|
|
139
|
+
this.env = options.env ?? process.env
|
|
140
|
+
this.cli = new DockerCli({
|
|
141
|
+
subprocess: options.subprocess,
|
|
142
|
+
config: this.config,
|
|
143
|
+
env: this.env,
|
|
144
|
+
home: this.home,
|
|
145
|
+
platform: options.platform,
|
|
146
|
+
logger: this.logger,
|
|
147
|
+
})
|
|
148
|
+
this.jobs = new JobManager({
|
|
149
|
+
subprocess: options.subprocess,
|
|
150
|
+
cli: this.cli,
|
|
151
|
+
maxOutputBytes: Number(this.config.maxOutputBytes ?? 1024 * 1024),
|
|
152
|
+
maxRuns: Number(this.config.maxConcurrentRuns ?? 4),
|
|
153
|
+
idPrefix: 'docker',
|
|
154
|
+
})
|
|
155
|
+
this.runtime = createRuntimeResolver({
|
|
156
|
+
cli: this.cli,
|
|
157
|
+
platform: options.platform,
|
|
158
|
+
env: this.env,
|
|
159
|
+
config: this.config,
|
|
160
|
+
})
|
|
161
|
+
this.attach = new AttachManager({
|
|
162
|
+
subprocess: options.subprocess,
|
|
163
|
+
maxBytes: Number(this.config.maxOutputBytes ?? 1024 * 1024),
|
|
164
|
+
maxSessions: Number(this.config.maxAttachSessions ?? 2),
|
|
165
|
+
})
|
|
166
|
+
this.apple = new AppleProvider({
|
|
167
|
+
subprocess: options.subprocess,
|
|
168
|
+
config: this.config,
|
|
169
|
+
env: this.env,
|
|
170
|
+
home: this.home,
|
|
171
|
+
platform: options.platform,
|
|
172
|
+
logger: this.logger,
|
|
173
|
+
})
|
|
174
|
+
this.contextsCache = undefined
|
|
175
|
+
this.toolsEnabled = false
|
|
176
|
+
this.imageArchCache = new Map()
|
|
177
|
+
this.statusCache = new Map()
|
|
178
|
+
this.valueCache = new Map()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** @param {string} method @param {unknown} rawPayload @param {AbortSignal | undefined} signal @param {any} peer */
|
|
182
|
+
async handle(method, rawPayload, signal, peer) {
|
|
183
|
+
try {
|
|
184
|
+
if (peer?.principal?.kind !== 'owner') return fail('docker/forbidden', '只有 DSH owner 会话可以访问本机 Docker')
|
|
185
|
+
const payload = payloadOf(rawPayload)
|
|
186
|
+
const provider = this.providerOf(payload)
|
|
187
|
+
switch (method) {
|
|
188
|
+
case 'ping':
|
|
189
|
+
return ok({
|
|
190
|
+
ok: true,
|
|
191
|
+
plugin: 'dsh-plugin-container',
|
|
192
|
+
version: '0.2.0',
|
|
193
|
+
dockerPath: this.cli.dockerPath ?? null,
|
|
194
|
+
containerPath: this.apple.cli.containerPath ?? null,
|
|
195
|
+
})
|
|
196
|
+
case 'providers':
|
|
197
|
+
return ok(await this.providers(signal))
|
|
198
|
+
case 'status':
|
|
199
|
+
case 'list':
|
|
200
|
+
case 'inspect':
|
|
201
|
+
case 'run':
|
|
202
|
+
case 'action':
|
|
203
|
+
case 'runtimeAction':
|
|
204
|
+
if (provider === 'apple-container') return ok(await this.apple[method](payload, signal))
|
|
205
|
+
if (method === 'action') return ok(await this.action(payload, signal))
|
|
206
|
+
return ok(await this[method](payload, signal))
|
|
207
|
+
case 'attach':
|
|
208
|
+
return ok(await this.attachStart(provider, payload, signal))
|
|
209
|
+
case 'attachPoll':
|
|
210
|
+
return ok(this.attach.poll(payload))
|
|
211
|
+
case 'attachInput':
|
|
212
|
+
return ok(await this.attach.write(payload))
|
|
213
|
+
case 'attachResize':
|
|
214
|
+
return ok(await this.attach.resize(payload))
|
|
215
|
+
case 'attachClose':
|
|
216
|
+
return ok(await this.attach.close(payload))
|
|
217
|
+
case 'poll':
|
|
218
|
+
case 'cancel': {
|
|
219
|
+
const runProvider = this.providerForRun(payload)
|
|
220
|
+
if (runProvider === 'apple-container') return ok(this.apple[method](payload))
|
|
221
|
+
return ok(method === 'poll' ? this.jobs.poll(this.pollInput(payload)) : this.jobs.cancel(requireString(payload.runId, 'runId')))
|
|
222
|
+
}
|
|
223
|
+
default:
|
|
224
|
+
return fail('docker/unknown-method', `未知的 RPC 方法: ${String(method)}`)
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return failureFrom(error)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** @param {Record<string, any>} payload */
|
|
232
|
+
providerOf(payload) {
|
|
233
|
+
return payload.provider === 'apple-container' || payload.provider === 'apple' ? 'apple-container' : 'docker'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** @param {Record<string, any>} payload */
|
|
237
|
+
providerForRun(payload) {
|
|
238
|
+
const runId = typeof payload.runId === 'string' ? payload.runId : ''
|
|
239
|
+
if (runId.startsWith('apple_')) return 'apple-container'
|
|
240
|
+
if (runId.startsWith('docker_')) return 'docker'
|
|
241
|
+
return this.providerOf(payload)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
enableTools() {
|
|
245
|
+
this.toolsEnabled = true
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** @param {AbortSignal | undefined} signal */
|
|
249
|
+
async providers(signal) {
|
|
250
|
+
const providers = []
|
|
251
|
+
try {
|
|
252
|
+
const path = await this.cli.ensureDocker({ signal })
|
|
253
|
+
providers.push({ id: 'docker', label: 'Docker', available: true, runtime: 'docker', detail: path })
|
|
254
|
+
} catch (error) {
|
|
255
|
+
providers.push({ id: 'docker', label: 'Docker', available: false, runtime: 'docker', detail: messageOf(error) })
|
|
256
|
+
}
|
|
257
|
+
providers.push(await this.apple.info(signal))
|
|
258
|
+
const defaultProvider = providers.find((provider) => provider.id === 'docker' && provider.available)?.id ?? providers.find((provider) => provider.available)?.id ?? 'docker'
|
|
259
|
+
const features = ['attach', 'run', 'image-architecture']
|
|
260
|
+
if (this.toolsEnabled) features.push('ai-tools')
|
|
261
|
+
return { providers, defaultProvider, features }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** @param {Record<string, any>} payload */
|
|
265
|
+
pollInput(payload) {
|
|
266
|
+
return {
|
|
267
|
+
runId: requireString(payload.runId, 'runId'),
|
|
268
|
+
stdoutOffset: optionalInteger(payload.stdoutOffset, 'stdoutOffset', { min: 0 }),
|
|
269
|
+
stderrOffset: optionalInteger(payload.stderrOffset, 'stderrOffset', { min: 0 }),
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** @param {AbortSignal | undefined} signal */
|
|
274
|
+
async loadContexts(signal) {
|
|
275
|
+
const now = Date.now()
|
|
276
|
+
if (this.contextsCache !== undefined && now - this.contextsCache.at < CONTEXT_CACHE_MS) return this.contextsCache.value
|
|
277
|
+
let value = []
|
|
278
|
+
try {
|
|
279
|
+
await this.cli.ensureDocker({ signal })
|
|
280
|
+
const result = await this.cli.docker(['context', 'ls', '--format', 'json'], { context: null, signal, timeoutMs: 10_000 })
|
|
281
|
+
value = parseContexts(result.stdout).contexts
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (!(error instanceof DockerCliError)) {
|
|
284
|
+
this.logger?.warn?.(`docker status: context discovery failed: ${messageOf(error)}`)
|
|
285
|
+
}
|
|
286
|
+
value = []
|
|
287
|
+
}
|
|
288
|
+
this.contextsCache = { at: now, value }
|
|
289
|
+
return value
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @param {string | undefined} requested
|
|
294
|
+
* @param {AbortSignal | undefined} signal
|
|
295
|
+
* @param {{strict?: boolean}} [options]
|
|
296
|
+
*/
|
|
297
|
+
async resolveContext(requested, signal, options = {}) {
|
|
298
|
+
const contexts = await this.loadContexts(signal)
|
|
299
|
+
const known = new Set(contexts.map((context) => context.name))
|
|
300
|
+
let name
|
|
301
|
+
try {
|
|
302
|
+
name = normalizeContext(requested, { known })
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (options.strict === false) name = null
|
|
305
|
+
else throw error
|
|
306
|
+
}
|
|
307
|
+
const selected = name === null ? contexts.find((context) => context.current) ?? contexts[0] ?? null : contexts.find((context) => context.name === name) ?? null
|
|
308
|
+
if (name !== null && selected === null) throw new CommandError(`未知的 Docker context: ${name}`, 'docker/context-unknown')
|
|
309
|
+
return {
|
|
310
|
+
contexts,
|
|
311
|
+
current: contexts.find((context) => context.current) ?? null,
|
|
312
|
+
selected,
|
|
313
|
+
name: selected?.name ?? null,
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** @param {unknown} value @param {string} field */
|
|
318
|
+
cacheKey(parts) {
|
|
319
|
+
return parts.map((part) => (part === undefined || part === null ? '' : String(part))).join('\u0000')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** @param {string} key @param {() => Promise<any>} produce @param {number} ttl */
|
|
323
|
+
async cached(key, produce, ttl = VALUE_CACHE_MS) {
|
|
324
|
+
const now = Date.now()
|
|
325
|
+
const hit = this.valueCache.get(key)
|
|
326
|
+
if (hit !== undefined && now - hit.at < ttl) return hit.value
|
|
327
|
+
const value = await produce()
|
|
328
|
+
this.valueCache.set(key, { at: now, value })
|
|
329
|
+
return value
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
invalidate() {
|
|
333
|
+
this.statusCache.clear()
|
|
334
|
+
this.valueCache.clear()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @param {Record<string, any>} payload
|
|
339
|
+
* @param {AbortSignal | undefined} signal
|
|
340
|
+
*/
|
|
341
|
+
async status(payload, signal) {
|
|
342
|
+
const requested = optionalString(payload.context, 'context')
|
|
343
|
+
const cacheKey = this.cacheKey(['status', requested ?? ''])
|
|
344
|
+
const now = Date.now()
|
|
345
|
+
const hit = this.statusCache.get(cacheKey)
|
|
346
|
+
if (hit !== undefined && now - hit.at < STATUS_CACHE_MS) return hit.value
|
|
347
|
+
|
|
348
|
+
const contexts = await this.loadContexts(signal)
|
|
349
|
+
const known = new Set(contexts.map((context) => context.name))
|
|
350
|
+
let selectedName = requested ?? null
|
|
351
|
+
if (selectedName !== null && !known.has(selectedName)) selectedName = null
|
|
352
|
+
const current = contexts.find((context) => context.current) ?? null
|
|
353
|
+
const selected = selectedName === null ? current ?? contexts[0] ?? null : contexts.find((context) => context.name === selectedName) ?? null
|
|
354
|
+
|
|
355
|
+
let dockerPath = null
|
|
356
|
+
let cliError = null
|
|
357
|
+
try {
|
|
358
|
+
dockerPath = await this.cli.ensureDocker({ signal })
|
|
359
|
+
} catch (error) {
|
|
360
|
+
cliError = error
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let version = null
|
|
364
|
+
let info = null
|
|
365
|
+
let daemonError = null
|
|
366
|
+
let daemonState = 'unknown'
|
|
367
|
+
let versionError = null
|
|
368
|
+
if (dockerPath !== null) {
|
|
369
|
+
try {
|
|
370
|
+
const result = await this.cli.docker(['version', '--format', '{{json .}}'], {
|
|
371
|
+
context: selected?.name ?? null,
|
|
372
|
+
signal,
|
|
373
|
+
timeoutMs: 12_000,
|
|
374
|
+
})
|
|
375
|
+
version = mapDockerVersion(JSON.parse(result.stdout))
|
|
376
|
+
if (version.server !== null) daemonState = 'running'
|
|
377
|
+
} catch (error) {
|
|
378
|
+
versionError = error
|
|
379
|
+
if (error instanceof DockerCommandError) daemonError = error.stderr || error.message
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const result = await this.cli.docker(['info', '--format', '{{json .}}'], {
|
|
383
|
+
context: selected?.name ?? null,
|
|
384
|
+
signal,
|
|
385
|
+
timeoutMs: 15_000,
|
|
386
|
+
})
|
|
387
|
+
info = mapDockerInfo(JSON.parse(result.stdout))
|
|
388
|
+
daemonState = 'running'
|
|
389
|
+
daemonError = null
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (daemonError === null) daemonError = error instanceof DockerCommandError ? error.stderr || error.message : messageOf(error)
|
|
392
|
+
if (daemonState !== 'running') {
|
|
393
|
+
daemonState = /cannot connect|is the docker daemon running|no such host|connection refused|permission denied/iu.test(daemonError)
|
|
394
|
+
? 'stopped'
|
|
395
|
+
: 'unreachable'
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
let runtime
|
|
401
|
+
try {
|
|
402
|
+
const resolved = await this.runtime.resolve(contexts, selected?.name ?? requested ?? null, {
|
|
403
|
+
running: daemonState === 'running',
|
|
404
|
+
error: daemonError,
|
|
405
|
+
}, signal)
|
|
406
|
+
runtime = resolved.runtime
|
|
407
|
+
} catch (error) {
|
|
408
|
+
runtime = {
|
|
409
|
+
kind: 'unknown',
|
|
410
|
+
label: 'Docker',
|
|
411
|
+
profile: null,
|
|
412
|
+
installed: null,
|
|
413
|
+
running: daemonState === 'running',
|
|
414
|
+
vmRunning: daemonState === 'running',
|
|
415
|
+
canStart: false,
|
|
416
|
+
canStop: false,
|
|
417
|
+
canRestart: false,
|
|
418
|
+
endpoint: selected?.endpoint ?? '',
|
|
419
|
+
details: null,
|
|
420
|
+
error: messageOf(error),
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const warnings = []
|
|
425
|
+
if (selected?.error) warnings.push(selected.error)
|
|
426
|
+
if (daemonError) warnings.push(firstLine(daemonError))
|
|
427
|
+
if (versionError !== null && daemonState !== 'running') warnings.push(firstLine(messageOf(versionError)))
|
|
428
|
+
|
|
429
|
+
const value = {
|
|
430
|
+
provider: 'docker',
|
|
431
|
+
fetchedAt: Date.now(),
|
|
432
|
+
platform: process.platform,
|
|
433
|
+
cli: {
|
|
434
|
+
installed: dockerPath !== null,
|
|
435
|
+
path: dockerPath,
|
|
436
|
+
version: version?.client?.version ?? null,
|
|
437
|
+
error: cliError === null ? null : messageOf(cliError),
|
|
438
|
+
},
|
|
439
|
+
daemon: {
|
|
440
|
+
state: daemonState,
|
|
441
|
+
error: daemonError === null ? null : summarizeStderr(daemonError),
|
|
442
|
+
},
|
|
443
|
+
context: {
|
|
444
|
+
selected: selected?.name ?? null,
|
|
445
|
+
current: current?.name ?? null,
|
|
446
|
+
endpoint: selected?.endpoint ?? '',
|
|
447
|
+
description: selected?.description ?? '',
|
|
448
|
+
contexts,
|
|
449
|
+
},
|
|
450
|
+
client: version?.client ?? info?.client ?? null,
|
|
451
|
+
server: info?.server ?? version?.server ?? null,
|
|
452
|
+
runtime,
|
|
453
|
+
env: {
|
|
454
|
+
dockerHost: typeof this.env.DOCKER_HOST === 'string' && this.env.DOCKER_HOST !== '',
|
|
455
|
+
dockerContext: typeof this.env.DOCKER_CONTEXT === 'string' ? this.env.DOCKER_CONTEXT : null,
|
|
456
|
+
},
|
|
457
|
+
capabilities: {
|
|
458
|
+
enableRuntimeControl: this.runtime.controlEnabled,
|
|
459
|
+
maxOutputBytes: Number(this.config.maxOutputBytes ?? 1024 * 1024),
|
|
460
|
+
defaultTimeoutMs: Number(this.config.defaultTimeoutMs ?? 120_000),
|
|
461
|
+
compose: true,
|
|
462
|
+
contexts: true,
|
|
463
|
+
canCompose: selected !== null,
|
|
464
|
+
},
|
|
465
|
+
warnings,
|
|
466
|
+
}
|
|
467
|
+
this.statusCache.set(cacheKey, { at: now, value })
|
|
468
|
+
return value
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* @param {Record<string, any>} payload
|
|
473
|
+
* @param {AbortSignal | undefined} signal
|
|
474
|
+
*/
|
|
475
|
+
async list(payload, signal) {
|
|
476
|
+
const resource = requireString(payload.resource, 'resource')
|
|
477
|
+
const resolved = await this.resolveContext(optionalString(payload.context, 'context'), signal)
|
|
478
|
+
const context = resolved.name
|
|
479
|
+
const all = payload.all !== false
|
|
480
|
+
const key = this.cacheKey(['list', resource, context ?? '', all ? 'all' : 'running'])
|
|
481
|
+
return this.cached(key, async () => {
|
|
482
|
+
const command = {
|
|
483
|
+
containers: ['ps', ...(all ? ['-a'] : []), '--format', '{{json .}}'],
|
|
484
|
+
images: ['images', '--format', '{{json .}}'],
|
|
485
|
+
volumes: ['volume', 'ls', '--format', '{{json .}}'],
|
|
486
|
+
networks: ['network', 'ls', '--format', '{{json .}}'],
|
|
487
|
+
compose: ['compose', 'ls', '-a', '--format', 'json'],
|
|
488
|
+
disk: ['system', 'df', '--format', '{{json .}}'],
|
|
489
|
+
}[resource]
|
|
490
|
+
if (command === undefined) throw new CommandError(`不支持的资源类型: ${resource}`)
|
|
491
|
+
let result
|
|
492
|
+
try {
|
|
493
|
+
result = await this.cli.docker(command, { context, signal, timeoutMs: resource === 'disk' ? 30_000 : 20_000 })
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (resource !== 'compose') throw error
|
|
496
|
+
// Older Compose releases reject --format json; retry with the table.
|
|
497
|
+
result = await this.cli.docker(['compose', 'ls', '-a'], { context, signal, timeoutMs: 20_000 })
|
|
498
|
+
const parsed = parseComposeList(result.stdout)
|
|
499
|
+
return { fetchedAt: Date.now(), items: parsed.items, format: parsed.format, warnings: this.warningsOf(result.stderr) }
|
|
500
|
+
}
|
|
501
|
+
let items
|
|
502
|
+
let format = 'json'
|
|
503
|
+
if (resource === 'containers') items = parseNdjson(result.stdout).map(normalizeContainer)
|
|
504
|
+
else if (resource === 'images') {
|
|
505
|
+
items = parseNdjson(result.stdout).map(normalizeImage)
|
|
506
|
+
items = await this.withImageArchitectures(items, context, signal)
|
|
507
|
+
}
|
|
508
|
+
else if (resource === 'volumes') items = parseNdjson(result.stdout).map(normalizeVolume)
|
|
509
|
+
else if (resource === 'networks') items = parseNdjson(result.stdout).map(normalizeNetwork)
|
|
510
|
+
else if (resource === 'disk') items = parseSystemDf(result.stdout)
|
|
511
|
+
else {
|
|
512
|
+
const parsed = parseComposeList(result.stdout)
|
|
513
|
+
items = parsed.items
|
|
514
|
+
format = parsed.format
|
|
515
|
+
}
|
|
516
|
+
return { fetchedAt: Date.now(), items, format, warnings: this.warningsOf(result.stderr) }
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* `docker images` does not expose image architecture; inspect the listed IDs
|
|
522
|
+
* once and cache the stable platform result.
|
|
523
|
+
* @param {any[]} items
|
|
524
|
+
* @param {string | null} context
|
|
525
|
+
* @param {AbortSignal | undefined} signal
|
|
526
|
+
*/
|
|
527
|
+
async withImageArchitectures(items, context, signal) {
|
|
528
|
+
if (items.length === 0) return items
|
|
529
|
+
const now = Date.now()
|
|
530
|
+
const keyFor = (id) => `${context ?? ''}\u0000${id}`
|
|
531
|
+
const missing = []
|
|
532
|
+
for (const item of items) {
|
|
533
|
+
const cached = this.imageArchCache.get(keyFor(item.id))
|
|
534
|
+
if (cached === undefined || now - cached.at > 5 * 60_000) missing.push(item.id)
|
|
535
|
+
}
|
|
536
|
+
if (missing.length > 0) {
|
|
537
|
+
try {
|
|
538
|
+
const result = await this.cli.docker(
|
|
539
|
+
['image', 'inspect', '--format', '{{json .}}', ...missing.slice(0, 64)],
|
|
540
|
+
{ context, signal, timeoutMs: 20_000, maxBytes: 4 * 1024 * 1024 },
|
|
541
|
+
)
|
|
542
|
+
const byId = new Map()
|
|
543
|
+
for (const line of result.stdout.split(/\r?\n/u)) {
|
|
544
|
+
if (line.trim() === '') continue
|
|
545
|
+
try {
|
|
546
|
+
const value = JSON.parse(line)
|
|
547
|
+
if (typeof value?.Id !== 'string') continue
|
|
548
|
+
const architecture = [value.Os, value.Architecture, value.Variant].filter(Boolean).join('/')
|
|
549
|
+
byId.set(value.Id, architecture)
|
|
550
|
+
byId.set(value.Id.replace(/^sha256:/u, ''), architecture)
|
|
551
|
+
} catch {
|
|
552
|
+
// Ignore malformed inspect output; architecture is optional metadata.
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
for (const id of missing) {
|
|
556
|
+
const full = [...byId.keys()].find((candidate) => candidate === id || candidate.startsWith(id) || id.startsWith(candidate))
|
|
557
|
+
const value = full === undefined ? '' : byId.get(full)
|
|
558
|
+
this.imageArchCache.set(keyFor(id), { at: now, value: value ?? '' })
|
|
559
|
+
}
|
|
560
|
+
} catch {
|
|
561
|
+
for (const id of missing) this.imageArchCache.set(keyFor(id), { at: 0, value: '' })
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return items.map((item) => {
|
|
565
|
+
const cached = this.imageArchCache.get(keyFor(item.id))
|
|
566
|
+
return { ...item, architecture: cached?.value ?? '' }
|
|
567
|
+
})
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** @param {string} stderr */
|
|
571
|
+
warningsOf(stderr) {
|
|
572
|
+
const line = firstLine(stderr)
|
|
573
|
+
return line === '' ? [] : [line]
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* @param {Record<string, any>} payload
|
|
578
|
+
* @param {AbortSignal | undefined} signal
|
|
579
|
+
*/
|
|
580
|
+
async inspect(payload, signal) {
|
|
581
|
+
const kind = requireString(payload.kind, 'kind')
|
|
582
|
+
if (!['container', 'image', 'volume', 'network'].includes(kind)) throw new CommandError('不支持的 inspect 类型')
|
|
583
|
+
const id = safeIdentifier(payload.id)
|
|
584
|
+
const resolved = await this.resolveContext(optionalString(payload.context, 'context'), signal)
|
|
585
|
+
const result = await this.cli.docker([kind, 'inspect', id], {
|
|
586
|
+
context: resolved.name,
|
|
587
|
+
signal,
|
|
588
|
+
timeoutMs: 20_000,
|
|
589
|
+
maxBytes: 4 * 1024 * 1024,
|
|
590
|
+
})
|
|
591
|
+
try {
|
|
592
|
+
const parsed = JSON.parse(result.stdout)
|
|
593
|
+
const value = Array.isArray(parsed) ? parsed[0] ?? null : parsed
|
|
594
|
+
return { value, raw: result.stdout, warnings: this.warningsOf(result.stderr) }
|
|
595
|
+
} catch (error) {
|
|
596
|
+
return { value: null, raw: result.stdout, parseError: messageOf(error), warnings: this.warningsOf(result.stderr) }
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* @param {Record<string, any>} payload
|
|
602
|
+
* @param {AbortSignal | undefined} signal
|
|
603
|
+
*/
|
|
604
|
+
async run(payload, signal) {
|
|
605
|
+
const dockerPath = await this.cli.ensureDocker({ signal })
|
|
606
|
+
const resolved = await this.resolveContext(optionalString(payload.context, 'context'), signal)
|
|
607
|
+
let subargv
|
|
608
|
+
if (payload.argv !== undefined) {
|
|
609
|
+
if (payload.command !== undefined) throw new CommandError('command 和 argv 只能提供一个')
|
|
610
|
+
subargv = normalizeSubcommand(payload.argv, { dockerPath })
|
|
611
|
+
} else {
|
|
612
|
+
subargv = parseCommand(requireString(payload.command, 'command'), { dockerPath })
|
|
613
|
+
}
|
|
614
|
+
const context = resolved.name
|
|
615
|
+
const cwd = resolveCwd(payload.cwd, { fallback: this.home, statSync })
|
|
616
|
+
const displayCommand = describeCommand(subargv, context)
|
|
617
|
+
const destructive = detectDestructive(subargv)
|
|
618
|
+
if (destructive.dangerous && payload.confirm !== true) {
|
|
619
|
+
throw Object.assign(new Error(`危险操作需要确认: ${displayCommand}`), {
|
|
620
|
+
code: 'docker/confirmation-required',
|
|
621
|
+
details: { command: displayCommand, reason: destructive.reason },
|
|
622
|
+
})
|
|
623
|
+
}
|
|
624
|
+
const timeoutMs = optionalInteger(payload.timeoutMs, 'timeoutMs', { min: 5000, max: 30 * 60_000 }) ?? Number(this.config.defaultTimeoutMs ?? 120_000)
|
|
625
|
+
const argv = this.cli.argvFor(subargv, { context })
|
|
626
|
+
const run = this.jobs.start({
|
|
627
|
+
argv,
|
|
628
|
+
displayCommand,
|
|
629
|
+
context,
|
|
630
|
+
cwd,
|
|
631
|
+
title: optionalString(payload.title, 'title') ?? '',
|
|
632
|
+
timeoutMs,
|
|
633
|
+
})
|
|
634
|
+
this.invalidate()
|
|
635
|
+
return { run }
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Start an interactive attach PTY. Docker attaches PID 1; Apple container has
|
|
640
|
+
* no attach verb, so it opens an exec shell in the running container.
|
|
641
|
+
* @param {'docker' | 'apple-container'} provider
|
|
642
|
+
* @param {Record<string, any>} payload
|
|
643
|
+
* @param {AbortSignal | undefined} signal
|
|
644
|
+
*/
|
|
645
|
+
async attachStart(provider, payload, signal) {
|
|
646
|
+
const id = safeIdentifier(payload.id)
|
|
647
|
+
const name = optionalString(payload.name, 'name') ?? id
|
|
648
|
+
const cols = optionalInteger(payload.cols, 'cols', { min: 20, max: 300 }) ?? 120
|
|
649
|
+
const rows = optionalInteger(payload.rows, 'rows', { min: 5, max: 120 }) ?? 30
|
|
650
|
+
if (provider === 'apple-container') {
|
|
651
|
+
const path = await this.apple.cli.ensureContainer({ signal })
|
|
652
|
+
let running = false
|
|
653
|
+
try {
|
|
654
|
+
const raw = await this.apple.cli.json(['list', '-a', '--format', 'json'], { signal, timeoutMs: 15_000 })
|
|
655
|
+
const found = (Array.isArray(raw) ? raw : []).find((entry) => entry?.id === id || entry?.configuration?.id === id)
|
|
656
|
+
running = found?.status?.state === 'running'
|
|
657
|
+
} catch (error) {
|
|
658
|
+
throw new CommandError(`无法读取 Apple container 状态: ${messageOf(error)}`, 'container/exec-failed')
|
|
659
|
+
}
|
|
660
|
+
if (!running) throw new CommandError('容器未运行,无法 attach/exec', 'container/not-running')
|
|
661
|
+
const argv = [path, 'exec', '-i', '-t', id, '/bin/sh']
|
|
662
|
+
return {
|
|
663
|
+
session: await this.attach.start({
|
|
664
|
+
argv,
|
|
665
|
+
cwd: this.home,
|
|
666
|
+
env: this.apple.cli.childEnv(),
|
|
667
|
+
provider,
|
|
668
|
+
containerId: id,
|
|
669
|
+
containerName: name,
|
|
670
|
+
command: `container exec -i -t ${id} /bin/sh`,
|
|
671
|
+
cols,
|
|
672
|
+
rows,
|
|
673
|
+
signal,
|
|
674
|
+
}),
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
await this.cli.ensureDocker({ signal })
|
|
678
|
+
const resolved = await this.resolveContext(optionalString(payload.context, 'context'), signal)
|
|
679
|
+
const context = resolved.name
|
|
680
|
+
const running = await this.cli.docker(['inspect', '--format', '{{.State.Running}}', id], {
|
|
681
|
+
context,
|
|
682
|
+
signal,
|
|
683
|
+
timeoutMs: 15_000,
|
|
684
|
+
})
|
|
685
|
+
if (running.stdout.trim() !== 'true') throw new CommandError('容器未运行,无法 attach', 'docker/not-running')
|
|
686
|
+
return {
|
|
687
|
+
session: await this.attach.start({
|
|
688
|
+
argv: this.cli.argvFor(['attach', id], { context }),
|
|
689
|
+
cwd: this.home,
|
|
690
|
+
env: this.cli.childEnv(),
|
|
691
|
+
provider: 'docker',
|
|
692
|
+
containerId: id,
|
|
693
|
+
containerName: name,
|
|
694
|
+
command: describeCommand(['attach', id], context),
|
|
695
|
+
cols,
|
|
696
|
+
rows,
|
|
697
|
+
signal,
|
|
698
|
+
}),
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Semantic quick action for Docker; provider-specific syntax is centralized here.
|
|
704
|
+
* @param {Record<string, any>} payload
|
|
705
|
+
* @param {AbortSignal | undefined} signal
|
|
706
|
+
*/
|
|
707
|
+
async action(payload, signal) {
|
|
708
|
+
const kind = requireString(payload.kind, 'kind')
|
|
709
|
+
const action = requireString(payload.action, 'action')
|
|
710
|
+
if (kind === 'container' && action === 'run') {
|
|
711
|
+
const image = safeImageRef(payload.image)
|
|
712
|
+
const name = optionalString(payload.name, 'name')
|
|
713
|
+
if (name !== undefined) safeIdentifier(name)
|
|
714
|
+
const args = parseArguments(optionalString(payload.command, 'command') ?? '')
|
|
715
|
+
const ports = stringList(payload.ports, 'ports').map(safePort)
|
|
716
|
+
const env = stringList(payload.env, 'env').map(safeEnv)
|
|
717
|
+
const argv = [
|
|
718
|
+
'run',
|
|
719
|
+
'-d',
|
|
720
|
+
'-i',
|
|
721
|
+
'-t',
|
|
722
|
+
...(name === undefined ? [] : ['--name', name]),
|
|
723
|
+
...ports.flatMap((port) => ['-p', port]),
|
|
724
|
+
...env.flatMap((entry) => ['-e', entry]),
|
|
725
|
+
image,
|
|
726
|
+
...args,
|
|
727
|
+
]
|
|
728
|
+
return this.run({
|
|
729
|
+
argv,
|
|
730
|
+
context: payload.context,
|
|
731
|
+
cwd: payload.cwd,
|
|
732
|
+
title: payload.title ?? `启动容器 ${image}`,
|
|
733
|
+
}, signal)
|
|
734
|
+
}
|
|
735
|
+
const id = safeIdentifier(payload.id ?? payload.reference)
|
|
736
|
+
let argv
|
|
737
|
+
if (kind === 'container') {
|
|
738
|
+
if (action === 'start') argv = ['start', id]
|
|
739
|
+
else if (action === 'stop') argv = ['stop', id]
|
|
740
|
+
else if (action === 'restart') argv = ['restart', id]
|
|
741
|
+
else if (action === 'delete') argv = ['rm', '-f', id]
|
|
742
|
+
else if (action === 'logs') argv = ['logs', '--tail', payload.follow === true ? '100' : '500', ...(payload.follow === true ? ['-f'] : []), id]
|
|
743
|
+
} else if (kind === 'image') {
|
|
744
|
+
if (action === 'pull') argv = ['pull', id]
|
|
745
|
+
else if (action === 'delete') argv = ['rmi', id]
|
|
746
|
+
} else if (kind === 'volume' && action === 'delete') {
|
|
747
|
+
argv = ['volume', 'rm', id]
|
|
748
|
+
} else if (kind === 'network' && action === 'delete') {
|
|
749
|
+
argv = ['network', 'rm', id]
|
|
750
|
+
}
|
|
751
|
+
if (argv === undefined) throw new CommandError(`不支持的操作: ${kind}/${action}`)
|
|
752
|
+
return this.run({
|
|
753
|
+
argv,
|
|
754
|
+
context: payload.context,
|
|
755
|
+
cwd: payload.cwd,
|
|
756
|
+
confirm: payload.confirm,
|
|
757
|
+
timeoutMs: payload.timeoutMs,
|
|
758
|
+
title: payload.title ?? `${kind} ${action} ${id}`,
|
|
759
|
+
}, signal)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* @param {Record<string, any>} payload
|
|
764
|
+
* @param {AbortSignal | undefined} signal
|
|
765
|
+
*/
|
|
766
|
+
async runtimeAction(payload, signal) {
|
|
767
|
+
const action = requireString(payload.action, 'action')
|
|
768
|
+
if (!['start', 'stop', 'restart'].includes(action)) throw new CommandError('不支持的运行时操作')
|
|
769
|
+
if ((action === 'stop' || action === 'restart') && payload.confirm !== true) {
|
|
770
|
+
throw Object.assign(new Error(`停止/重启 Docker 运行时需要确认`), {
|
|
771
|
+
code: 'docker/confirmation-required',
|
|
772
|
+
details: { command: action === 'stop' ? '停止运行时' : '重启运行时', reason: 'runtime-stop' },
|
|
773
|
+
})
|
|
774
|
+
}
|
|
775
|
+
const status = await this.status({ context: payload.context }, signal)
|
|
776
|
+
const spec = this.runtime.actionSpec(action, status.runtime)
|
|
777
|
+
const run = this.jobs.start({
|
|
778
|
+
argv: spec.argv,
|
|
779
|
+
displayCommand: spec.displayCommand,
|
|
780
|
+
context: null,
|
|
781
|
+
cwd: this.home,
|
|
782
|
+
title: spec.title,
|
|
783
|
+
timeoutMs: spec.timeoutMs,
|
|
784
|
+
})
|
|
785
|
+
this.invalidate()
|
|
786
|
+
return { run, dispatched: true, message: spec.title }
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Read container logs for a tool (not exposed as a UI RPC).
|
|
791
|
+
* @param {Record<string, any>} payload
|
|
792
|
+
* @param {AbortSignal | undefined} signal
|
|
793
|
+
*/
|
|
794
|
+
async logs(payload, signal) {
|
|
795
|
+
const provider = payload.provider === 'apple-container' ? 'apple-container' : 'docker'
|
|
796
|
+
const id = safeIdentifier(payload.id)
|
|
797
|
+
const tail = optionalInteger(payload.tail ?? 200, 'tail', { min: 1, max: 500 })
|
|
798
|
+
const maxBytes = 256 * 1024
|
|
799
|
+
if (provider === 'apple-container') {
|
|
800
|
+
const path = await this.apple.cli.ensureContainer({ signal })
|
|
801
|
+
const result = await this.apple.cli.exec(['logs', '-n', String(tail), id], { signal, cwd: this.home, timeoutMs: 20_000, maxBytes })
|
|
802
|
+
const text = `${result.stdout}${result.stderr}`
|
|
803
|
+
if (result.timedOut) throw new CommandError('读取 Apple container 日志超时', 'container/timeout')
|
|
804
|
+
if (result.exitCode !== 0) {
|
|
805
|
+
throw Object.assign(new CommandError(text.trim() || `container logs 退出码 ${String(result.exitCode)}`, 'container/exec-failed'), { stderr: result.stderr })
|
|
806
|
+
}
|
|
807
|
+
return { provider, id, tail, text }
|
|
808
|
+
}
|
|
809
|
+
await this.cli.ensureDocker({ signal })
|
|
810
|
+
const resolved = await this.resolveContext(optionalString(payload.context, 'context'), signal)
|
|
811
|
+
const result = await this.cli.docker(['logs', '--tail', String(tail), id], {
|
|
812
|
+
context: resolved.name,
|
|
813
|
+
signal,
|
|
814
|
+
timeoutMs: 20_000,
|
|
815
|
+
maxBytes,
|
|
816
|
+
})
|
|
817
|
+
return { provider, id, tail, text: `${result.stdout}${result.stderr}` }
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
dispose() {
|
|
821
|
+
this.jobs.dispose()
|
|
822
|
+
this.attach.dispose().catch(() => {})
|
|
823
|
+
this.apple.dispose()
|
|
824
|
+
this.statusCache.clear()
|
|
825
|
+
this.valueCache.clear()
|
|
826
|
+
this.imageArchCache.clear()
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export function createDockerService(options) {
|
|
831
|
+
return new DockerService(options)
|
|
832
|
+
}
|