file support
parent
6e6c05519d
commit
931dedb79b
@ -0,0 +1,89 @@
|
||||
import * as pty from 'node-pty'
|
||||
import { getContainerPath, runCommand } from './utils.js'
|
||||
|
||||
import fs from 'fs/promises'
|
||||
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
const stat = path =>
|
||||
new Promise(resolve =>
|
||||
fs
|
||||
.access(path)
|
||||
.then(() => resolve(true))
|
||||
.catch(() => resolve(false))
|
||||
)
|
||||
|
||||
const containerIds = new Set()
|
||||
|
||||
await cleanContainers()
|
||||
|
||||
async function storeContainers() {
|
||||
await fs.writeFile('containers-db.local.json', JSON.stringify([...containerIds]))
|
||||
}
|
||||
|
||||
async function cleanContainers() {
|
||||
if (!(await stat('containers-db.local.json'))) {
|
||||
storeContainers()
|
||||
}
|
||||
|
||||
const raw = JSON.parse(await fs.readFile('containers-db.local.json', 'utf8'))
|
||||
await Promise.all(raw.map(id => destroyContainer(id)))
|
||||
}
|
||||
|
||||
async function createContainer(tag) {
|
||||
const id = randomUUID()
|
||||
|
||||
await fs.mkdir(getContainerPath('phc', id), { recursive: true })
|
||||
|
||||
await runCommand('docker', [
|
||||
'run',
|
||||
'-d',
|
||||
'--name',
|
||||
id,
|
||||
'--volume',
|
||||
`${getContainerPath('phc', id)}:/project`,
|
||||
tag,
|
||||
'sleep',
|
||||
'infinity',
|
||||
])
|
||||
|
||||
containerIds.add(id)
|
||||
await storeContainers()
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
export async function createContainerPty(
|
||||
tag,
|
||||
options = {
|
||||
onExit,
|
||||
onData,
|
||||
|
||||
shellCommand: ['/bin/sh'],
|
||||
}
|
||||
) {
|
||||
const shellCommand = options.shellCommand ?? ['/bin/sh']
|
||||
const { onData, onExit } = options
|
||||
|
||||
const id = await createContainer(tag)
|
||||
|
||||
const container = pty.spawn('docker', ['exec', '-it', id, ...shellCommand])
|
||||
|
||||
container.onExit(async e => {
|
||||
onExit?.(e)
|
||||
await destroyContainer(id)
|
||||
})
|
||||
|
||||
container.onData(data => {
|
||||
onData?.(data)
|
||||
})
|
||||
|
||||
return { id, pty: container }
|
||||
}
|
||||
|
||||
async function destroyContainer(id) {
|
||||
await runCommand('docker', ['rm', '-f', id])
|
||||
|
||||
containerIds.delete(id)
|
||||
await storeContainers()
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import { spawn } from 'child_process'
|
||||
|
||||
export const CONTAINERS_ROOT = '/tmp/phc-run/containers'
|
||||
|
||||
export const getContainerPath = (ns, uuid) => `${CONTAINERS_ROOT}/${ns}/${uuid}`
|
||||
|
||||
export function runCommand(command, args, options = { stdin: null, capture: ['stdout', 'stderr'] }) {
|
||||
console.log(`Running: ${command} ${JSON.stringify(args)}`)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args)
|
||||
|
||||
let output = ''
|
||||
|
||||
if (options.capture.includes('stdout')) {
|
||||
child.stdout.on('data', data => {
|
||||
output += data.toString()
|
||||
})
|
||||
}
|
||||
|
||||
if (options.capture.includes('stderr')) {
|
||||
child.stderr.on('data', data => {
|
||||
output += data.toString()
|
||||
})
|
||||
}
|
||||
|
||||
if (options.stdin) {
|
||||
child.stdin.write(options.stdin)
|
||||
}
|
||||
|
||||
child.on('close', code => {
|
||||
if (code === 0) {
|
||||
resolve(output)
|
||||
} else {
|
||||
reject(new Error(`Command failed with code ${code}: ${output}`))
|
||||
console.error('Command error:', output)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue