diff --git a/src/bin/scheduler/build.rs b/src/bin/scheduler/build.rs index c8d7f74d..ac0c83cf 100644 --- a/src/bin/scheduler/build.rs +++ b/src/bin/scheduler/build.rs @@ -62,11 +62,12 @@ fn build_environment( build_stage_reporter.update(id, EnvironmentBuildStage::Complete(outcome.clone()))?; return Ok(outcome); }; + let base_path = &working_directory.join(session.id()).join(id); info!("Building environment for plan {id}"); let run_spec = RunSpec { id: &format!("robotmk_env_building_{id}"), command_spec: &build_instructions.command_spec, - base_path: &working_directory.join(id), + base_path, timeout: build_instructions.timeout, cancellation_token, }; diff --git a/src/bin/scheduler/setup/general.rs b/src/bin/scheduler/setup/general.rs index 28b7b968..339f4931 100644 --- a/src/bin/scheduler/setup/general.rs +++ b/src/bin/scheduler/setup/general.rs @@ -1,82 +1,167 @@ -use super::{failed_plan_ids_human_readable, grant_permissions_to_all_plan_users}; +use super::rcc::rcc_setup_working_directory; +use super::{grant_full_access, plans_by_sessions}; use crate::build::environment_building_working_directory; use crate::internal_config::{sort_plans_by_grouping, GlobalConfig, Plan}; -use anyhow::{Context, Result as AnyhowResult}; + +use anyhow::{anyhow, Context, Result as AnyhowResult}; use camino::{Utf8Path, Utf8PathBuf}; -use log::{debug, error}; +use log::info; +use robotmk::environment::Environment; use robotmk::results::{plan_results_directory, GeneralSetupFailures}; use robotmk::section::WriteSection; +use robotmk::session::Session; use std::collections::{HashMap, HashSet}; use std::fs::{create_dir_all, remove_dir_all, remove_file}; pub fn setup(global_config: &GlobalConfig, plans: Vec) -> AnyhowResult> { - setup_working_directories(&global_config.working_directory, &plans)?; + if global_config.working_directory.exists() { + remove_dir_all(&global_config.working_directory) + .context("Failed to remove working directory")?; + } + create_dir_all(&global_config.working_directory) + .context("Failed to create working directory")?; setup_results_directories(global_config, &plans)?; - let mut surviving_plans: Vec; - let mut general_setup_failures = GeneralSetupFailures::default(); - ( - surviving_plans, - general_setup_failures.working_directory_permissions, - ) = adjust_working_directory_permissions(global_config, plans); + let mut surviving_plans: Vec = setup_working_directories(global_config, plans)?; + sort_plans_by_grouping(&mut surviving_plans); + Ok(surviving_plans) +} - general_setup_failures.write( +fn setup_working_directories( + global_config: &GlobalConfig, + plans: Vec, +) -> AnyhowResult> { + let (surviving_plans, plan_failures) = setup_plans_working_directory(plans); + let (surviving_plans, rcc_failures) = + setup_rcc_working_directories(&global_config.working_directory, surviving_plans); + GeneralSetupFailures { + working_directory_permissions: plan_failures + .into_iter() + .chain(rcc_failures.into_iter()) + .collect(), + } + .write( global_config .results_directory .join("general_setup_failures.json"), &global_config.results_directory_locker, )?; - - sort_plans_by_grouping(&mut surviving_plans); Ok(surviving_plans) } -fn setup_working_directories(working_directory: &Utf8Path, plans: &[Plan]) -> AnyhowResult<()> { - if working_directory.exists() { - remove_dir_all(working_directory).context("Failed to remove working directory")?; - } - create_dir_all(working_directory).context("Failed to create working directory")?; - for plan in plans { - create_dir_all(&plan.working_directory).context(format!( - "Failed to create working directory {} of plan {}", - plan.working_directory, plan.id - ))?; +fn setup_plans_working_directory(plans: Vec) -> (Vec, HashMap) { + let mut surviving_plans = Vec::new(); + let mut failures = HashMap::new(); + for plan in plans.into_iter() { + if let Err(e) = create_dir_all(&plan.working_directory) { + let error = anyhow!(e).context(format!( + "Failed to create working directory {} of plan {}", + plan.working_directory, plan.id + )); + info!("{error:#}"); + failures.insert(plan.id.clone(), format!("{error:#}")); + continue; + } + if let Session::User(user_session) = &plan.session { + info!( + "Granting full access for {} to user `{}`.", + &plan.working_directory, &user_session.user_name + ); + if let Err(e) = grant_full_access(&user_session.user_name, &plan.working_directory) { + let error = anyhow!(e).context(format!( + "Failed to set permissions for working directory {} of plan {}", + plan.working_directory, plan.id + )); + info!("{error:#}"); + failures.insert(plan.id.clone(), format!("{error:#}")); + continue; + }; + } + surviving_plans.push(plan); } - create_dir_all(environment_building_working_directory(working_directory)) - .context("Failed to create environment building working directory") + (surviving_plans, failures) } -fn setup_results_directories(global_config: &GlobalConfig, plans: &[Plan]) -> AnyhowResult<()> { - create_dir_all(&global_config.results_directory) - .context("Failed to create results directory")?; - create_dir_all(plan_results_directory(&global_config.results_directory)) - .context("Failed to create plan results directory")?; - clean_up_results_directory(global_config, plans).context("Failed to clean up results directory") -} - -fn adjust_working_directory_permissions( - global_config: &GlobalConfig, +fn setup_rcc_working_directories( + working_directory: &Utf8Path, plans: Vec, ) -> (Vec, HashMap) { - debug!( - "Granting all plan users full access to {}", - global_config.working_directory + let (rcc_plans, system_plans): (Vec, Vec) = plans + .into_iter() + .partition(|plan| matches!(plan.environment, Environment::Rcc(_))); + let (surviving_plans, environment_failures) = setup_with_one_directory_per_user( + &environment_building_working_directory(working_directory), + rcc_plans, ); - let (surviving_plans, failures_by_plan_id) = grant_permissions_to_all_plan_users( - &global_config.working_directory, - plans, - "(OI)(CI)F", - &["/T"], + let (mut surviving_plans, rcc_setup_failures) = setup_with_one_directory_per_user( + &rcc_setup_working_directory(working_directory), + surviving_plans, ); + surviving_plans.extend(system_plans); + ( + surviving_plans, + environment_failures + .into_iter() + .chain(rcc_setup_failures) + .collect(), + ) +} - if !failures_by_plan_id.is_empty() { - error!( - "Dropping the following plans due to failure to adjust working directory permissions: {}", - failed_plan_ids_human_readable(failures_by_plan_id.keys()) - ); +fn setup_with_one_directory_per_user( + target: &Utf8Path, + plans: Vec, +) -> (Vec, HashMap) { + let mut surviving_plans = Vec::new(); + let mut failures = HashMap::new(); + if let Err(e) = create_dir_all(target) { + let error = anyhow!(e).context(format!("Failed to create directory {target}",)); + info!("{error:#}"); + for plan in plans { + failures.insert(plan.id.clone(), format!("{error:#}")); + } + return (surviving_plans, failures); } + for (session, plans_in_session) in plans_by_sessions(plans) { + let user_target = &target.join(&session.id()); + if let Err(e) = create_dir_all(user_target) { + let error = anyhow!(e).context(format!( + "Failed to create directory {} for session {}", + user_target, &session + )); + info!("{error:#}"); + for plan in plans_in_session { + failures.insert(plan.id.clone(), format!("{error:#}")); + } + continue; + } + if let Session::User(user_session) = &session { + info!( + "Granting full access for {} to user `{}`.", + user_target, &user_session.user_name + ); + if let Err(e) = grant_full_access(&user_session.user_name, user_target) { + let error = anyhow!(e).context(format!( + "Failed to grant full access for {} to user `{}`.", + user_target, &user_session.user_name + )); + info!("{error:#}"); + for plan in plans_in_session { + failures.insert(plan.id.clone(), format!("{error:#}")); + } + continue; + }; + } + surviving_plans.extend(plans_in_session); + } + (surviving_plans, failures) +} - (surviving_plans, failures_by_plan_id) +fn setup_results_directories(global_config: &GlobalConfig, plans: &[Plan]) -> AnyhowResult<()> { + create_dir_all(&global_config.results_directory) + .context("Failed to create results directory")?; + create_dir_all(plan_results_directory(&global_config.results_directory)) + .context("Failed to create plan results directory")?; + clean_up_results_directory(global_config, plans).context("Failed to clean up results directory") } fn clean_up_results_directory(global_config: &GlobalConfig, plans: &[Plan]) -> AnyhowResult<()> { diff --git a/src/bin/scheduler/setup/mod.rs b/src/bin/scheduler/setup/mod.rs index 40c6fe9c..ce82ce7d 100644 --- a/src/bin/scheduler/setup/mod.rs +++ b/src/bin/scheduler/setup/mod.rs @@ -10,6 +10,19 @@ use icacls::run_icacls_command; use robotmk::session::Session; use std::collections::HashMap; +fn grant_full_access(user: &str, target_path: &Utf8Path) -> anyhow::Result<()> { + let arguments = [ + target_path.as_ref(), + "/grant", + &format!("{user}:(OI)(CI)F"), + "/T", + ]; + run_icacls_command(arguments).map_err(|e| { + let message = format!("Adjusting permissions of {target_path} for user `{user}` failed"); + e.context(message) + }) +} + fn plans_by_sessions(plans: Vec) -> HashMap> { let mut plans_by_session = HashMap::new(); for plan in plans { diff --git a/src/bin/scheduler/setup/rcc.rs b/src/bin/scheduler/setup/rcc.rs index 3d254485..9b7eddcf 100644 --- a/src/bin/scheduler/setup/rcc.rs +++ b/src/bin/scheduler/setup/rcc.rs @@ -17,7 +17,6 @@ use robotmk::{ section::WriteSection, }; use std::collections::HashMap; -use std::fs::{create_dir_all, remove_dir_all}; use std::vec; pub fn setup(global_config: &GlobalConfig, plans: Vec) -> AnyhowResult> { @@ -30,10 +29,6 @@ pub fn setup(global_config: &GlobalConfig, plans: Vec) -> AnyhowResult) -> AnyhowResult AnyhowResult<()> { - if working_directory.exists() { - remove_dir_all(working_directory).context(format!( - "Failed to remove working directory for RCC setup: {working_directory}" - ))?; - } - create_dir_all(working_directory).context(format!( - "Failed to create working directory for RCC setup: {working_directory}" - )) -} - -fn rcc_setup_working_directory(working_directory: &Utf8Path) -> Utf8PathBuf { +pub fn rcc_setup_working_directory(working_directory: &Utf8Path) -> Utf8PathBuf { working_directory.join("rcc_setup") } @@ -274,18 +258,13 @@ fn holotree_disable_sharing( for (session, plans) in plans_by_sessions(plans) { debug!("Running {} for `{}`", command_spec, &session); - let session_id = &format!( - "holotree_disabling_sharing_{}", - match &session { - Session::Current(_) => "current_user".into(), - Session::User(user_session) => format!("user_{}", user_session.user_name), - } - ); + let name = "holotree_disabling_sharing"; let run_spec = &RunSpec { - id: &format!("robotmk_{session_id}"), + id: &format!("robotmk_{name}_{}", session.id()), command_spec: &command_spec, base_path: &rcc_setup_working_directory(&global_config.working_directory) - .join(session_id), + .join(session.id()) + .join(name), timeout: 120, cancellation_token: &global_config.cancellation_token, }; @@ -344,13 +323,17 @@ fn run_command_spec_once_in_current_session( command_spec: &CommandSpec, id: &str, ) -> Result<(Vec, HashMap), Cancelled> { + let session = Session::Current(CurrentSession {}); + let base_path = &rcc_setup_working_directory(&global_config.working_directory) + .join(session.id()) + .join(id); Ok( match execute_run_spec_in_session( - &Session::Current(CurrentSession {}), + &session, &RunSpec { id: &format!("robotmk_{id}"), command_spec, - base_path: &rcc_setup_working_directory(&global_config.working_directory).join(id), + base_path, timeout: 120, cancellation_token: &global_config.cancellation_token, }, @@ -374,23 +357,16 @@ fn run_command_spec_per_session( let mut failed_plans: HashMap = HashMap::new(); for (session, plans) in plans_by_sessions(plans) { - let session_id = format!( - "{}_{}", - id, - match &session { - Session::Current(_) => "current_user".into(), - Session::User(user_session) => format!("user_{}", user_session.user_name), - } - ); - + let base_path = &rcc_setup_working_directory(&global_config.working_directory) + .join(session.id()) + .join(id); debug!("Running {} for `{}`", command_spec, &session); match execute_run_spec_in_session( &session, &RunSpec { - id: &format!("robotmk_{session_id}"), + id: &format!("robotmk_{id}"), command_spec, - base_path: &rcc_setup_working_directory(&global_config.working_directory) - .join(session_id), + base_path, timeout: 120, cancellation_token: &global_config.cancellation_token, }, diff --git a/src/session.rs b/src/session.rs index 8d0bf41f..6faec411 100644 --- a/src/session.rs +++ b/src/session.rs @@ -31,6 +31,13 @@ impl Session { Self::User(user_session) => user_session.run(spec), } } + + pub fn id(&self) -> String { + match self { + Self::Current(session) => session.id(), + Self::User(session) => session.id(), + } + } } impl Display for Session { @@ -92,6 +99,10 @@ impl CurrentSession { Outcome::Cancel => Ok(Outcome::Cancel), } } + + pub fn id(&self) -> String { + "current_user".into() + } } impl UserSession { @@ -105,6 +116,10 @@ impl UserSession { cancellation_token: spec.cancellation_token, }) } + + pub fn id(&self) -> String { + format!("user_{}", self.user_name) + } } #[cfg(test)] diff --git a/tests/test_scheduler.rs b/tests/test_scheduler.rs index c0798dc8..f5bf8a92 100644 --- a/tests/test_scheduler.rs +++ b/tests/test_scheduler.rs @@ -203,58 +203,71 @@ async fn assert_working_directory( working_directory: &Utf8Path, headed_user_name: &str, ) -> AnyhowResult<()> { - assert_permissions( - &working_directory, - &format!("{headed_user_name}:(OI)(CI)(F)"), - ) - .await?; assert!(working_directory.is_dir()); assert_eq!( directory_entries(working_directory, 1), ["environment_building", "plans", "rcc_setup"] ); assert_eq!( - directory_entries(working_directory.join("rcc_setup"), 1), + directory_entries(working_directory.join("rcc_setup"), 2), [ - "custom_profile_import_current_user.stderr", - "custom_profile_import_current_user.stdout", - &format!("custom_profile_import_user_{headed_user_name}.bat"), - &format!("custom_profile_import_user_{headed_user_name}.exit_code"), - &format!("custom_profile_import_user_{headed_user_name}.stderr"), - &format!("custom_profile_import_user_{headed_user_name}.stdout"), - "custom_profile_switch_current_user.stderr", - "custom_profile_switch_current_user.stdout", - &format!("custom_profile_switch_user_{headed_user_name}.bat"), - &format!("custom_profile_switch_user_{headed_user_name}.exit_code"), - &format!("custom_profile_switch_user_{headed_user_name}.stderr"), - &format!("custom_profile_switch_user_{headed_user_name}.stdout"), - "holotree_disabling_sharing_current_user.stderr", - "holotree_disabling_sharing_current_user.stdout", - &format!("holotree_disabling_sharing_user_{headed_user_name}.bat"), - &format!("holotree_disabling_sharing_user_{headed_user_name}.exit_code"), - &format!("holotree_disabling_sharing_user_{headed_user_name}.stderr"), - &format!("holotree_disabling_sharing_user_{headed_user_name}.stdout"), - "long_path_support_enabling.stderr", - "long_path_support_enabling.stdout", - "telemetry_disabling_current_user.stderr", - "telemetry_disabling_current_user.stdout", - &format!("telemetry_disabling_user_{headed_user_name}.bat"), - &format!("telemetry_disabling_user_{headed_user_name}.exit_code"), - &format!("telemetry_disabling_user_{headed_user_name}.stderr"), - &format!("telemetry_disabling_user_{headed_user_name}.stdout") - ] + "current_user", + "current_user\\custom_profile_import.stderr", + "current_user\\custom_profile_import.stdout", + "current_user\\custom_profile_switch.stderr", + "current_user\\custom_profile_switch.stdout", + "current_user\\holotree_disabling_sharing.stderr", + "current_user\\holotree_disabling_sharing.stdout", + "current_user\\long_path_support_enabling.stderr", + "current_user\\long_path_support_enabling.stdout", + "current_user\\telemetry_disabling.stderr", + "current_user\\telemetry_disabling.stdout", + &format!("user_{headed_user_name}"), + &format!("user_{headed_user_name}\\custom_profile_import.bat"), + &format!("user_{headed_user_name}\\custom_profile_import.exit_code"), + &format!("user_{headed_user_name}\\custom_profile_import.stderr"), + &format!("user_{headed_user_name}\\custom_profile_import.stdout"), + &format!("user_{headed_user_name}\\custom_profile_switch.bat"), + &format!("user_{headed_user_name}\\custom_profile_switch.exit_code"), + &format!("user_{headed_user_name}\\custom_profile_switch.stderr"), + &format!("user_{headed_user_name}\\custom_profile_switch.stdout"), + &format!("user_{headed_user_name}\\holotree_disabling_sharing.bat"), + &format!("user_{headed_user_name}\\holotree_disabling_sharing.exit_code"), + &format!("user_{headed_user_name}\\holotree_disabling_sharing.stderr"), + &format!("user_{headed_user_name}\\holotree_disabling_sharing.stdout"), + &format!("user_{headed_user_name}\\telemetry_disabling.bat"), + &format!("user_{headed_user_name}\\telemetry_disabling.exit_code"), + &format!("user_{headed_user_name}\\telemetry_disabling.stderr"), + &format!("user_{headed_user_name}\\telemetry_disabling.stdout"), + ], ); + assert_permissions( + working_directory + .join("rcc_setup") + .join(&format!("user_{headed_user_name}")), + &format!("{headed_user_name}:(OI)(CI)(F)"), + ) + .await?; assert_eq!( - directory_entries(working_directory.join("environment_building"), 1), + directory_entries(working_directory.join("environment_building"), 2), [ - "rcc_headed.bat", - "rcc_headed.exit_code", - "rcc_headed.stderr", - "rcc_headed.stdout", - "rcc_headless.stderr", - "rcc_headless.stdout" + "current_user", + "current_user\\rcc_headless.stderr", + "current_user\\rcc_headless.stdout", + &format!("user_{headed_user_name}"), + &format!("user_{headed_user_name}\\rcc_headed.bat"), + &format!("user_{headed_user_name}\\rcc_headed.exit_code"), + &format!("user_{headed_user_name}\\rcc_headed.stderr"), + &format!("user_{headed_user_name}\\rcc_headed.stdout"), ] ); + assert_permissions( + working_directory + .join("environment_building") + .join(&format!("user_{headed_user_name}")), + &format!("{headed_user_name}:(OI)(CI)(F)"), + ) + .await?; assert_eq!( directory_entries(working_directory.join("plans"), 1), ["no_rcc", "rcc_headed", "rcc_headless"]