use std::sync::Arc;
use wsb::peer::compatibility::RepositoryFacts;
use wsb::peer::converge::{Baseline, EntryKind};
use wsb::peer::model::PeerId;
use wsb::peer::protocol::{
baseline_digest, RefusalCode, Request, Response, Write, WorkspaceSummary,
};
use wsb::peer::scan;
use wsb::peer::transport::Session;
use super::coordinator::LocalWorkspace;
use super::service::now;
use super::store::MachineStore;
pub trait Machine: Send + Sync {
fn workspaces(&self) -> Vec<WorkspaceSummary>;
fn workspace(&self, id: &wsb::peer::model::WorkspaceId) -> Result<Arc<LocalWorkspace>, String>;
fn store(&self) -> Arc<MachineStore>;
fn staging_root(&self) -> std::path::PathBuf;
fn plugins(&self) -> Arc<crate::plugin::PluginRegistry>;
}
pub fn serve(machine: &dyn Machine, session: &mut Session, peer: &PeerId) -> Result<(), String> {
loop {
let request: Request = match session.receive() {
Ok(request) => request,
Err(error) if is_disconnect(&error) => return Ok(()),
Err(error) => return Err(error),
};
let response = answer(machine, peer, request);
session.send(&response)?;
}
}
fn is_disconnect(error: &str) -> bool {
error.contains("cannot read from the peer")
}
pub fn answer(machine: &dyn Machine, peer: &PeerId, request: Request) -> Response {
match request {
Request::Inventory => Response::Inventory {
workspaces: machine.workspaces(),
},
Request::Report {
workspace,
repositories,
} => match machine.workspace(&workspace) {
Err(_) => Response::Report {
workspace_present: Some(false),
repositories: Vec::new(),
},
Ok(local) => Response::Report {
workspace_present: Some(true),
repositories: local.report(&repositories),
},
},
Request::Fetch {
workspace,
repository,
epoch,
paths,
} => {
match machine.store().desired(peer, &workspace) {
Ok(wsb::peer::model::Desired::Enabled) => {}
Ok(_) => {
return refuse(
RefusalCode::NotEnabled,
"continuity is not enabled for this workspace on this machine",
)
}
Err(error) => return refuse(RefusalCode::Internal, &error),
}
let local = match machine.workspace(&workspace) {
Ok(local) => local,
Err(why) => return refuse(RefusalCode::UnknownWorkspace, &why),
};
let Some(repo) = local.repository(&repository) else {
return refuse(RefusalCode::UnknownRepository, "this machine does not have that repository");
};
match RepositoryFacts::read(repository.clone(), &repo.workdir, &repo.git_dir) {
Ok(facts) if epoch.still_current(&facts) => {}
Ok(_) => return refuse(RefusalCode::EpochMoved, "the repository moved"),
Err(error) => return refuse(RefusalCode::Internal, &error),
}
let mut writes = Vec::new();
for path in paths {
let absolute = match scan::local_path(&repo.workdir, &path) {
Ok(absolute) => absolute,
Err(error) => return refuse(RefusalCode::ForbiddenPath, &error),
};
let entry = match wsb::peer::converge::Entry::read(&absolute) {
Ok(Some(entry)) => entry,
Ok(None) => continue,
Err(error) => return refuse(RefusalCode::Internal, &error),
};
let content = match entry.kind {
EntryKind::Symlink => match std::fs::read_link(&absolute) {
Ok(target) => {
wsb::peer::identity::base64_encode(scan::to_slash(&target).as_bytes())
}
Err(error) => return refuse(RefusalCode::Internal, &error.to_string()),
},
EntryKind::File => match std::fs::read(&absolute) {
Ok(bytes) => wsb::peer::identity::base64_encode(&bytes),
Err(error) => return refuse(RefusalCode::Internal, &error.to_string()),
},
};
writes.push(Write {
path,
entry,
content,
});
}
Response::Fetched { writes }
}
Request::Confirm {
workspace,
repository,
baseline_digest: asker_digest,
} => {
let local = match machine.workspace(&workspace) {
Ok(local) => local,
Err(why) => return refuse(RefusalCode::UnknownWorkspace, &why),
};
let baseline = match machine.store().baseline(peer, &workspace, &repository) {
Ok(baseline) => baseline,
Err(error) => return refuse(RefusalCode::Internal, &error),
};
if baseline_digest(&baseline) != asker_digest {
if let Err(error) = reseed_from_head(machine, peer, &local, &repository) {
tracing::warn!(
%peer,
repository = %repository,
%error,
"could not re-seed after a baseline mismatch; the peer is \
still told the baselines differ"
);
}
return refuse(
RefusalCode::BaselineMismatch,
"this machine holds a different baseline for that repository",
);
}
Response::Acknowledged
}
Request::Apply {
workspace,
repository,
epoch,
writes,
deletions,
found,
baseline_digest: sender_digest,
} => {
let store = machine.store();
match store.desired(peer, &workspace) {
Ok(wsb::peer::model::Desired::Enabled) => {}
Ok(_) => {
return refuse(
RefusalCode::NotEnabled,
"continuity is not enabled for this workspace on this machine",
)
}
Err(error) => return refuse(RefusalCode::Internal, &error),
}
let local = match machine.workspace(&workspace) {
Ok(local) => local,
Err(why) => return refuse(RefusalCode::UnknownWorkspace, &why),
};
let Some(repo) = local.repository(&repository) else {
return refuse(RefusalCode::UnknownRepository, "this machine does not have that repository");
};
let baseline = match store.baseline(peer, &workspace, &repository) {
Ok(baseline) => baseline,
Err(error) => return refuse(RefusalCode::Internal, &error),
};
if baseline_digest(&baseline) != sender_digest {
return refuse(
RefusalCode::BaselineMismatch,
"this machine holds a different baseline for that repository",
);
}
match super::transfer::apply(
&repo.workdir,
&repository,
&repo.git_dir,
&epoch,
&writes,
&deletions,
&found,
&machine.staging_root(),
) {
Ok(applied) => Response::Applied { applied },
Err(super::transfer::ApplyRefusal::EpochMoved) => {
refuse(RefusalCode::EpochMoved, "the repository moved")
}
Err(super::transfer::ApplyRefusal::Forbidden(detail)) => {
refuse(RefusalCode::ForbiddenPath, &detail)
}
Err(super::transfer::ApplyRefusal::Malformed(detail)) => {
refuse(RefusalCode::Malformed, &detail)
}
Err(super::transfer::ApplyRefusal::Internal(detail)) => {
refuse(RefusalCode::Internal, &detail)
}
}
}
Request::Acknowledge {
workspace,
repository,
epoch: _,
paths,
baseline_digest: sender_digest,
} => {
let store = machine.store();
let local = match machine.workspace(&workspace) {
Ok(local) => local,
Err(why) => return refuse(RefusalCode::UnknownWorkspace, &why),
};
let Some(repo) = local.repository(&repository) else {
return refuse(RefusalCode::UnknownRepository, "this machine does not have that repository");
};
let baseline = match store.baseline(peer, &workspace, &repository) {
Ok(baseline) => baseline,
Err(error) => return refuse(RefusalCode::Internal, &error),
};
if baseline_digest(&baseline) != sender_digest {
return refuse(
RefusalCode::BaselineMismatch,
"this machine holds a different baseline for that repository",
);
}
let mut next: Baseline = baseline;
for path in paths {
let absolute = match scan::local_path(&repo.workdir, &path) {
Ok(absolute) => absolute,
Err(error) => return refuse(RefusalCode::ForbiddenPath, &error),
};
match wsb::peer::converge::Entry::read(&absolute) {
Ok(Some(entry)) => {
next.insert(path, entry);
}
Ok(None) => {
next.remove(&path);
}
Err(error) => return refuse(RefusalCode::Internal, &error),
}
}
match store.put_baseline(peer, &workspace, &repository, &next) {
Ok(()) => {
let _ = store.note_seen(peer, now());
Response::Acknowledged
}
Err(error) => refuse(RefusalCode::Internal, &error),
}
}
Request::Plugin { service, body } => answer_plugin(machine, peer, service, body),
}
}
fn answer_plugin(
machine: &dyn Machine,
peer: &PeerId,
service: wsb::plugin::PluginService,
body: String,
) -> Response {
if body.len() > crate::plugin::encoded_bound() {
return refuse(
RefusalCode::Malformed,
&format!("a plugin request may carry at most {} bytes", wsb::plugin::MAX_BODY),
);
}
let store = machine.store();
let switch = match store.service(peer, wsb::peer::model::Service::Plugin(service.clone())) {
Ok(switch) => switch,
Err(error) => return refuse(RefusalCode::Internal, &error),
};
if !switch.is_on() {
return refuse(
RefusalCode::ServiceOff,
&format!("{service} is switched off with this machine on the answering side"),
);
}
let known = match store.peer(peer) {
Ok(Some(known)) => known,
Ok(None) => return refuse(RefusalCode::Internal, "the asking machine is not a paired peer here"),
Err(error) => return refuse(RefusalCode::Internal, &error),
};
let registration = match machine.plugins().lookup(&service) {
crate::plugin::Lookup::Offered(registration) => registration,
crate::plugin::Lookup::Gone => {
return refuse(
RefusalCode::PluginAbsent,
&format!("the {} plugin that offers {service} here has stopped", service.plugin()),
)
}
crate::plugin::Lookup::NotOffered => {
return refuse(
RefusalCode::NoSuchService,
&format!("no plugin here offers {service}"),
)
}
};
let call = wsb::plugin::Call {
service: service.clone(),
caller: wsb::plugin::Caller {
id: known.id.to_string(),
name: known.name.clone(),
fingerprint: known.fingerprint.as_str().to_string(),
},
body,
};
match crate::plugin::PluginRegistry::call(®istration, &call) {
Ok(wsb::plugin::Answer::Reply { body }) => Response::Plugin { body },
Ok(wsb::plugin::Answer::Refused { detail }) => refuse(RefusalCode::PluginRefused, &detail),
Err(failure) => {
tracing::warn!(%peer, %service, failure = %failure.0, "a plugin did not answer a peer's request");
refuse(RefusalCode::PluginAbsent, &failure.0)
}
}
}
fn reseed_from_head(
machine: &dyn Machine,
peer: &PeerId,
local: &LocalWorkspace,
repository: &wsb::peer::model::RepositoryId,
) -> Result<(), String> {
let repo = local
.repository(repository)
.ok_or_else(|| format!("{repository} vanished while re-seeding"))?;
let seeded = wsb::peer::seed::from_head(&repo.workdir)?;
let store = machine.store();
store.put_baseline(peer, &local.id, repository, &seeded)?;
store.mark_seeded(peer, &local.id, repository, crate::peer::service::now())?;
tracing::warn!(
%peer,
repository = %repository,
paths = seeded.len(),
"a peer planned against a different baseline; re-seeded from the commit they share"
);
Ok(())
}
fn refuse(code: RefusalCode, detail: &str) -> Response {
Response::Refused {
code,
detail: detail.to_string(),
}
}
#[cfg(test)]
#[path = "connection_tests.rs"]
mod tests;