use super::*;
pub(super) struct ProcessCancellationGuard(pub(super) Arc<AtomicBool>);
impl Drop for ProcessCancellationGuard {
fn drop(&mut self) {
self.0.store(true, Ordering::Release);
}
}
#[cfg(test)]
pub(super) fn run_new_preflight(
config: Config,
bundle_id: String,
target_id: String,
project_directory: Option<PathBuf>,
) -> Result<crate::server::PreflightNew> {
run_new_preflight_with_cancellation(
config,
bundle_id,
target_id,
project_directory,
Arc::new(AtomicBool::new(false)),
Vec::new(),
)
}
pub(super) fn spawn_resume_preflight(
jobs: &mut tokio::task::JoinSet<()>,
controller: &Controller,
request: crate::server::ResumePreflightRequest,
termination: &tokio_util::sync::CancellationToken,
) {
let crate::server::ResumePreflightRequest {
session_id,
target_id,
mut reply,
} = request;
let config = controller.config.clone();
let session = controller.state.sessions.get(&session_id).cloned();
let termination = termination.clone();
jobs.spawn(async move {
let cancelled = Arc::new(AtomicBool::new(false));
let cancellation_guard = ProcessCancellationGuard(cancelled.clone());
let mut blocking = tokio::task::spawn_blocking(move || {
run_resume_preflight(config, session, &target_id, cancelled)
});
let answer = tokio::select! {
biased;
_ = termination.cancelled() => None,
_ = reply.closed() => None,
answer = &mut blocking => Some(answer),
};
let Some(answer) = answer else {
drop(cancellation_guard);
if let Err(error) = blocking.await {
tracing::warn!(%error, "cancelled phone resume preflight task failed");
}
return;
};
let answer = answer.map_err(|error| {
tracing::warn!(%error, "phone resume preflight task failed");
PreflightFailure::Controller(format!("resume preflight task failed: {error}"))
});
if reply.send(answer).is_err() {
tracing::debug!("phone resume preflight reply dropped after client disconnect");
}
});
}
pub(super) fn run_resume_preflight(
config: Config,
session: Option<mj_core::state::SessionRecord>,
target_id: &str,
cancelled: Arc<AtomicBool>,
) -> crate::server::PreflightResume {
let Some(session) = session else {
return crate::server::PreflightResume::Unavailable {
detail: "this session is no longer available".to_owned(),
};
};
match crate::controller::resume_compatibility(&session, &config, target_id) {
Err(reason) => crate::server::PreflightResume::Unavailable { detail: reason },
Ok(plan) if plan != crate::controller::ResumePlan::RawToWorkspace => {
crate::server::PreflightResume::Ready
}
Ok(_) => {
let executor =
CancellableProcessExecutor::new(cancelled).with_deadline(Duration::from_secs(30));
match crate::controller::raw_conversion_preview_for(&session, &config, &executor) {
Err(error) => crate::server::PreflightResume::Unavailable {
detail: format!("{error:#}"),
},
Ok(mut preview) => {
preview.fetch_url = display_url(&preview.fetch_url);
preview.push_urls = preview
.push_urls
.iter()
.map(|url| display_url(url))
.collect();
crate::server::PreflightResume::ConvertingRawCheckout {
preview: Box::new(preview),
}
}
}
}
}
}
pub(super) fn run_new_preflight_with_cancellation(
config: Config,
bundle_id: String,
target_id: String,
project_directory: Option<PathBuf>,
cancelled: Arc<AtomicBool>,
remote_repairs: Vec<mj_core::local_git::LocalRemoteRepair>,
) -> Result<crate::server::PreflightNew> {
let executor =
CancellableProcessExecutor::new(cancelled).with_deadline(Duration::from_secs(30));
if !remote_repairs.is_empty() {
let target = config.targets.get(&target_id).context("unknown target")?;
anyhow::ensure!(
!is_bare_project_target(target) && project_directory.is_none(),
"remote repair requires an isolated target"
);
let bundle = config.bundles.get(&bundle_id).context("unknown bundle")?;
mj_core::local_git::apply_repository_remote_repairs(bundle, &remote_repairs, &executor)?;
}
run_new_preflight_with_executor(config, bundle_id, target_id, project_directory, &executor)
}
pub(super) fn run_new_preflight_with_executor(
config: Config,
bundle_id: String,
target_id: String,
project_directory: Option<PathBuf>,
executor: &impl CommandExecutor,
) -> Result<crate::server::PreflightNew> {
let target_is_bare = config
.targets
.get(&target_id)
.with_context(|| format!("unknown target template {target_id:?}"))
.map(is_bare_project_target)?;
if target_is_bare {
let directory =
project_directory.context("project directory is required for a bare target")?;
let controller = config_only_controller(config);
let directory = controller.resolve_project_directory(&target_id, &directory, executor)?;
let managed_worktree =
controller.managed_worktree_options(&target_id, &directory, executor)?;
return Ok(crate::server::PreflightNew {
managed_worktree,
project_directory: Some(directory),
remote_repairs: Vec::new(),
dirty_repositories: Vec::new(),
remote_repositories: Vec::new(),
local_changes_excluded: false,
});
}
if project_directory.is_some() {
bail!("project directory is unsupported for this target");
}
let bundle = config.bundles.get(&bundle_id).context("unknown bundle")?;
let repairs = mj_core::local_git::repository_remote_repairs(bundle, executor)?;
if !repairs.is_empty() {
return Ok(crate::server::PreflightNew {
managed_worktree: Default::default(),
project_directory: None,
remote_repairs: repairs,
dirty_repositories: Vec::new(),
remote_repositories: Vec::new(),
local_changes_excluded: true,
});
}
let remote_repositories = bundle
.repositories
.iter()
.map(|repository| {
let source = resolve_repository(repository, executor)
.with_context(|| format!("repository {:?}", repository.id))?;
let default_branch = default_branch(&source, executor)
.with_context(|| format!("repository {:?}", repository.id))?;
Ok(crate::server::PreflightRepository {
id: repository.id.clone(),
fetch_url: display_url(&source.fetch_url),
default_branch,
push_urls: source
.push_urls
.iter()
.map(|url| display_url(url))
.collect(),
})
})
.collect::<Result<Vec<_>>>()?;
Ok(crate::server::PreflightNew {
managed_worktree: Default::default(),
project_directory: None,
remote_repairs: Vec::new(),
dirty_repositories: Vec::new(),
remote_repositories,
local_changes_excluded: true,
})
}