1- use std:: num :: NonZeroUsize ;
1+ use std:: process :: ExitCode ;
22use std:: sync:: Mutex ;
33
44use clap:: Parser ;
5+ use colored:: Colorize ;
56use crossbeam:: channel as crossbeam_channel;
6- use red_knot_workspace:: site_packages:: site_packages_dirs_of_venv;
77
8+ use red_knot_server:: run_server;
89use red_knot_workspace:: db:: RootDatabase ;
10+ use red_knot_workspace:: site_packages:: site_packages_dirs_of_venv;
911use red_knot_workspace:: watch;
1012use red_knot_workspace:: watch:: WorkspaceWatcher ;
1113use red_knot_workspace:: workspace:: WorkspaceMetadata ;
@@ -83,13 +85,34 @@ pub enum Command {
8385 Server ,
8486}
8587
86- #[ allow(
87- clippy:: print_stdout,
88- clippy:: unnecessary_wraps,
89- clippy:: print_stderr,
90- clippy:: dbg_macro
91- ) ]
92- pub fn main ( ) -> anyhow:: Result < ( ) > {
88+ #[ allow( clippy:: print_stdout, clippy:: unnecessary_wraps, clippy:: print_stderr) ]
89+ pub fn main ( ) -> ExitCode {
90+ match run ( ) {
91+ Ok ( status) => status. into ( ) ,
92+ Err ( error) => {
93+ {
94+ use std:: io:: Write ;
95+
96+ // Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken.
97+ let mut stderr = std:: io:: stderr ( ) . lock ( ) ;
98+
99+ // This communicates that this isn't a linter error but ruff itself hard-errored for
100+ // some reason (e.g. failed to resolve the configuration)
101+ writeln ! ( stderr, "{}" , "ruff failed" . red( ) . bold( ) ) . ok ( ) ;
102+ // Currently we generally only see one error, but e.g. with io errors when resolving
103+ // the configuration it is help to chain errors ("resolving configuration failed" ->
104+ // "failed to read file: subdir/pyproject.toml")
105+ for cause in error. chain ( ) {
106+ writeln ! ( stderr, " {} {cause}" , "Cause:" . bold( ) ) . ok ( ) ;
107+ }
108+ }
109+
110+ ExitStatus :: Error . into ( )
111+ }
112+ }
113+ }
114+
115+ fn run ( ) -> anyhow:: Result < ExitStatus > {
93116 let Args {
94117 command,
95118 current_directory,
@@ -101,20 +124,12 @@ pub fn main() -> anyhow::Result<()> {
101124 watch,
102125 } = Args :: parse_from ( std:: env:: args ( ) . collect :: < Vec < _ > > ( ) ) ;
103126
104- let verbosity = verbosity. level ( ) ;
105- countme:: enable ( verbosity. is_trace ( ) ) ;
106-
107127 if matches ! ( command, Some ( Command :: Server ) ) {
108- let four = NonZeroUsize :: new ( 4 ) . unwrap ( ) ;
109-
110- // by default, we set the number of worker threads to `num_cpus`, with a maximum of 4.
111- let worker_threads = std:: thread:: available_parallelism ( )
112- . unwrap_or ( four)
113- . max ( four) ;
114-
115- return red_knot_server:: Server :: new ( worker_threads) ?. run ( ) ;
128+ return run_server ( ) . map ( |( ) | ExitStatus :: Success ) ;
116129 }
117130
131+ let verbosity = verbosity. level ( ) ;
132+ countme:: enable ( verbosity. is_trace ( ) ) ;
118133 let _guard = setup_tracing ( verbosity) ?;
119134
120135 let cwd = if let Some ( cwd) = current_directory {
@@ -167,17 +182,35 @@ pub fn main() -> anyhow::Result<()> {
167182 }
168183 } ) ?;
169184
170- if watch {
171- main_loop. watch ( & mut db) ?;
185+ let exit_status = if watch {
186+ main_loop. watch ( & mut db) ?
172187 } else {
173- main_loop. run ( & mut db) ;
188+ main_loop. run ( & mut db)
174189 } ;
175190
176- tracing:: trace!( "Counts for entire CLI run :\n {}" , countme:: get_all( ) ) ;
191+ tracing:: trace!( "Counts for entire CLI run:\n {}" , countme:: get_all( ) ) ;
177192
178193 std:: mem:: forget ( db) ;
179194
180- Ok ( ( ) )
195+ Ok ( exit_status)
196+ }
197+
198+ #[ derive( Copy , Clone ) ]
199+ pub enum ExitStatus {
200+ /// Checking was successful and there were no errors.
201+ Success = 0 ,
202+
203+ /// Checking was successful but there were errors.
204+ Failure = 1 ,
205+
206+ /// Checking failed.
207+ Error = 2 ,
208+ }
209+
210+ impl From < ExitStatus > for ExitCode {
211+ fn from ( status : ExitStatus ) -> Self {
212+ ExitCode :: from ( status as u8 )
213+ }
181214}
182215
183216struct MainLoop {
@@ -205,7 +238,7 @@ impl MainLoop {
205238 )
206239 }
207240
208- fn watch ( mut self , db : & mut RootDatabase ) -> anyhow:: Result < ( ) > {
241+ fn watch ( mut self , db : & mut RootDatabase ) -> anyhow:: Result < ExitStatus > {
209242 tracing:: debug!( "Starting watch mode" ) ;
210243 let sender = self . sender . clone ( ) ;
211244 let watcher = watch:: directory_watcher ( move |event| {
@@ -216,19 +249,21 @@ impl MainLoop {
216249
217250 self . run ( db) ;
218251
219- Ok ( ( ) )
252+ Ok ( ExitStatus :: Success )
220253 }
221254
222- fn run ( mut self , db : & mut RootDatabase ) {
255+ fn run ( mut self , db : & mut RootDatabase ) -> ExitStatus {
223256 self . sender . send ( MainLoopMessage :: CheckWorkspace ) . unwrap ( ) ;
224257
225- self . main_loop ( db) ;
258+ let result = self . main_loop ( db) ;
226259
227260 tracing:: debug!( "Exiting main loop" ) ;
261+
262+ result
228263 }
229264
230265 #[ allow( clippy:: print_stderr) ]
231- fn main_loop ( & mut self , db : & mut RootDatabase ) {
266+ fn main_loop ( & mut self , db : & mut RootDatabase ) -> ExitStatus {
232267 // Schedule the first check.
233268 tracing:: debug!( "Starting main loop" ) ;
234269
@@ -263,7 +298,11 @@ impl MainLoop {
263298 }
264299
265300 if self . watcher . is_none ( ) {
266- return ;
301+ return if result. is_empty ( ) {
302+ ExitStatus :: Success
303+ } else {
304+ ExitStatus :: Failure
305+ } ;
267306 }
268307
269308 tracing:: trace!( "Counts after last check:\n {}" , countme:: get_all( ) ) ;
@@ -279,12 +318,14 @@ impl MainLoop {
279318 self . sender . send ( MainLoopMessage :: CheckWorkspace ) . unwrap ( ) ;
280319 }
281320 MainLoopMessage :: Exit => {
282- return ;
321+ return ExitStatus :: Success ;
283322 }
284323 }
285324
286325 tracing:: debug!( "Waiting for next main loop message." ) ;
287326 }
327+
328+ ExitStatus :: Success
288329 }
289330}
290331
0 commit comments