feat: release Dever 0.1.5 with direct workers and long transactions
This commit is contained in:
1 parent
89629c2197
commit
a900d7e8db
153 files changed
+3676
-3708
No files matched your search
@@ -13,14 +13,14 @@ default = []
|
||||
auth-crypto = ["dep:sha2", "dep:hmac", "dep:subtle"]
|
||||
crypto = ["auth-crypto", "dep:argon2", "dep:getrandom"]
|
||||
wire = ["dep:serde", "dep:serde_json", "serde_json/raw_value", "dep:base64", "dep:getrandom"]
|
||||
external = ["wire", "dep:sha2", "dep:dever-sandbox"]
|
||||
external = ["wire", "dep:sha2", "dep:dever-process"]
|
||||
api = ["wire", "auth-crypto"]
|
||||
database = ["wire", "dep:getrandom"]
|
||||
sqlite = ["database", "dep:deadpool-sqlite"]
|
||||
postgres = ["database", "dep:deadpool-postgres", "dep:tokio-postgres", "dep:tokio-postgres-rustls"]
|
||||
|
||||
[dependencies]
|
||||
dever-sandbox = { path = "../dever-sandbox", optional = true }
|
||||
dever-process = { path = "../dever-process", optional = true }
|
||||
dec.workspace = true
|
||||
indexmap.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -5,7 +5,13 @@ use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
|
||||
#[cfg(not(feature = "external"))]
|
||||
use tokio::process::Child;
|
||||
use tokio::process::{ChildStderr, ChildStdin, ChildStdout, Command};
|
||||
#[cfg(feature = "external")]
|
||||
mod child;
|
||||
#[cfg(feature = "external")]
|
||||
use child::Child;
|
||||
use tokio::sync::{Mutex, mpsc, oneshot};
|
||||
|
||||
mod command;
|
||||
@@ -99,7 +105,6 @@ fn session() -> Result<Arc<Session>, String> {
|
||||
current().ok_or_else(|| "external Adapter requires a Dever invocation".into())
|
||||
}
|
||||
|
||||
/// One settings snapshot owns both Adapter selection and deployment grants.
|
||||
pub fn adapter_settings() -> Result<Arc<crate::config::Settings>, String> {
|
||||
#[cfg(feature = "api")]
|
||||
if let Some(application) = crate::application::current() {
|
||||
@@ -359,14 +364,11 @@ async fn supervise(
|
||||
let mut command = worker_command(&definition)?;
|
||||
command
|
||||
.arg("--dever-component")
|
||||
.env_clear()
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|error| format!("cannot start external Adapter worker: {error}"))?;
|
||||
let mut child = spawn_child(&mut command)?;
|
||||
let Some(stdin) = child.stdin.take() else {
|
||||
terminate_child(&mut child).await?;
|
||||
return Err("external Adapter worker has no stdin".into());
|
||||
@@ -395,7 +397,7 @@ fn worker_command(definition: &Definition) -> Result<Command, String> {
|
||||
#[cfg(not(feature = "external"))]
|
||||
{
|
||||
let _ = definition;
|
||||
Err("external Worker sandbox runtime feature is not enabled".into())
|
||||
Err("external Worker process_runtime runtime feature is not enabled".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,21 +532,30 @@ async fn wait_for_exit(child: &mut Child) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn terminate_child(child: &mut Child) -> Result<(), String> {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_)) => return Ok(()),
|
||||
Ok(None) => {}
|
||||
Err(error) => return Err(format!("cannot inspect external Adapter worker: {error}")),
|
||||
}
|
||||
if let Err(error) = child.start_kill()
|
||||
&& !matches!(child.try_wait(), Ok(Some(_)))
|
||||
fn spawn_child(command: &mut Command) -> Result<Child, String> {
|
||||
let child = command
|
||||
.spawn()
|
||||
.map_err(|error| format!("cannot start external program: {error}"))?;
|
||||
#[cfg(feature = "external")]
|
||||
{
|
||||
return Err(format!("cannot stop external Adapter worker: {error}"));
|
||||
Child::new(child)
|
||||
}
|
||||
#[cfg(not(feature = "external"))]
|
||||
{
|
||||
Ok(child)
|
||||
}
|
||||
}
|
||||
|
||||
async fn terminate_child(child: &mut Child) -> Result<(), String> {
|
||||
if child.id().is_some() {
|
||||
child
|
||||
.start_kill()
|
||||
.map_err(|error| format!("cannot stop external program: {error}"))?;
|
||||
}
|
||||
match tokio::time::timeout(SHUTDOWN_GRACE, child.wait()).await {
|
||||
Ok(Ok(_)) => Ok(()),
|
||||
Ok(Err(error)) => Err(format!("cannot wait for external Adapter worker: {error}")),
|
||||
Err(_) => Err("external Adapter worker could not be reaped before its deadline".into()),
|
||||
Ok(Err(error)) => Err(format!("cannot reap external program: {error}")),
|
||||
Err(_) => Err("external program exceeded its cleanup deadline".into()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::io;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::process::ExitStatus;
|
||||
|
||||
pub(super) struct Child {
|
||||
child: tokio::process::Child,
|
||||
#[cfg(target_os = "linux")]
|
||||
group: Option<tokio::io::unix::AsyncFd<dever_process::ProcessGroup>>,
|
||||
}
|
||||
|
||||
impl Child {
|
||||
pub(super) fn new(mut child: tokio::process::Child) -> Result<Self, String> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let result =
|
||||
dever_process::ProcessGroup::pin(child.id().ok_or("new child has no PID")?)
|
||||
.and_then(|group| {
|
||||
tokio::io::unix::AsyncFd::new(group).map_err(|error| error.to_string())
|
||||
});
|
||||
match result {
|
||||
Ok(group) => Ok(Self {
|
||||
child,
|
||||
group: Some(group),
|
||||
}),
|
||||
Err(error) => {
|
||||
let _ = child.start_kill();
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = child.start_kill();
|
||||
Err("managed external programs currently require Linux".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_group(&mut self) -> io::Result<()> {
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(group) = self.group.take() {
|
||||
group.get_ref().terminate().map_err(io::Error::other)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn start_kill(&mut self) -> io::Result<()> {
|
||||
self.stop_group()?;
|
||||
self.child.start_kill()
|
||||
}
|
||||
|
||||
pub(super) async fn wait(&mut self) -> io::Result<ExitStatus> {
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(group) = &self.group {
|
||||
let _ready = group.readable().await?;
|
||||
}
|
||||
self.stop_group()?;
|
||||
self.child.wait().await
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Child {
|
||||
type Target = tokio::process::Child;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.child
|
||||
}
|
||||
}
|
||||
impl DerefMut for Child {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.child
|
||||
}
|
||||
}
|
||||
impl Drop for Child {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.stop_group();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
//! One ordinary child per request, owned by the existing Adapter supervisor.
|
||||
use super::{Definition, Reply, Request, adapter_settings, terminate_child, worker_command};
|
||||
#[cfg(feature = "external")]
|
||||
use super::spawn_child;
|
||||
use super::{Child, Definition, Reply, Request, adapter_settings, terminate_child, worker_command};
|
||||
use crate::bytes::Bytes;
|
||||
use crate::config::CommandLimits;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout};
|
||||
use tokio::process::{ChildStderr, ChildStdin, ChildStdout};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
mod observation;
|
||||
use observation::Observation;
|
||||
|
||||
#[cfg(feature = "external")]
|
||||
const ARGUMENT_BYTES: usize = 65_536;
|
||||
#[cfg(feature = "external")]
|
||||
@@ -71,7 +70,6 @@ pub(super) async fn supervise(
|
||||
) -> Result<(), String> {
|
||||
let prepared: Result<CommandLimits, String> = (|| {
|
||||
let limits = adapter_settings()?.command_limits(&definition.port)?;
|
||||
// Validate resources and grants without starting the user's executable.
|
||||
worker_command(&definition)?;
|
||||
Ok(limits)
|
||||
})();
|
||||
@@ -155,31 +153,27 @@ fn input(payload: &str) -> Result<Input, String> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "external")]
|
||||
fn spawn(definition: &Definition, payload: &str) -> Result<(Input, Child, Observation), String> {
|
||||
fn spawn(definition: &Definition, payload: &str) -> Result<(Input, Child), String> {
|
||||
let input = input(payload)?;
|
||||
let (command, observation) = crate::external::observed_command(definition)?;
|
||||
let observation = Observation::new(observation)?;
|
||||
let command = crate::external::worker_command(definition)?;
|
||||
let mut command = tokio::process::Command::from(command);
|
||||
// Resource arguments were mapped by the sandbox owner. Call arguments
|
||||
// are opaque values, including absolute sandbox paths and shell syntax.
|
||||
// Resource arguments were mapped by the process_runtime owner. Call arguments
|
||||
// are opaque values, including absolute process_runtime paths and shell syntax.
|
||||
command
|
||||
.args(&input.arguments)
|
||||
.env_clear()
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
let child = command
|
||||
.spawn()
|
||||
.map_err(|error| format!("cannot start external command: {error}"))?;
|
||||
let child = spawn_child(&mut command)?;
|
||||
// Dropping Command closes the parent's copies of mapped pipe descriptors.
|
||||
drop(command);
|
||||
Ok((input, child, observation))
|
||||
Ok((input, child))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "external"))]
|
||||
fn spawn(_: &Definition, _: &str) -> Result<(Input, Child, Observation), String> {
|
||||
Err("external command sandbox runtime feature is not enabled".into())
|
||||
fn spawn(_: &Definition, _: &str) -> Result<(Input, Child), String> {
|
||||
Err("external command process_runtime runtime feature is not enabled".into())
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
@@ -188,7 +182,7 @@ async fn execute(
|
||||
request: &mut Request,
|
||||
stopping: &mut oneshot::Receiver<()>,
|
||||
) -> Execution {
|
||||
let (input, mut child, mut observation) = match spawn(definition, &request.payload) {
|
||||
let (input, mut child) = match spawn(definition, &request.payload) {
|
||||
Ok(prepared) => prepared,
|
||||
Err(error) => {
|
||||
return Execution {
|
||||
@@ -198,12 +192,9 @@ async fn execute(
|
||||
};
|
||||
}
|
||||
};
|
||||
let wrapper = child
|
||||
.id()
|
||||
.expect("newly spawned command launcher has a PID");
|
||||
let pipes = (child.stdin.take(), child.stdout.take(), child.stderr.take());
|
||||
let (Some(stdin), Some(stdout), Some(stderr)) = pipes else {
|
||||
let cleanup_error = cleanup(&mut child, &mut observation, wrapper).await.err();
|
||||
let cleanup_error = terminate_child(&mut child).await.err();
|
||||
return Execution {
|
||||
reply: Err(cleanup_error
|
||||
.clone()
|
||||
@@ -215,8 +206,7 @@ async fn execute(
|
||||
let mut stopped = false;
|
||||
let result = {
|
||||
let completion = async {
|
||||
let (_, _, (stdout, stderr), status) = tokio::try_join!(
|
||||
observation.started(wrapper),
|
||||
let (_, (stdout, stderr), status) = tokio::try_join!(
|
||||
write_stdin(stdin, input.stdin),
|
||||
capture(stdout, stderr, limits.output_limit),
|
||||
async {
|
||||
@@ -228,7 +218,7 @@ async fn execute(
|
||||
)?;
|
||||
let code = status
|
||||
.code()
|
||||
.ok_or("external command sandbox terminated without an exit code")?;
|
||||
.ok_or("external command process_runtime terminated without an exit code")?;
|
||||
output(code, stdout, stderr)
|
||||
};
|
||||
tokio::select! {
|
||||
@@ -243,7 +233,7 @@ async fn execute(
|
||||
}
|
||||
}
|
||||
};
|
||||
let cleanup_error = cleanup(&mut child, &mut observation, wrapper).await.err();
|
||||
let cleanup_error = terminate_child(&mut child).await.err();
|
||||
let reply = match (result, &cleanup_error) {
|
||||
(result, None) => result,
|
||||
(Ok(_), Some(cleanup)) => Err(cleanup.clone()),
|
||||
@@ -256,25 +246,6 @@ async fn execute(
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup(
|
||||
child: &mut Child,
|
||||
observation: &mut Observation,
|
||||
wrapper: u32,
|
||||
) -> Result<(), String> {
|
||||
let namespace = tokio::time::timeout(super::SHUTDOWN_GRACE, observation.terminate(wrapper))
|
||||
.await
|
||||
.unwrap_or_else(|_| Err("external command namespace exceeded its cleanup deadline".into()));
|
||||
// Reap the owned launcher even if namespace observation itself failed.
|
||||
// Both stages have independent finite deadlines; neither error is lost.
|
||||
let launcher = terminate_child(child).await;
|
||||
match (namespace, launcher) {
|
||||
(Err(namespace), Err(launcher)) => {
|
||||
Err(format!("{namespace}; launcher cleanup failed: {launcher}"))
|
||||
}
|
||||
(namespace, launcher) => namespace.and(launcher),
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_stdin(mut stdin: ChildStdin, input: Option<Bytes>) -> Result<(), String> {
|
||||
if let Some(input) = input {
|
||||
match stdin.write_all(input.values()).await {
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
#[cfg(all(feature = "external", target_os = "linux"))]
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
pub(super) struct Observation {
|
||||
#[cfg(all(feature = "external", target_os = "linux"))]
|
||||
startup: tokio::net::unix::pipe::Receiver,
|
||||
#[cfg(all(feature = "external", target_os = "linux"))]
|
||||
information: tokio::net::unix::pipe::Receiver,
|
||||
#[cfg(all(feature = "external", target_os = "linux"))]
|
||||
information_bytes: Vec<u8>,
|
||||
#[cfg(all(feature = "external", target_os = "linux"))]
|
||||
release: Option<std::fs::File>,
|
||||
#[cfg(all(feature = "external", target_os = "linux"))]
|
||||
namespace: Option<tokio::io::unix::AsyncFd<dever_sandbox::CommandNamespace>>,
|
||||
}
|
||||
|
||||
impl Observation {
|
||||
#[cfg(all(feature = "external", target_os = "linux"))]
|
||||
pub(super) fn new(observation: dever_sandbox::CommandObservation) -> Result<Self, String> {
|
||||
use tokio::net::unix::pipe::Receiver;
|
||||
Ok(Self {
|
||||
startup: Receiver::from_file(observation.startup).map_err(|error| error.to_string())?,
|
||||
information: Receiver::from_file(observation.namespace)
|
||||
.map_err(|error| error.to_string())?,
|
||||
information_bytes: Vec::new(),
|
||||
release: Some(observation.release),
|
||||
namespace: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "external", not(target_os = "linux")))]
|
||||
pub(super) fn new(_: dever_sandbox::CommandObservation) -> Result<Self, String> {
|
||||
Err("external commands require Linux".into())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "external", target_os = "linux"))]
|
||||
async fn pin(&mut self, wrapper: u32) -> Result<(), String> {
|
||||
if self.namespace.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut buffer = [0; 1024];
|
||||
loop {
|
||||
let count = self
|
||||
.information
|
||||
.read(&mut buffer)
|
||||
.await
|
||||
.map_err(|error| format!("cannot read command namespace identity: {error}"))?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
if self.information_bytes.len() + count > 4096 {
|
||||
return Err("command namespace identity exceeds its byte limit".into());
|
||||
}
|
||||
self.information_bytes.extend_from_slice(&buffer[..count]);
|
||||
}
|
||||
let text = std::str::from_utf8(&self.information_bytes)
|
||||
.map_err(|_| "command namespace identity is not UTF-8")?;
|
||||
let node = crate::wire::parse(text)?;
|
||||
let pid = node
|
||||
.object()?
|
||||
.get("child-pid")
|
||||
.ok_or("command namespace identity is missing")?
|
||||
.int()?;
|
||||
let namespace = dever_sandbox::CommandNamespace::pin(
|
||||
i32::try_from(pid).map_err(|_| "invalid command namespace PID")?,
|
||||
wrapper,
|
||||
)?;
|
||||
self.namespace = Some(
|
||||
tokio::io::unix::AsyncFd::new(namespace)
|
||||
.map_err(|error| format!("cannot observe command namespace exit: {error}"))?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn started(&mut self, wrapper: u32) -> Result<(), String> {
|
||||
#[cfg(all(feature = "external", target_os = "linux"))]
|
||||
{
|
||||
self.pin(wrapper).await?;
|
||||
// EOF releases bwrap only after its namespace is pinned. Neither
|
||||
// normal cancellation nor a very short process can race PID reuse.
|
||||
drop(self.release.take());
|
||||
let mut status = Vec::new();
|
||||
(&mut self.startup)
|
||||
.take(4096)
|
||||
.read_to_end(&mut status)
|
||||
.await
|
||||
.map_err(|error| format!("cannot read command startup status: {error}"))?;
|
||||
match status.as_slice() {
|
||||
b"R" => Ok(()),
|
||||
[] => Err("external command sandbox failed before starting its guard".into()),
|
||||
status => {
|
||||
let message = status
|
||||
.strip_prefix(b"RE")
|
||||
.or_else(|| status.strip_prefix(b"E"));
|
||||
Err(match message {
|
||||
Some(message) => format!(
|
||||
"external command startup failed: {}",
|
||||
String::from_utf8_lossy(message)
|
||||
),
|
||||
None => "invalid external command startup status".into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(all(feature = "external", target_os = "linux")))]
|
||||
{
|
||||
let _ = wrapper;
|
||||
Err("external commands require Linux".into())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn terminate(&mut self, wrapper: u32) -> Result<(), String> {
|
||||
#[cfg(all(feature = "external", target_os = "linux"))]
|
||||
{
|
||||
self.pin(wrapper).await?;
|
||||
let namespace = self
|
||||
.namespace
|
||||
.as_ref()
|
||||
.expect("namespace pinned before cleanup");
|
||||
namespace.get_ref().terminate()?;
|
||||
// pidfd readiness follows kernel namespace teardown, unlike pipe
|
||||
// EOF, which occurs while the init is still closing its files.
|
||||
let _exited = namespace
|
||||
.readable()
|
||||
.await
|
||||
.map_err(|error| format!("cannot wait for command namespace exit: {error}"))?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(all(feature = "external", target_os = "linux")))]
|
||||
{
|
||||
let _ = wrapper;
|
||||
Err("external commands require Linux".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ pub use command::CommandLimits;
|
||||
pub use compilation::{CompilationBindings, CompilationSite, DatabaseDriver};
|
||||
|
||||
const DATA_DIRECTORIES: [&str; 5] = ["db", "upload", "log", "cache", "tmp"];
|
||||
const ADAPTER_FIELDS: &[&str] = &["use", "setting", "files", "command"];
|
||||
const ADAPTER_FIELDS: &[&str] = &["use", "setting", "command"];
|
||||
static SETTINGS: OnceLock<Settings> = OnceLock::new();
|
||||
pub const MAX_JOB_TIMEOUT_MS: u32 = 3_600_000;
|
||||
pub const MAX_JOB_LEASE_MS: i64 = 2 * MAX_JOB_TIMEOUT_MS as i64;
|
||||
@@ -764,77 +764,6 @@ impl Settings {
|
||||
Ok((index, setting))
|
||||
}
|
||||
|
||||
/// File access is a deployment grant for the selected Port, not Worker
|
||||
/// business settings. Paths stay below the application and appear at /data.
|
||||
#[cfg(feature = "external")]
|
||||
pub fn external_grants(
|
||||
&self,
|
||||
identity: &str,
|
||||
capabilities: &[String],
|
||||
) -> Result<Vec<dever_sandbox::Grant>, String> {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct FileGrant {
|
||||
path: PathBuf,
|
||||
#[serde(default)]
|
||||
write: bool,
|
||||
}
|
||||
let Some(adapter) = &self.adapter else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let node = crate::wire::parse(adapter.get())?;
|
||||
let Some(binding) = node.object()?.get(identity) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let binding = binding.fields(ADAPTER_FIELDS)?;
|
||||
let Some(files) = binding.get("files") else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
if !capabilities.iter().any(|capability| capability == "file") {
|
||||
return Err("Adapter file grants require its checked file capability".into());
|
||||
}
|
||||
let files: BTreeMap<String, FileGrant> = serde_json::from_str(files.raw())
|
||||
.map_err(|_| "Adapter files must map names to {path, write} grants")?;
|
||||
if files.len() > 64 {
|
||||
return Err("Adapter has too many file grants".into());
|
||||
}
|
||||
let mut grants = Vec::new();
|
||||
for (name, grant) in files {
|
||||
if name.is_empty()
|
||||
|| name.len() > 64
|
||||
|| !name
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
|
||||
{
|
||||
return Err("Adapter file grant names must contain lowercase letters, digits or underscores".into());
|
||||
}
|
||||
if grant.path.as_os_str().is_empty()
|
||||
|| grant.path.is_absolute()
|
||||
|| grant
|
||||
.path
|
||||
.components()
|
||||
.any(|part| !matches!(part, Component::Normal(_)))
|
||||
{
|
||||
return Err("Adapter file grant path must be relative to the application".into());
|
||||
}
|
||||
let executable_cache = Path::new("data/cache/lib");
|
||||
if grant.write
|
||||
&& (grant.path.starts_with(executable_cache)
|
||||
|| executable_cache.starts_with(&grant.path))
|
||||
{
|
||||
return Err(
|
||||
"Adapter writable file grant overlaps the executable resource cache".into(),
|
||||
);
|
||||
}
|
||||
grants.push(dever_sandbox::Grant {
|
||||
source: self.executable_directory.join(grant.path),
|
||||
destination: Path::new("/data").join(name),
|
||||
writable: grant.write,
|
||||
});
|
||||
}
|
||||
Ok(grants)
|
||||
}
|
||||
|
||||
pub fn initialize_data_directories(&self) -> Result<(), String> {
|
||||
for name in DATA_DIRECTORIES {
|
||||
let path = self.executable_directory.join("data").join(name);
|
||||
|
||||
@@ -87,59 +87,26 @@ struct LiteralArgument {
|
||||
pub fn worker_command(
|
||||
definition: &crate::component::Definition,
|
||||
) -> Result<std::process::Command, String> {
|
||||
sandbox_launch(definition, |launch| launch.command())
|
||||
}
|
||||
|
||||
pub(crate) fn observed_command(
|
||||
definition: &crate::component::Definition,
|
||||
) -> Result<(std::process::Command, dever_sandbox::CommandObservation), String> {
|
||||
sandbox_launch(definition, |launch| launch.observed_command())
|
||||
}
|
||||
|
||||
fn sandbox_launch<T>(
|
||||
definition: &crate::component::Definition,
|
||||
build: impl FnOnce(dever_sandbox::Launch<'_>) -> Result<T, String>,
|
||||
) -> Result<T, String> {
|
||||
let session =
|
||||
crate::component::current().ok_or("external resources require a Dever invocation")?;
|
||||
let bundle = session
|
||||
.resources
|
||||
.get()
|
||||
.ok_or("external Worker resources and sandbox were not prepared")?;
|
||||
.ok_or("external Worker resources were not prepared")?;
|
||||
let launch = verified_worker_launch(
|
||||
&bundle.directory,
|
||||
&bundle.resources,
|
||||
&definition.entry,
|
||||
&definition.ecosystem,
|
||||
)?;
|
||||
let settings = crate::component::adapter_settings()?
|
||||
.external_grants(&definition.port, &definition.capabilities)?;
|
||||
if definition
|
||||
.capabilities
|
||||
.iter()
|
||||
.any(|capability| capability == "gpu")
|
||||
{
|
||||
return Err("GPU Worker requires a supported device isolation backend".into());
|
||||
}
|
||||
let arguments = launch.arguments;
|
||||
build(dever_sandbox::Launch {
|
||||
assets: &bundle.directory.join("sandbox"),
|
||||
dever_process::Launch {
|
||||
assets: &bundle.directory.join("process-runtime"),
|
||||
worker: &launch.working_directory,
|
||||
executable: &launch.executable,
|
||||
arguments: &arguments,
|
||||
arguments: &launch.arguments,
|
||||
working_directory: &launch.working_directory,
|
||||
capabilities: dever_sandbox::Capabilities {
|
||||
network: definition
|
||||
.capabilities
|
||||
.iter()
|
||||
.any(|capability| capability == "network"),
|
||||
process: definition
|
||||
.capabilities
|
||||
.iter()
|
||||
.any(|capability| capability == "process"),
|
||||
},
|
||||
grants: &settings,
|
||||
})
|
||||
}
|
||||
.command()
|
||||
}
|
||||
|
||||
pub fn verified_worker_launch(
|
||||
|
||||
Reference in new issue
Block a user