Skip to content

Commit

Permalink
Add viewable cli process class
Browse files Browse the repository at this point in the history
  • Loading branch information
mattseddon committed Feb 27, 2023
1 parent 9bcbeaa commit 0429299
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 139 deletions.
99 changes: 18 additions & 81 deletions extension/src/cli/dvc/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ import { Config } from '../../config'
import { PseudoTerminal } from '../../vscode/pseudoTerminal'
import { createProcess, Process } from '../../process/execution'
import { StopWatch } from '../../util/time'
import { sendErrorTelemetryEvent, sendTelemetryEvent } from '../../telemetry'
import { EventName } from '../../telemetry/constants'
import { Toast } from '../../vscode/toast'
import { Disposable } from '../../class/dispose'
import {
captureStdErr,
notifyCompleted,
notifyOutput,
notifyStarted
} from '../util'

export const autoRegisteredCommands = {
EXPERIMENT_RESET_AND_RUN: 'runExperimentReset',
Expand Down Expand Up @@ -167,25 +171,23 @@ export class DvcRunner extends Disposable implements ICli {
const process = this.dispose.track(createProcess(options))
const baseEvent = { command, cwd, pid: process.pid }

this.processStarted.fire(baseEvent)
notifyStarted(baseEvent, this.processStarted)

this.notifyOutput(process)
notifyOutput(process, this.processOutput)

let stderr = ''
process.stderr?.on(
'data',
chunk => (stderr += (chunk as Buffer).toString())
)
const stderr = captureStdErr(process)

void process.on('close', exitCode => {
void this.dispose.untrack(process)
this.notifyCompleted({
...baseEvent,
duration: stopWatch.getElapsedTime(),
exitCode,
killed: process.killed,
stderr
})
notifyCompleted(
{
...baseEvent,
duration: stopWatch.getElapsedTime(),
exitCode,
stderr
},
this.processCompleted
)
})

return process
Expand All @@ -208,69 +210,4 @@ export class DvcRunner extends Disposable implements ICli {
cwd
})
}

private notifyOutput(process: Process) {
process.all?.on('data', chunk =>
this.processOutput.fire(
(chunk as Buffer)
.toString()
.split(/(\r?\n)/g)
.join('\r')
)
)
}

private notifyCompleted({
command,
pid,
cwd,
duration,
exitCode,
killed,
stderr
}: CliResult & {
killed: boolean
}) {
this.processCompleted.fire({
command,
cwd,
duration,
exitCode,
pid,
stderr: stderr?.replace(/\n+/g, '\n')
})

this.sendTelemetryEvent({ command, duration, exitCode, killed, stderr })
}

private sendTelemetryEvent({
command,
exitCode,
stderr,
duration,
killed
}: {
command: string
exitCode: number | null
stderr?: string
duration: number
killed: boolean
}) {
const properties = { command, exitCode }

if (!killed && exitCode && stderr) {
return sendErrorTelemetryEvent(
EventName.EXPERIMENTS_RUNNER_COMPLETED,
new Error(stderr),
duration,
properties
)
}

return sendTelemetryEvent(
EventName.EXPERIMENTS_RUNNER_COMPLETED,
{ ...properties, wasStopped: killed },
{ duration }
)
}
}
34 changes: 21 additions & 13 deletions extension/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Event, EventEmitter } from 'vscode'
import { CliError, MaybeConsoleError } from './error'
import { getCommandString } from './command'
import { CliError, MaybeConsoleError } from './error'
import { notifyCompleted, notifyStarted } from './util'
import { createProcess, Process, ProcessOptions } from '../process/execution'
import { StopWatch } from '../util/time'
import { Disposable } from '../class/dispose'
Expand Down Expand Up @@ -80,11 +81,15 @@ export class Cli extends Disposable implements ICli {

const { stdout, exitCode } = await process

this.processCompleted.fire({
...baseEvent,
duration: stopWatch.getElapsedTime(),
exitCode
})
notifyCompleted(
{
...baseEvent,
duration: stopWatch.getElapsedTime(),
exitCode
},
this.processCompleted
)

return stdout
} catch (error: unknown) {
throw this.processCliError(
Expand Down Expand Up @@ -121,7 +126,7 @@ export class Cli extends Disposable implements ICli {
private createProcess(baseEvent: CliEvent, options: ProcessOptions) {
const createdProcess = createProcess(options)
baseEvent.pid = createdProcess.pid
this.processStarted.fire(baseEvent)
notifyStarted(baseEvent, this.processStarted)

return createdProcess
}
Expand Down Expand Up @@ -171,12 +176,15 @@ export class Cli extends Disposable implements ICli {
baseError: error,
options
})
this.processCompleted.fire({
...baseEvent,
duration,
exitCode: cliError.exitCode,
stderr: cliError.stderr
})
notifyCompleted(
{
...baseEvent,
duration,
exitCode: cliError.exitCode,
stderr: cliError.stderr
},
this.processCompleted
)
return cliError
}
}
41 changes: 41 additions & 0 deletions extension/src/cli/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { EventEmitter } from 'vscode'
import { CliEvent, CliResult } from '.'
import { Process } from '../process/execution'

