@@ -7,6 +7,14 @@ import { SpawnOptions } from 'child_process'
7
7
import { getLogger } from '../../logger'
8
8
import { getUserAgent } from '../../telemetry/util'
9
9
import { ChildProcessResult , ChildProcessOptions } from '../../utilities/childProcess'
10
+ import { ErrorInformation , ToolkitError } from '../../errors'
11
+
12
+ /** Generic SAM CLI invocation error. */
13
+ export class SamCliError extends ToolkitError . named ( 'SamCliError' ) {
14
+ public constructor ( message ?: string , info ?: ErrorInformation ) {
15
+ super ( message ?? 'SAM CLI failed' , { ...info , code : 'SamCliFailed' } )
16
+ }
17
+ }
10
18
11
19
export interface SamCliProcessInvokeOptions {
12
20
spawnOptions ?: SpawnOptions
@@ -34,71 +42,86 @@ export interface SamCliProcessInvoker {
34
42
}
35
43
36
44
export function makeUnexpectedExitCodeError ( message : string ) : Error {
37
- return new Error ( `Error with child process: ${ message } ` )
45
+ const msg = message ? message : 'SAM CLI failed'
46
+ return new SamCliError ( msg )
38
47
}
39
48
40
- export function logAndThrowIfUnexpectedExitCode ( processResult : ChildProcessResult , expectedExitCode : number ) : void {
41
- if ( processResult . exitCode === expectedExitCode ) {
49
+ export function logAndThrowIfUnexpectedExitCode ( r : ChildProcessResult , expectedExitCode : number ) : void {
50
+ if ( r . exitCode === expectedExitCode ) {
42
51
return
43
52
}
44
53
45
- const logger = getLogger ( )
46
-
47
- logger . error ( `Unexpected exitcode (${ processResult . exitCode } ), expecting (${ expectedExitCode } )` )
48
- logger . error ( `Error: ${ processResult . error } ` )
49
- logger . error ( `stderr: ${ processResult . stderr } ` )
50
- logger . error ( `stdout: ${ processResult . stdout } ` )
54
+ const errIndented = r . stderr . replace ( / \n / g, '\n ' ) . trim ( )
55
+ const outIndented = r . stdout . replace ( / \n / g, '\n ' ) . trim ( )
51
56
52
- let message : string | undefined
57
+ getLogger ( ) . error ( `SAM CLI failed (exitcode: ${ r . exitCode } , expected ${ expectedExitCode } ): ${ r . error ?. message ?? '' }
58
+ stdout:
59
+ ${ outIndented }
60
+ stderr:
61
+ ${ errIndented }
62
+ ` )
53
63
54
- if ( processResult . error instanceof Error ) {
55
- if ( processResult . error . message ) {
56
- message = processResult . error . message
57
- }
58
- }
59
- const usefulErrors = collectAcceptedErrorMessages ( processResult . stderr ) . join ( '\n' )
60
- throw makeUnexpectedExitCodeError ( message ?? usefulErrors )
64
+ const message = r . error instanceof Error ? r . error . message : collectSamErrors ( r . stderr ) . join ( '\n' )
65
+ throw makeUnexpectedExitCodeError ( message )
61
66
}
62
67
63
68
/**
64
- * Collect known errors messages from a sam error message
65
- * that may have multiple errors in one message.
66
- * @param errors A string that can have multiple error messages
69
+ * Collect known error messages from sam cli output, so they can be surfaced to the user.
70
+ *
71
+ * @param samOutput SAM CLI output containing potential error messages
67
72
*/
68
- export function collectAcceptedErrorMessages ( errorMessage : string ) : string [ ] {
69
- const errors = errorMessage . split ( '\n' )
70
- const shouldCollectFuncs = [ startsWithEscapeSequence , startsWithError ]
71
- return errors . filter ( error => {
72
- return shouldCollectFuncs . some ( shouldCollect => {
73
- return shouldCollect ( error )
74
- } )
75
- } )
73
+ export function collectSamErrors ( samOutput : string ) : string [ ] {
74
+ const lines = samOutput . split ( '\n' )
75
+ const matchedLines : string [ ] = [ ]
76
+ const matchers = [ matchSamError , matchAfterEscapeSeq ]
77
+ for ( const line of lines ) {
78
+ for ( const m of matchers ) {
79
+ const match = m ( line )
80
+ if ( match && match . trim ( ) !== '' ) {
81
+ matchedLines . push ( match )
82
+ break // Skip remaining matchers, go to next line.
83
+ }
84
+ }
85
+ }
86
+ return matchedLines
76
87
}
77
88
78
- /**
79
- * All accepted escape sequences.
80
- */
89
+ /** All accepted escape sequences. */
81
90
const yellowForeground = '[33m'
82
91
const acceptedSequences = [ yellowForeground ]
83
92
84
- /**
85
- * Returns true if text starts with an escape
86
- * sequence with one of the accepted sequences.
87
- */
88
- function startsWithEscapeSequence ( text : string , sequences = acceptedSequences ) : boolean {
93
+ /** Returns text after a known escape sequence, or empty string. */
94
+ function matchAfterEscapeSeq ( text : string , sequences = acceptedSequences ) : string {
95
+ text = text . trim ( )
89
96
const escapeInDecimal = 27
90
97
if ( text . charCodeAt ( 0 ) !== escapeInDecimal ) {
91
- return false
98
+ return ''
92
99
}
93
100
94
101
const remainingText = text . substring ( 1 )
95
- return sequences . some ( code => {
96
- return remainingText . startsWith ( code )
97
- } )
102
+ for ( const seq of sequences ) {
103
+ if ( remainingText . startsWith ( seq ) ) {
104
+ return remainingText . substring ( seq . length ) . trim ( )
105
+ }
106
+ }
107
+ return ''
98
108
}
99
109
100
- function startsWithError ( text : string ) : boolean {
101
- return text . startsWith ( 'Error:' )
110
+ function matchSamError ( text : string ) : string {
111
+ // These should be ordered by "specificity", to make the result more relevant for users.
112
+ const patterns = [
113
+ / \s * ( R u n n i n g .* r e q u i r e s D o c k e r \. ? ) / ,
114
+ / \s * ( D o c k e r .* n o t r e a c h a b l e \. ? ) / ,
115
+ / \b E r r o r : \s * ( .* ) / , // Goes last because it is the least specific.
116
+ ]
117
+ for ( const re of patterns ) {
118
+ const match = text . match ( re )
119
+ if ( match ?. [ 1 ] ) {
120
+ // Capture group 1 is the first (…) group in the regex pattern.
121
+ return match [ 1 ] . trim ( ) // Return _only_ the matched text. The rest is noise.
122
+ }
123
+ }
124
+ return ''
102
125
}
103
126
104
127
export async function addTelemetryEnvVar ( options : SpawnOptions | undefined ) : Promise < SpawnOptions > {
0 commit comments