1
1
use super :: error:: AnalyzeError ;
2
2
use crate :: models:: HelpMetadata ;
3
3
use crate :: prelude:: {
4
- CaptureError , CaptureOpts , DefaultExecutionProvider , ExecutionProvider , OutputDestination ,
4
+ generate_env_vars, CaptureError , CaptureOpts , DefaultExecutionProvider , DoctorFix ,
5
+ ExecutionProvider , OutputCapture , OutputDestination ,
5
6
} ;
6
7
use crate :: shared:: prelude:: FoundConfig ;
7
8
use anyhow:: Result ;
@@ -12,7 +13,7 @@ use std::io::Cursor;
12
13
use std:: path:: PathBuf ;
13
14
use tokio:: fs:: File ;
14
15
use tokio:: io:: { AsyncBufReadExt , AsyncRead , BufReader , Stdin } ;
15
- use tracing:: { debug, info, warn} ;
16
+ use tracing:: { debug, error , info, warn} ;
16
17
17
18
#[ derive( Debug , Args ) ]
18
19
pub struct AnalyzeArgs {
@@ -52,16 +53,13 @@ pub async fn analyze_root(found_config: &FoundConfig, args: &AnalyzeArgs) -> Res
52
53
}
53
54
54
55
async fn analyze_logs ( found_config : & FoundConfig , args : & AnalyzeLogsArgs ) -> Result < i32 > {
55
- let has_known_error = match args. location . as_str ( ) {
56
+ let result = match args. location . as_str ( ) {
56
57
"-" => process_lines ( found_config, read_from_stdin ( ) . await ?) . await ?,
57
58
file_path => process_lines ( found_config, read_from_file ( file_path) . await ?) . await ?,
58
59
} ;
59
60
60
- if has_known_error {
61
- Ok ( 1 )
62
- } else {
63
- Ok ( 0 )
64
- }
61
+ report_result ( & result) ;
62
+ Ok ( result. to_exit_code ( ) )
65
63
}
66
64
67
65
async fn analyze_command ( found_config : & FoundConfig , args : & AnalyzeCommandArgs ) -> Result < i32 > {
@@ -78,26 +76,35 @@ async fn analyze_command(found_config: &FoundConfig, args: &AnalyzeCommandArgs)
78
76
output_dest : OutputDestination :: StandardOutWithPrefix ( "analyzing" . to_string ( ) ) ,
79
77
} ;
80
78
81
- let has_known_error = process_lines (
79
+ let result = process_lines (
82
80
found_config,
83
81
read_from_command ( & exec_runner, capture_opts) . await ?,
84
82
)
85
83
. await ?;
86
84
87
- if has_known_error {
88
- Ok ( 1 )
89
- } else {
90
- Ok ( 0 )
85
+ report_result ( & result) ;
86
+ Ok ( result. to_exit_code ( ) )
87
+ }
88
+
89
+ fn report_result ( status : & AnalyzeStatus ) {
90
+ match status {
91
+ AnalyzeStatus :: NoKnownErrorsFound => info ! ( target: "always" , "No known errors found" ) ,
92
+ AnalyzeStatus :: KnownErrorFoundNoFixFound => {
93
+ info ! ( target: "always" , "No automatic fix available" )
94
+ }
95
+ AnalyzeStatus :: KnownErrorFoundUserDenied => warn ! ( target: "always" , "User denied fix" ) ,
96
+ AnalyzeStatus :: KnownErrorFoundFixFailed => error ! ( target: "always" , "Fix failed" ) ,
97
+ AnalyzeStatus :: KnownErrorFoundFixSucceeded => info ! ( target: "always" , "Fix succeeded" ) ,
91
98
}
92
99
}
93
100
94
- async fn process_lines < T > ( found_config : & FoundConfig , input : T ) -> Result < bool >
101
+ async fn process_lines < T > ( found_config : & FoundConfig , input : T ) -> Result < AnalyzeStatus >
95
102
where
96
103
T : AsyncRead ,
97
104
T : AsyncBufReadExt ,
98
105
T : Unpin ,
99
106
{
100
- let mut has_known_error = false ;
107
+ let mut result = AnalyzeStatus :: NoKnownErrorsFound ;
101
108
let mut known_errors: BTreeMap < _ , _ > = found_config. known_error . clone ( ) ;
102
109
let mut line_number = 0 ;
103
110
@@ -110,8 +117,21 @@ where
110
117
if ke. regex . is_match ( & line) {
111
118
warn ! ( target: "always" , "Known error '{}' found on line {}" , ke. name( ) , line_number) ;
112
119
info ! ( target: "always" , "\t ==> {}" , ke. help_text) ;
120
+
121
+ result = match & ke. fix {
122
+ Some ( fix) => {
123
+ info ! ( target: "always" , "found a fix!" ) ;
124
+
125
+ tracing_indicatif:: suspend_tracing_indicatif ( || {
126
+ let exec_path = ke. metadata . exec_path ( ) ;
127
+ prompt_and_run_fix ( & found_config. working_dir , exec_path, fix)
128
+ } )
129
+ . await ?
130
+ }
131
+ None => AnalyzeStatus :: KnownErrorFoundNoFixFound ,
132
+ } ;
133
+
113
134
known_errors_to_remove. push ( name. clone ( ) ) ;
114
- has_known_error = true ;
115
135
}
116
136
}
117
137
@@ -127,7 +147,83 @@ where
127
147
}
128
148
}
129
149
130
- Ok ( has_known_error)
150
+ Ok ( result)
151
+ }
152
+
153
+ async fn prompt_and_run_fix (
154
+ working_dir : & PathBuf ,
155
+ exec_path : String ,
156
+ fix : & DoctorFix ,
157
+ ) -> Result < AnalyzeStatus > {
158
+ let fix_prompt = & fix. prompt . as_ref ( ) ;
159
+ let prompt_text = fix_prompt
160
+ . map ( |p| p. text . clone ( ) )
161
+ . unwrap_or ( "Would you like to run it?" . to_string ( ) ) ;
162
+ let extra_context = & fix_prompt. map ( |p| p. extra_context . clone ( ) ) . flatten ( ) ;
163
+
164
+ let prompt = {
165
+ let base_prompt = inquire:: Confirm :: new ( & prompt_text) . with_default ( false ) ;
166
+ match extra_context {
167
+ Some ( help_text) => base_prompt. with_help_message ( help_text) ,
168
+ None => base_prompt,
169
+ }
170
+ } ;
171
+
172
+ if prompt. prompt ( ) . unwrap ( ) {
173
+ let outputs = run_fix ( working_dir, & exec_path, fix) . await ?;
174
+ // failure indicates an issue with us actually executing it,
175
+ // not the success/failure of the command itself.
176
+ let max_exit_code = outputs
177
+ . iter ( )
178
+ . map ( |c| c. exit_code . unwrap_or ( -1 ) )
179
+ . max ( )
180
+ . unwrap ( ) ;
181
+
182
+ match max_exit_code {
183
+ 0 => Ok ( AnalyzeStatus :: KnownErrorFoundFixSucceeded ) ,
184
+ _ => {
185
+ if let Some ( help_text) = & fix. help_text {
186
+ error ! ( target: "user" , "Fix Help: {}" , help_text) ;
187
+ }
188
+ if let Some ( help_url) = & fix. help_url {
189
+ error ! ( target: "user" , "For more help, please visit {}" , help_url) ;
190
+ }
191
+
192
+ Ok ( AnalyzeStatus :: KnownErrorFoundFixFailed )
193
+ }
194
+ }
195
+ } else {
196
+ Ok ( AnalyzeStatus :: KnownErrorFoundUserDenied )
197
+ }
198
+ }
199
+
200
+ async fn run_fix (
201
+ working_dir : & PathBuf ,
202
+ exec_path : & str ,
203
+ fix : & DoctorFix ,
204
+ ) -> Result < Vec < OutputCapture > > {
205
+ let exec_runner = DefaultExecutionProvider :: default ( ) ;
206
+
207
+ let commands = fix. command . as_ref ( ) . expect ( "Expected a command" ) ;
208
+
209
+ let mut outputs = Vec :: < OutputCapture > :: new ( ) ;
210
+ for cmd in commands. expand ( ) {
211
+ let capture_opts = CaptureOpts {
212
+ working_dir,
213
+ args : & [ cmd] ,
214
+ output_dest : OutputDestination :: StandardOutWithPrefix ( "fixing" . to_string ( ) ) ,
215
+ path : exec_path,
216
+ env_vars : generate_env_vars ( ) ,
217
+ } ;
218
+ let output = exec_runner. run_command ( capture_opts) . await ?;
219
+ let exit_code = output. exit_code . expect ( "Expected an exit code" ) ;
220
+ outputs. push ( output) ;
221
+ if exit_code != 0 {
222
+ break ;
223
+ }
224
+ }
225
+
226
+ Ok ( outputs)
131
227
}
132
228
133
229
async fn read_from_command (
@@ -154,3 +250,23 @@ async fn read_from_file(file_name: &str) -> Result<BufReader<File>, AnalyzeError
154
250
}
155
251
Ok ( BufReader :: new ( File :: open ( file_path) . await ?) )
156
252
}
253
+
254
+ #[ derive( Copy , Clone ) ]
255
+ enum AnalyzeStatus {
256
+ NoKnownErrorsFound ,
257
+ KnownErrorFoundNoFixFound ,
258
+ KnownErrorFoundUserDenied ,
259
+ KnownErrorFoundFixFailed ,
260
+ KnownErrorFoundFixSucceeded ,
261
+ }
262
+
263
+ impl AnalyzeStatus {
264
+ fn to_exit_code ( self ) -> i32 {
265
+ match self {
266
+ // we need this to return a success code
267
+ AnalyzeStatus :: KnownErrorFoundFixSucceeded => 0 ,
268
+ // all others can return their discriminant value
269
+ status => status as i32 ,
270
+ }
271
+ }
272
+ }
0 commit comments