@@ -579,20 +579,28 @@ pub fn handle_configure(context: Arc<Context>, id: u32, params: Value) {
579579 ) ;
580580 }
581581
582- apply_configure_options (
582+ if let Err ( message ) = apply_configure_options (
583583 context. configuration . as_ref ( ) ,
584584 & context. locators ,
585585 configure_options,
586586 workspace_directories,
587587 environment_directories,
588- ) ;
588+ ) {
589+ error ! ( "Configure failed: {message}" ) ;
590+ send_error ( Some ( id) , -4 , message) ;
591+ return ;
592+ }
589593 info ! ( "Configure completed in {:?}" , now. elapsed( ) ) ;
590594 send_reply ( id, None :: < ( ) > ) ;
591595 } ) ;
592596 }
593597 Err ( e) => {
594- send_reply ( id, None :: < u128 > ) ;
595598 error ! ( "Failed to parse configure options {:?}: {}" , params, e) ;
599+ send_error (
600+ Some ( id) ,
601+ -4 ,
602+ format ! ( "Failed to parse configure options {params:?}: {e}" ) ,
603+ ) ;
596604 }
597605 }
598606}
@@ -655,31 +663,95 @@ fn apply_configure_options(
655663 configure_options : ConfigureOptions ,
656664 workspace_directories : Option < Vec < PathBuf > > ,
657665 environment_directories : Option < Vec < PathBuf > > ,
658- ) {
666+ ) -> Result < ( ) , String > {
659667 let mut state = configuration. write ( ) . unwrap ( ) ;
660- state. config . workspace_directories = workspace_directories;
661- state. config . conda_executable = configure_options. conda_executable ;
662- state. config . environment_directories = environment_directories;
663- state. config . pipenv_executable = configure_options. pipenv_executable ;
664- state. config . poetry_executable = configure_options. poetry_executable ;
668+ let previous_config = state. config . clone ( ) ;
669+ let mut next_config = state. config . clone ( ) ;
670+ let next_generation = state. generation + 1 ;
671+
672+ next_config. workspace_directories = workspace_directories;
673+ next_config. conda_executable = configure_options. conda_executable ;
674+ next_config. environment_directories = environment_directories;
675+ next_config. pipenv_executable = configure_options. pipenv_executable ;
676+ next_config. poetry_executable = configure_options. poetry_executable ;
665677 // We will not support changing the cache directories once set.
666678 // No point, supporting such a use case.
667- if let Some ( cache_directory) = configure_options. cache_directory {
668- set_cache_directory ( cache_directory. clone ( ) ) ;
669- state . config . cache_directory = Some ( cache_directory) ;
679+ let cache_directory = configure_options. cache_directory ;
680+ if let Some ( cache_directory ) = cache_directory. clone ( ) {
681+ next_config . cache_directory = Some ( cache_directory) ;
670682 }
671- state. generation += 1 ;
683+
684+ trace ! (
685+ "Configuring locators with generation {}: {:?}" ,
686+ next_generation,
687+ next_config
688+ ) ;
689+
690+ if let Err ( panic_payload) = panic:: catch_unwind ( AssertUnwindSafe ( || {
691+ configure_locators ( locators, & next_config) ;
692+ } ) ) {
693+ let panic_message = panic_payload_message ( panic_payload. as_ref ( ) ) ;
694+ error ! (
695+ "Locator configuration panicked for generation {}: {}" ,
696+ next_generation, panic_message
697+ ) ;
698+ rollback_locator_config ( locators, & previous_config, next_generation) ;
699+ return Err ( format ! (
700+ "Locator configuration failed for generation {next_generation}: {panic_message}"
701+ ) ) ;
702+ }
703+
704+ if let Some ( cache_directory) = cache_directory {
705+ if let Err ( panic_payload) = panic:: catch_unwind ( AssertUnwindSafe ( || {
706+ set_cache_directory ( cache_directory) ;
707+ } ) ) {
708+ let panic_message = panic_payload_message ( panic_payload. as_ref ( ) ) ;
709+ error ! (
710+ "Cache directory configuration panicked for generation {}: {}" ,
711+ next_generation, panic_message
712+ ) ;
713+ rollback_locator_config ( locators, & previous_config, next_generation) ;
714+ return Err ( format ! (
715+ "Cache directory configuration failed for generation {next_generation}: {panic_message}"
716+ ) ) ;
717+ }
718+ }
719+ state. config = next_config;
720+ state. generation = next_generation;
672721 // Reset missing-env reporting so that the next refresh after
673722 // reconfiguration can trigger it again (Fixes #395). Done inside the write
674723 // lock to avoid a TOCTOU window with concurrent refresh threads reading the
675724 // generation.
676725 MISSING_ENVS_REPORTING_STATE . store ( MISSING_ENVS_AVAILABLE , Ordering :: Release ) ;
677- trace ! (
678- "Configuring locators with generation {}: {:?}" ,
679- state. generation,
680- state. config
681- ) ;
682- configure_locators ( locators, & state. config ) ;
726+
727+ Ok ( ( ) )
728+ }
729+
730+ fn rollback_locator_config (
731+ locators : & Arc < Vec < Arc < dyn Locator > > > ,
732+ previous_config : & Configuration ,
733+ failed_generation : u64 ,
734+ ) {
735+ if let Err ( panic_payload) = panic:: catch_unwind ( AssertUnwindSafe ( || {
736+ configure_locators ( locators, previous_config) ;
737+ } ) ) {
738+ error ! (
739+ "Rollback after failed locator configuration for generation {} also panicked: {}" ,
740+ failed_generation,
741+ panic_payload_message( panic_payload. as_ref( ) )
742+ ) ;
743+ }
744+ }
745+
746+ fn panic_payload_message ( panic_payload : & ( dyn std:: any:: Any + Send ) ) -> String {
747+ if let Some ( message) = panic_payload. downcast_ref :: < & str > ( ) {
748+ return ( * message) . to_string ( ) ;
749+ }
750+ if let Some ( message) = panic_payload. downcast_ref :: < String > ( ) {
751+ return message. clone ( ) ;
752+ }
753+
754+ "unknown panic payload" . to_string ( )
683755}
684756
685757fn configure_locators ( locators : & Arc < Vec < Arc < dyn Locator > > > , config : & Configuration ) {
@@ -1267,6 +1339,12 @@ mod tests {
12671339 configured_workspace_directories : Mutex < Option < Vec < PathBuf > > > ,
12681340 }
12691341
1342+ struct PanicConfigureLocator ;
1343+
1344+ struct RecordingConfigureLocator {
1345+ configured_workspace_directories : Mutex < Option < Vec < PathBuf > > > ,
1346+ }
1347+
12701348 impl Reporter for RecordingReporter {
12711349 fn report_manager ( & self , manager : & EnvManager ) {
12721350 self . managers . lock ( ) . unwrap ( ) . push ( manager. clone ( ) ) ;
@@ -1321,6 +1399,47 @@ mod tests {
13211399 fn find ( & self , _reporter : & dyn Reporter ) { }
13221400 }
13231401
1402+ impl Locator for PanicConfigureLocator {
1403+ fn get_kind ( & self ) -> LocatorKind {
1404+ LocatorKind :: Venv
1405+ }
1406+
1407+ fn configure ( & self , _config : & Configuration ) {
1408+ panic ! ( "configure boom" ) ;
1409+ }
1410+
1411+ fn supported_categories ( & self ) -> Vec < PythonEnvironmentKind > {
1412+ vec ! [ PythonEnvironmentKind :: Venv ]
1413+ }
1414+
1415+ fn try_from ( & self , _env : & pet_core:: env:: PythonEnv ) -> Option < PythonEnvironment > {
1416+ None
1417+ }
1418+
1419+ fn find ( & self , _reporter : & dyn Reporter ) { }
1420+ }
1421+
1422+ impl Locator for RecordingConfigureLocator {
1423+ fn get_kind ( & self ) -> LocatorKind {
1424+ LocatorKind :: Venv
1425+ }
1426+
1427+ fn configure ( & self , config : & Configuration ) {
1428+ * self . configured_workspace_directories . lock ( ) . unwrap ( ) =
1429+ config. workspace_directories . clone ( ) ;
1430+ }
1431+
1432+ fn supported_categories ( & self ) -> Vec < PythonEnvironmentKind > {
1433+ vec ! [ PythonEnvironmentKind :: Venv ]
1434+ }
1435+
1436+ fn try_from ( & self , _env : & pet_core:: env:: PythonEnv ) -> Option < PythonEnvironment > {
1437+ None
1438+ }
1439+
1440+ fn find ( & self , _reporter : & dyn Reporter ) { }
1441+ }
1442+
13241443 fn make_refresh_key ( generation : u64 , options : RefreshOptions ) -> RefreshKey {
13251444 RefreshKey :: new ( & options, generation)
13261445 }
@@ -1480,7 +1599,8 @@ mod tests {
14801599 } ,
14811600 Some ( workspace_directories) ,
14821601 None ,
1483- ) ;
1602+ )
1603+ . unwrap ( ) ;
14841604 done_tx. send ( ( ) ) . unwrap ( ) ;
14851605 } )
14861606 } ;
@@ -1504,6 +1624,68 @@ mod tests {
15041624 ) ;
15051625 }
15061626
1627+ #[ test]
1628+ fn test_configure_panic_does_not_publish_state_or_poison_lock ( ) {
1629+ let configuration = RwLock :: new ( ConfigurationState :: default ( ) ) ;
1630+ let locators = Arc :: new ( vec ! [ Arc :: new( PanicConfigureLocator ) as Arc <dyn Locator >] ) ;
1631+ let workspace_directories = vec ! [ PathBuf :: from( "/workspace" ) ] ;
1632+
1633+ let result = apply_configure_options (
1634+ & configuration,
1635+ & locators,
1636+ ConfigureOptions {
1637+ workspace_directories : None ,
1638+ conda_executable : Some ( PathBuf :: from ( "/configured/conda" ) ) ,
1639+ pipenv_executable : None ,
1640+ poetry_executable : None ,
1641+ environment_directories : None ,
1642+ cache_directory : None ,
1643+ } ,
1644+ Some ( workspace_directories) ,
1645+ None ,
1646+ ) ;
1647+
1648+ assert ! ( result. unwrap_err( ) . contains( "configure boom" ) ) ;
1649+ let state = configuration. read ( ) . unwrap ( ) ;
1650+ assert_eq ! ( state. generation, 0 ) ;
1651+ assert ! ( state. config. workspace_directories. is_none( ) ) ;
1652+ assert ! ( state. config. conda_executable. is_none( ) ) ;
1653+ }
1654+
1655+ #[ test]
1656+ fn test_configure_panic_rolls_back_previously_configured_locators ( ) {
1657+ let configuration = RwLock :: new ( ConfigurationState :: default ( ) ) ;
1658+ let recording_locator = Arc :: new ( RecordingConfigureLocator {
1659+ configured_workspace_directories : Mutex :: new ( None ) ,
1660+ } ) ;
1661+ let locators = Arc :: new ( vec ! [
1662+ recording_locator. clone( ) as Arc <dyn Locator >,
1663+ Arc :: new( PanicConfigureLocator ) as Arc <dyn Locator >,
1664+ ] ) ;
1665+
1666+ let result = apply_configure_options (
1667+ & configuration,
1668+ & locators,
1669+ ConfigureOptions {
1670+ workspace_directories : None ,
1671+ conda_executable : None ,
1672+ pipenv_executable : None ,
1673+ poetry_executable : None ,
1674+ environment_directories : None ,
1675+ cache_directory : None ,
1676+ } ,
1677+ Some ( vec ! [ PathBuf :: from( "/workspace" ) ] ) ,
1678+ None ,
1679+ ) ;
1680+
1681+ assert ! ( result. is_err( ) ) ;
1682+ assert ! ( recording_locator
1683+ . configured_workspace_directories
1684+ . lock( )
1685+ . unwrap( )
1686+ . is_none( ) ) ;
1687+ }
1688+
15071689 #[ test]
15081690 fn test_stale_generation_does_not_begin_missing_env_reporting ( ) {
15091691 let _guard = MISSING_ENVS_TEST_LOCK . lock ( ) . unwrap ( ) ;
0 commit comments