Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 99 additions & 23 deletions lib/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,8 @@ export const configureNextcloud = async function(apps = ['viewer'], vendoredBran
console.log('│ └─ OK !')

// Build app list
const json = await runOcc(['app:list', '--output', 'json'], { container })
// fix dockerode bug returning invalid leading characters
const applist = JSON.parse(json.substring(json.indexOf('{')))
const json = await runOcc(['app:list', '--output', 'json'], { container, verbose: true })
const applist = JSON.parse(json)

// Enable apps and give status
for (const app of apps) {
Expand All @@ -270,6 +269,7 @@ export const configureNextcloud = async function(apps = ['viewer'], vendoredBran
await runOcc(['app:enable', '--force', app], { container, verbose: true })
} else if (app in VENDOR_APPS) {
// apps that are vendored but still missing (i.e. not build in or mounted already)
// NOTE: This currently fails in workflows since mounts are RO at this point
await runExec(['git', 'clone', '--depth=1', `--branch=${vendoredBranch}`, VENDOR_APPS[app], `apps/${app}`], { container, verbose: true })
await runOcc(['app:enable', '--force', app], { container, verbose: true })
} else {
Expand Down Expand Up @@ -386,13 +386,18 @@ interface RunExecOptions {
verbose: boolean;
}

type RunExecResult = {
stdout: string
stderr: string
}

/**
* Execute a command in the container
* Execute a command in the container and return stdout/stderr separately.
*/
export const runExec = async function(
export const runExecRaw = async function(
command: string | string[],
{ container, user='www-data', verbose=false, env=[] }: Partial<RunExecOptions> = {},
) {
): Promise<RunExecResult> {
container = container || getContainer()
const exec = await container.exec({
Cmd: typeof command === 'string' ? [command] : command,
Expand All @@ -402,32 +407,103 @@ export const runExec = async function(
Env: env,
})

return new Promise<string>((resolve, reject) => {
const dataStream = new PassThrough()
return new Promise<RunExecResult>((resolve, reject) => {
const stdoutStream = new PassThrough()
const stderrStream = new PassThrough()

exec.start({}, (err, stream) => {
if (stream) {
// Pass stdout and stderr to dataStream
exec.modem.demuxStream(stream, dataStream, dataStream)
stream.on('end', () => dataStream.end())
} else {
reject(err)
const stdout: string[] = []
const stderr: string[] = []

let settled = false
let finishedStreams = 0

const cleanup = () => {
stdoutStream.removeAllListeners()
stderrStream.removeAllListeners()
}

const settleResolve = (result: RunExecResult) => {
if (settled) {
return
}
settled = true
cleanup()
resolve(result)
}

const settleReject = (err: unknown) => {
if (settled) {
return
}
settled = true
cleanup()
reject(err)
}

const maybeResolve = () => {
finishedStreams++
if (finishedStreams === 2) {
settleResolve({
stdout: stdout.join(''),
stderr: stderr.join(''),
})
}
}

stdoutStream.on('data', (chunk) => {
const text = chunk.toString('utf8')
stdout.push(text)
if (verbose && text.trim()) {
console.log(`├─ stdout: ${text.trim().replace(/\n/gi, '\n├─ stdout: ')}`)
}
})

stderrStream.on('data', (chunk) => {
const text = chunk.toString('utf8')
stderr.push(text)
if (verbose && text.trim()) {
console.log(`├─ stderr: ${text.trim().replace(/\n/gi, '\n├─ stderr: ')}`)
}
})

const data: string[] = []
dataStream.on('data', (chunk) => {
data.push(chunk.toString('utf8'))
const printable = data.at(-1)?.trim()
if (verbose && printable) {
console.log(`├─ ${printable.replace(/\n/gi, '\n├─ ')}`)
stdoutStream.on('error', settleReject)
stderrStream.on('error', settleReject)

stdoutStream.on('end', maybeResolve)
stderrStream.on('end', maybeResolve)

exec.start({}, (err, stream) => {
if (err) {
settleReject(err)
return
}
if (!stream) {
settleReject(new Error('No exec stream returned'))
return
}

stream.on('error', settleReject)
stream.on('end', () => {
stdoutStream.end()
stderrStream.end()
})

exec.modem.demuxStream(stream, stdoutStream, stderrStream)
})
dataStream.on('error', (err) => reject(err))
dataStream.on('end', () => resolve(data.join('')))
})
}

/**
* Execute a command in the container and return stdout only.
*/
export const runExec = async function(
command: string | string[],
options: Partial<RunExecOptions> = {},
): Promise<string> {
const { stdout } = await runExecRaw(command, options)
return stdout
}

/**
* Execute an occ command in the container
*/
Expand Down