Skip to content

Commit bfbacf5

Browse files
authored
chore(vscode): clean up format and improve logs (#4409)
1 parent 43094a1 commit bfbacf5

2 files changed

Lines changed: 51 additions & 52 deletions

File tree

vscode/extension/src/commands/format.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { traceLog } from '../utilities/common/log'
2-
import { execSync } from 'child_process'
32
import { sqlmeshExec } from '../utilities/sqlmesh/sqlmesh'
43
import { err, isErr, ok, Result } from '@bus/result'
54
import * as vscode from 'vscode'
@@ -10,6 +9,7 @@ import {
109
handleSqlmeshLspDependenciesMissingError,
1110
} from '../utilities/errors'
1211
import { AuthenticationProviderTobikoCloud } from '../auth/auth'
12+
import { execAsync } from '../utilities/exec'
1313

1414
export const format =
1515
(authProvider: AuthenticationProviderTobikoCloud) =>
@@ -37,22 +37,20 @@ export const format =
3737
vscode.window.showInformationMessage('Project formatted successfully')
3838
}
3939

40-
const internalFormat = async (): Promise<Result<number, ErrorType>> => {
41-
try {
42-
const exec = await sqlmeshExec()
43-
if (isErr(exec)) {
44-
return exec
45-
}
46-
execSync(`${exec.value.bin} format`, {
47-
encoding: 'utf-8',
48-
cwd: exec.value.workspacePath,
49-
env: exec.value.env,
50-
})
51-
return ok(0)
52-
} catch (error: any) {
40+
const internalFormat = async (): Promise<Result<undefined, ErrorType>> => {
41+
const exec = await sqlmeshExec()
42+
if (isErr(exec)) {
43+
return exec
44+
}
45+
const result = await execAsync(`${exec.value.bin}`, ['format'], {
46+
cwd: exec.value.workspacePath,
47+
env: exec.value.env,
48+
})
49+
if (result.exitCode !== 0) {
5350
return err({
5451
type: 'generic',
55-
message: `Error executing sqlmesh format: ${error.message}`,
52+
message: `Error executing sqlmesh format: ${result.stderr}`,
5653
})
5754
}
55+
return ok(undefined)
5856
}
Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { exec, ExecOptions } from 'child_process'
1+
import { exec, ExecOptions } from 'node:child_process'
22
import { traceInfo } from './common/log'
33

44
export interface ExecResult {
@@ -7,29 +7,51 @@ export interface ExecResult {
77
stderr: string
88
}
99

10-
export interface CancellableExecOptions extends ExecOptions {
11-
/** When `abort()` is called on this signal the child process is killed. */
12-
signal?: AbortSignal
10+
export async function execAsync(
11+
command: string,
12+
args: string[] = [],
13+
options: ExecOptions & { signal?: AbortSignal } = {},
14+
): Promise<ExecResult> {
15+
const fullCmd = `${command} ${args.join(' ')}`
16+
traceInfo(`Executing command: ${fullCmd}`)
17+
18+
try {
19+
const result = await execAsyncCore(command, args, options)
20+
traceInfo(
21+
`Command ${fullCmd} exited with code ${result.exitCode}; stdout: ${result.stdout}; stderr: ${result.stderr}`,
22+
)
23+
return result
24+
} catch (err) {
25+
if ((err as any)?.name === 'AbortError') {
26+
traceInfo(`Command ${fullCmd} was cancelled by AbortController`)
27+
} else {
28+
traceInfo(`Command ${fullCmd} failed: ${(err as Error).message}`)
29+
}
30+
throw err // keep original error semantics
31+
}
1332
}
1433

15-
export function execAsync(
34+
function execAsyncCore(
1635
command: string,
1736
args: string[],
18-
options: CancellableExecOptions = {},
37+
options: ExecOptions & { signal?: AbortSignal } = {},
1938
): Promise<ExecResult> {
20-
return new Promise((resolve, reject) => {
21-
// Pass the signal straight through to `exec`
22-
traceInfo(`Executing command: ${command} ${args.join(' ')}`)
39+
return new Promise<ExecResult>((resolve, reject) => {
2340
const child = exec(
2441
`${command} ${args.join(' ')}`,
2542
options,
2643
(error, stdout, stderr) => {
2744
if (error) {
28-
resolve({
29-
exitCode: typeof error.code === 'number' ? error.code : 1,
30-
stdout,
31-
stderr,
32-
})
45+
// Forward AbortError unchanged so callers can detect cancellation
46+
if ((error as NodeJS.ErrnoException).name === 'AbortError') {
47+
reject(error)
48+
} else {
49+
resolve({
50+
exitCode: typeof error.code === 'number' ? error.code : 1,
51+
stdout,
52+
stderr,
53+
})
54+
}
3355
return
3456
}
3557

@@ -41,28 +63,7 @@ export function execAsync(
4163
},
4264
)
4365

44-
/* ---------- Tie the Promise life‑cycle to the AbortSignal ---------- */
45-
46-
if (options.signal) {
47-
// If the caller aborts: kill the child and reject the promise
48-
const onAbort = () => {
49-
// `SIGTERM` is the default; use `SIGKILL` if you need something stronger
50-
child.kill()
51-
reject(new Error('Process cancelled'))
52-
}
53-
54-
if (options.signal.aborted) {
55-
onAbort()
56-
return
57-
}
58-
options.signal.addEventListener('abort', onAbort, { once: true })
59-
60-
// Clean‑up the event listener when the promise settles
61-
const cleanup = () => {
62-
options.signal!.removeEventListener('abort', onAbort)
63-
}
64-
child.once('exit', cleanup)
65-
child.once('error', cleanup)
66-
}
66+
// surface “spawn failed” errors that occur before the callback
67+
child.once('error', reject)
6768
})
6869
}

0 commit comments

Comments
 (0)