253 lines
8.5 KiB
Rust
253 lines
8.5 KiB
Rust
//! 真实受管 Worker 与隔离资源目录;不访问已安装服务或生产缓存。
|
|
|
|
use dever_runtime::{
|
|
task::{Limits, Runtime},
|
|
wire::json::Json,
|
|
worker::{self, Asset, Bundle, Reply, Setting},
|
|
};
|
|
use sha2::{Digest, Sha256};
|
|
use std::{
|
|
fs,
|
|
os::unix::fs::{DirBuilderExt, PermissionsExt},
|
|
path::PathBuf,
|
|
process::Command,
|
|
time::Duration,
|
|
};
|
|
|
|
const ENTRY: &str = "module/demo/test/worker";
|
|
static EXECUTOR: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
mod command;
|
|
mod lifecycle;
|
|
mod performance;
|
|
const CONTRACT: &str = r#"{"port":"demo.test","adapter":"default","ecosystem":"exec","entry":"module/demo/test/worker","schema":"owned-fixture","capabilities":[],"operations":["bad","crash","echo","error","extra_field","fragment","hang","ignore","measure","missing_error","null_error","shutdown_bad"],"inputs":{},"outputs":{},"errors":{"demo.test.Error.Failed":{}},"setting":null}"#;
|
|
|
|
struct Directory(PathBuf);
|
|
|
|
impl Directory {
|
|
fn new() -> Self {
|
|
let path = std::env::temp_dir().join(format!(
|
|
"dever-worker-{}-{}",
|
|
std::process::id(),
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos()
|
|
));
|
|
fs::DirBuilder::new().mode(0o700).create(&path).unwrap();
|
|
Self(path)
|
|
}
|
|
}
|
|
|
|
impl Drop for Directory {
|
|
fn drop(&mut self) {
|
|
fs::remove_dir_all(&self.0).unwrap();
|
|
}
|
|
}
|
|
|
|
fn bundle(bytes: Vec<u8>) -> &'static Bundle {
|
|
let launch = serde_json::to_vec(&serde_json::json!({"format":"dever-worker-launch-v1","ecosystem":"exec","entry":ENTRY,"executable":ENTRY,"arguments":[],"working_directory":"module/demo/test"})).unwrap();
|
|
let files = [
|
|
(ENTRY, bytes, true),
|
|
("module/demo/test/worker.dever-worker.json", launch, false),
|
|
]
|
|
.map(|(path, bytes, executable)| Asset {
|
|
path,
|
|
sha256: Box::leak(format!("{:x}", Sha256::digest(&bytes)).into_boxed_str()),
|
|
bytes: Box::leak(bytes.into_boxed_slice()),
|
|
executable,
|
|
});
|
|
Box::leak(Box::new(Bundle {
|
|
files: Box::leak(Box::new(files)),
|
|
}))
|
|
}
|
|
|
|
fn pid(reply: Reply) -> u32 {
|
|
let Reply::Result(value) = reply else {
|
|
panic!("expected result")
|
|
};
|
|
let value: serde_json::Value = serde_json::from_str(&value).unwrap();
|
|
assert_eq!(value["value"], 42);
|
|
value["pid"].as_u64().unwrap() as u32
|
|
}
|
|
|
|
async fn call(
|
|
scope: &dever_runtime::task::Scope<'_>,
|
|
bundle: &'static Bundle,
|
|
operation: &str,
|
|
) -> Result<Reply, String> {
|
|
worker::call(
|
|
scope,
|
|
bundle,
|
|
ENTRY,
|
|
CONTRACT,
|
|
&Setting::empty(),
|
|
operation,
|
|
Json::parse("{}").unwrap(),
|
|
)
|
|
.await?
|
|
.resolve(Ok)
|
|
.await
|
|
}
|
|
|
|
#[test]
|
|
fn real_worker_runs_from_a_relocated_owned_executable() {
|
|
let _execution = EXECUTOR.lock().unwrap();
|
|
let directory = Directory::new();
|
|
let fixture = directory.0.join("fixture");
|
|
let source = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../../../test/rust/runtime/worker/fixture.rs");
|
|
let result = Command::new("rustup")
|
|
.args(["run", "1.95.0", "rustc", "--edition=2024", "-O"])
|
|
.arg(source)
|
|
.arg("-o")
|
|
.arg(&fixture)
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
result.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&result.stderr)
|
|
);
|
|
let runner = directory.0.join("runner");
|
|
fs::copy(std::env::current_exe().unwrap(), &runner).unwrap();
|
|
let result = Command::new(runner)
|
|
.args(["--exact", "owned_process_protocol_case", "--nocapture"])
|
|
.env_clear()
|
|
.env("DEVER_WORKER_FIXTURE", &fixture)
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
result.status.success(),
|
|
"{}\n{}",
|
|
String::from_utf8_lossy(&result.stdout),
|
|
String::from_utf8_lossy(&result.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn owned_process_protocol_case() {
|
|
let Some(fixture) = std::env::var_os("DEVER_WORKER_FIXTURE") else {
|
|
return;
|
|
};
|
|
let bundle = bundle(fs::read(&fixture).unwrap());
|
|
fs::remove_file(fixture).unwrap();
|
|
let runtime = Runtime::new(Limits {
|
|
threads: 2,
|
|
workers: 1,
|
|
tasks: 32,
|
|
})
|
|
.unwrap();
|
|
let process = runtime
|
|
.run(async |scope| {
|
|
let first = pid(call(scope, bundle, "echo").await.unwrap());
|
|
assert_eq!(first, pid(call(scope, bundle, "echo").await.unwrap()));
|
|
for source in [
|
|
"null",
|
|
" \n{\"text\":\"雪\\u96ea\\n🙂\",\"value\":1e+02} \t",
|
|
] {
|
|
let response = worker::call(
|
|
scope,
|
|
bundle,
|
|
ENTRY,
|
|
CONTRACT,
|
|
&Setting::empty(),
|
|
"measure",
|
|
Json::parse(source).unwrap(),
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.resolve(Ok)
|
|
.await
|
|
.unwrap();
|
|
let Reply::Result(payload) = response else {
|
|
panic!("expected measured result")
|
|
};
|
|
let measured: serde_json::Value = serde_json::from_str(&payload).unwrap();
|
|
let expected = source.trim_ascii();
|
|
assert_eq!(measured["length"], expected.len());
|
|
assert_eq!(
|
|
measured["checksum"],
|
|
expected.bytes().map(u64::from).sum::<u64>()
|
|
);
|
|
}
|
|
let Reply::Error { identity, payload } = call(scope, bundle, "error").await.unwrap()
|
|
else {
|
|
panic!("business error")
|
|
};
|
|
assert_eq!(identity, "demo.test.Error.Failed");
|
|
assert_eq!(payload, "{\"message\":\"failed\"}");
|
|
for operation in ["hang", "fragment"] {
|
|
assert!(
|
|
tokio::time::timeout(Duration::from_millis(30), call(scope, bundle, operation))
|
|
.await
|
|
.is_err()
|
|
);
|
|
assert_eq!(first, pid(call(scope, bundle, "echo").await.unwrap()));
|
|
}
|
|
assert!(call(scope, bundle, "crash").await.is_err());
|
|
let second = pid(call(scope, bundle, "echo").await.unwrap());
|
|
assert_ne!(first, second);
|
|
assert!(call(scope, bundle, "bad").await.is_err());
|
|
let third = pid(call(scope, bundle, "echo").await.unwrap());
|
|
assert_ne!(second, third);
|
|
worker::finish(scope, Ok(third)).await.unwrap()
|
|
})
|
|
.unwrap();
|
|
assert!(!PathBuf::from(format!("/proc/{process}")).exists());
|
|
for operation in ["null_error", "missing_error", "extra_field"] {
|
|
runtime
|
|
.run(async |scope| {
|
|
let process = pid(call(scope, bundle, "echo").await.unwrap());
|
|
assert!(call(scope, bundle, operation).await.is_err());
|
|
assert!(!PathBuf::from(format!("/proc/{process}")).exists());
|
|
worker::finish(scope, Ok(())).await.unwrap();
|
|
})
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn resource_publication_reuses_only_exact_owned_bytes_and_modes() {
|
|
let directory = Directory::new();
|
|
let resources = bundle(b"not executed".to_vec());
|
|
fs::create_dir(directory.0.join("data")).unwrap();
|
|
fs::set_permissions(directory.0.join("data"), fs::Permissions::from_mode(0o755)).unwrap();
|
|
let root = resources.prepare_in(&directory.0).unwrap();
|
|
assert_eq!(root, resources.prepare_in(&directory.0).unwrap());
|
|
fs::write(root.join("unlocked.py"), b"not part of bundle").unwrap();
|
|
assert!(
|
|
resources
|
|
.prepare_in(&directory.0)
|
|
.unwrap_err()
|
|
.contains("unexpected")
|
|
);
|
|
fs::remove_file(root.join("unlocked.py")).unwrap();
|
|
fs::set_permissions(root.join(ENTRY), fs::Permissions::from_mode(0o700)).unwrap();
|
|
assert!(
|
|
resources
|
|
.prepare_in(&directory.0)
|
|
.unwrap_err()
|
|
.contains("mode")
|
|
);
|
|
fs::write(root.join(ENTRY), b"bad resource").unwrap();
|
|
fs::set_permissions(root.join(ENTRY), fs::Permissions::from_mode(0o500)).unwrap();
|
|
assert!(resources.prepare_in(&directory.0).is_err());
|
|
let duplicate = Bundle {
|
|
files: Box::leak(
|
|
vec![Asset {
|
|
path: "../escape",
|
|
bytes: b"",
|
|
sha256: "",
|
|
executable: false,
|
|
}]
|
|
.into_boxed_slice(),
|
|
),
|
|
};
|
|
assert!(
|
|
duplicate
|
|
.prepare_in(&directory.0)
|
|
.unwrap_err()
|
|
.contains("path")
|
|
);
|
|
}
|