export const notifyStarted = (
baseEvent: CliEvent,
processStarted: EventEmitter<CliEvent>
): void => processStarted.fire(baseEvent)

export const notifyOutput = (
process: Process,
processOutput: EventEmitter<string>
): void => {
process.all?.on('data', chunk =>
processOutput.fire(
(chunk as Buffer)
.toString()
.split(/(\r?\n)/g)
.join('\r')
)
)
}

export const notifyCompleted = (
{ command, pid, cwd, duration, exitCode, stderr }: CliResult,
processCompleted: EventEmitter<CliResult>
): void =>
processCompleted.fire({
command,
cwd,
duration,
exitCode,
pid,
stderr: stderr?.replace(/\n+/g, '\n')
})

export const captureStdErr = (process: Process): string => {
let stderr = ''
process.stderr?.on('data', chunk => (stderr += (chunk as Buffer).toString()))
return stderr
}
92 changes: 92 additions & 0 deletions extension/src/cli/viewable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Event, EventEmitter } from 'vscode'
import { CliEvent, CliResult, CliStarted } from '.'
import {
captureStdErr,
notifyCompleted,
notifyOutput,
notifyStarted
} from './util'
import { getCommandString } from './command'
import { ProcessOptions, createProcess } from '../process/execution'
import { StopWatch } from '../util/time'
import { PseudoTerminal } from '../vscode/pseudoTerminal'
import { DeferredDisposable } from '../class/deferred'

export class ViewableCliProcess extends DeferredDisposable {
public readonly onDidDispose: Event<void>

private readonly pseudoTerminal: PseudoTerminal

constructor(
termName: string,
options: ProcessOptions,
processStarted: EventEmitter<CliStarted>,
processCompleted: EventEmitter<CliResult>
) {
super()
const processOutput = this.dispose.track(new EventEmitter<string>())
const terminalClosed = this.dispose.track(new EventEmitter<void>())
const onDidCloseTerminal = terminalClosed.event

this.pseudoTerminal = this.dispose.track(
new PseudoTerminal(processOutput, terminalClosed, termName)
)

this.pseudoTerminal.setBlocked(true)
void this.show().then(() => this.deferred.resolve())

this.createProcess(options, processStarted, processOutput, processCompleted)

const disposed = this.dispose.track(new EventEmitter<void>())
this.onDidDispose = disposed.event

this.dispose.track(
onDidCloseTerminal(() => {
disposed.fire()
this.dispose()
})
)
}

public show() {
return this.pseudoTerminal.openCurrentInstance()
}

private createProcess(
options: ProcessOptions,
processStarted: EventEmitter<CliEvent>,
processOutput: EventEmitter<string>,
processCompleted: EventEmitter<CliResult>
) {
processOutput.fire(
`Running: ${
options.executable === 'git' ? 'git' : 'dvc'
} ${options.args.join(' ')}\r\n\n`
)
const stopWatch = new StopWatch()
const process = this.dispose.track(createProcess(options))

const command = getCommandString(options)
const baseEvent = { command, cwd: options.cwd, pid: process.pid }

notifyStarted(baseEvent, processStarted)

notifyOutput(process, processOutput)

const stderr = captureStdErr(process)

void process.on('close', exitCode => {
void this.dispose.untrack(process)
notifyCompleted(
{
...baseEvent,
duration: stopWatch.getElapsedTime(),
exitCode,
stderr
},
processCompleted
)
processOutput.fire('\r\nPress any key to close\r\n\n')
})
}
}
8 changes: 0 additions & 8 deletions extension/src/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ export type ViewOpenedEventName =

export const EventName = Object.assign(
{
EXPERIMENTS_RUNNER_COMPLETED: 'experiments.runner.completed',

EXTENSION_EXECUTION_DETAILS_CHANGED: 'extension.executionDetails.changed',
EXTENSION_LOAD: 'extension.load',

Expand Down Expand Up @@ -118,12 +116,6 @@ export interface IEventNamePropertyMapping {
[EventName.EXTENSION_EXECUTION_DETAILS_CHANGED]: ExtensionProperties
[EventName.EXTENSION_LOAD]: ExtensionProperties

[EventName.EXPERIMENTS_RUNNER_COMPLETED]: {
command: string
exitCode: number | null
wasStopped?: boolean
}

[EventName.EXPERIMENT_AND_PLOTS_SHOW]: undefined
[EventName.EXPERIMENT_APPLY]: undefined
[EventName.EXPERIMENT_BRANCH]: undefined
Expand Down
7 changes: 6 additions & 1 deletion extension/src/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ const sanitizeProperties = (
if (value === undefined || value === null) {
continue
}
sanitizeProperty(eventName as string, sanitizedProperties, key, value)
sanitizeProperty(
eventName,
sanitizedProperties,
key,
value as string | number | boolean
)
}
return sanitizedProperties
}
Expand Down
Loading

0 comments on commit 0429299

Please sign in to comment.