sausage core: modules per topic, journal writer thread with WAL and batches, hash cache by metadata, 49 tests per rule
Blank stdout lines give no warning. core.exit requests the exit in its own handler.
This commit is contained in:
+673
-98
@@ -52,15 +52,35 @@ async fn call(h: &Host, m: &str, p: Value) -> Value {
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn routing_environment_files_and_errors() {
|
||||
|
||||
/// PROTOCOL 3.1: json rpc lines carry results.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn json_rpc_lines_carry_results() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(call(&h, "probe.echo", json!({"x":1})).await, json!({"x":1}));
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.2: method names split at the first dot.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn method_names_split_at_the_first_dot() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
call(&h, "probe.rpc.discover", json!({"x":2})).await,
|
||||
json!({"x":2})
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.3: processes can call other plugins.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn processes_can_call_other_plugins() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
call(
|
||||
&h,
|
||||
@@ -70,10 +90,28 @@ async fn routing_environment_files_and_errors() {
|
||||
.await,
|
||||
json!({"nested":true})
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.3: pending ids distinguish numbers from strings.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pending_ids_distinguish_numbers_from_strings() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
call(&h, "probe.multicall", json!({})).await,
|
||||
json!({"count":2})
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.3: notifications start attached processes.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn notifications_start_attached_processes() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(
|
||||
&h,
|
||||
"probe.notification",
|
||||
@@ -84,14 +122,41 @@ async fn routing_environment_files_and_errors() {
|
||||
std::fs::read_to_string(w.0.join("state/notification")).unwrap(),
|
||||
"delivered"
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.3: unknown response ids are dropped.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn unknown_response_ids_are_dropped() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
h.call("probe.unknownid", json!({})).await.unwrap_err()["code"],
|
||||
-32002
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.8: process environment identifies the caller.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn process_environment_identifies_the_caller() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let env = call(&h, "probe.env", json!({})).await;
|
||||
assert_eq!(env["plugin"], "probe");
|
||||
assert_eq!(env["parent"], "console");
|
||||
assert!(env["pid"].as_str().unwrap().starts_with("probe-"));
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.6: processes read and write the world.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn processes_read_and_write_the_world() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(
|
||||
&h,
|
||||
"probe.write",
|
||||
@@ -102,18 +167,54 @@ async fn routing_environment_files_and_errors() {
|
||||
call(&h, "probe.read", json!({"path":"/state/test"})).await,
|
||||
"hello"
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.9: missing plugins return minus 32001.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn missing_plugins_return_minus_32001() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
h.call("absent.run", json!({})).await.unwrap_err()["code"],
|
||||
-32001
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 9: unimplemented core methods return method not found.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn unimplemented_core_methods_return_method_not_found() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
h.call("core.activate", json!({})).await.unwrap_err()["code"],
|
||||
-32601
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.1: params must be objects.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn params_must_be_objects() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
h.call("probe.echo", json!([])).await.unwrap_err()["code"],
|
||||
-32602
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.9: exit without answer reports status and last logs.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exit_without_answer_reports_status_and_last_logs() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let e = h.call("probe.exit", json!({})).await.unwrap_err();
|
||||
assert_eq!(e["code"], -32002);
|
||||
assert!(
|
||||
@@ -123,10 +224,13 @@ async fn routing_environment_files_and_errors() {
|
||||
.iter()
|
||||
.any(|l| l["line"] == "last line")
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn journal_notifications_locks_and_kill() {
|
||||
|
||||
/// PROTOCOL 3.5: progress notifications reach the caller.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn progress_notifications_reach_the_caller() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let mut notifications = h.subscribe();
|
||||
@@ -135,6 +239,15 @@ async fn journal_notifications_locks_and_kill() {
|
||||
notifications.recv().await.unwrap()["params"]["name"],
|
||||
"progress"
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.1: non message stdout warns once without its content.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn non_message_stdout_warns_once_without_its_content() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(&h, "probe.junk", json!({})).await;
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
||||
while !h.processes().is_empty() {
|
||||
@@ -148,8 +261,28 @@ async fn journal_notifications_locks_and_kill() {
|
||||
let warnings = call(&h, "core.log", json!({"pid":pid,"grep":"discarded stdout"})).await;
|
||||
assert_eq!(warnings.as_array().unwrap().len(), 1);
|
||||
assert!(!warnings.to_string().contains("junk"));
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.1: stderr lines enter the journal.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn stderr_lines_enter_the_journal() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(&h, "probe.junk", json!({})).await;
|
||||
h.stop().await;
|
||||
let log = call(&h, "core.log", json!({"grep":"stderr line"})).await;
|
||||
assert_eq!(log.as_array().unwrap().len(), 1);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 6: try lock reports held without waiting.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn try_lock_reports_held_without_waiting() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
call(&h, "core.lock", json!({"path":"x"})).await,
|
||||
json!({"held":true})
|
||||
@@ -163,13 +296,25 @@ async fn journal_notifications_locks_and_kill() {
|
||||
call(&h, "probe.lock", json!({"path":"x"})).await,
|
||||
json!({"held":true})
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 9: kill ends a process at once.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn kill_ends_a_process_at_once() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let pid = call(&h, "probe.serve", json!({})).await;
|
||||
call(&h, "core.kill", json!({"pid":pid})).await;
|
||||
assert!(!h.processes().contains_key(pid.as_str().unwrap()));
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn native_and_reserved_world_paths() {
|
||||
|
||||
/// PROTOCOL 9: native calls have process ids and results.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn native_calls_have_process_ids_and_results() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
h.register_native(
|
||||
@@ -184,21 +329,30 @@ async fn native_and_reserved_world_paths() {
|
||||
let result = call(&h, "native.echo", json!({"x":1})).await;
|
||||
assert_eq!(result["method"], "echo");
|
||||
assert!(result["pid"].as_str().unwrap().starts_with("native-"));
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 2: reserved world roots prevent startup.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn reserved_world_roots_prevent_startup() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
for name in ["proc", "run", "self"] {
|
||||
std::fs::create_dir(w.0.join(name)).unwrap();
|
||||
assert!(World::open(&w.0).is_err());
|
||||
std::fs::remove_dir(w.0.join(name)).unwrap();
|
||||
}
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn tree_detach_stop_boot_and_scheduler() {
|
||||
/// PROTOCOL 8: boot waits for each readiness result before the next service.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn boot_waits_for_readiness_in_order() {
|
||||
let w = TestWorld::new();
|
||||
std::fs::write(
|
||||
w.0.join("config/boot.json"),
|
||||
serde_json::to_vec(&json!([
|
||||
{"method":"absent.start","params":{}},
|
||||
{"method":"probe.write","params":{"path":"/state/boot","text":"ready"}},
|
||||
{"method":"probe.read","params":{"path":"/state/boot"}}
|
||||
]))
|
||||
@@ -210,6 +364,15 @@ async fn tree_detach_stop_boot_and_scheduler() {
|
||||
std::fs::read_to_string(w.0.join("state/boot")).unwrap(),
|
||||
"ready"
|
||||
);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.6: attached children stop when the parent ends.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn attached_children_stop_when_the_parent_ends() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let attached = call(
|
||||
&h,
|
||||
"probe.call",
|
||||
@@ -223,6 +386,15 @@ async fn tree_detach_stop_boot_and_scheduler() {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.4: detached calls have no parent and outlive the caller.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn detached_calls_have_no_parent_and_outlive_the_caller() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let detached = call(
|
||||
&h,
|
||||
"probe.call",
|
||||
@@ -231,6 +403,15 @@ async fn tree_detach_stop_boot_and_scheduler() {
|
||||
.await;
|
||||
assert!(h.processes()[detached.as_str().unwrap()].detached);
|
||||
call(&h, "core.stop", json!({"pid":detached})).await;
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.8: busy guests do not block other calls.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn busy_guests_do_not_block_other_calls() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let hc = h.clone();
|
||||
let busy = tokio::spawn(async move { hc.call("probe.busy", json!({})).await });
|
||||
let pid = tokio::time::timeout(std::time::Duration::from_secs(20), async {
|
||||
@@ -249,13 +430,16 @@ async fn tree_detach_stop_boot_and_scheduler() {
|
||||
);
|
||||
call(&h, "core.kill", json!({"pid":pid})).await;
|
||||
assert_eq!(busy.await.unwrap().unwrap_err()["code"], -32002);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
/// PROTOCOL 6: same process lock acquisitions queue.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn same_process_lock_acquisitions_queue() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
|
||||
h.register_native(
|
||||
"locks",
|
||||
std::sync::Arc::new(|ctx, _, _| {
|
||||
@@ -290,46 +474,16 @@ async fn same_process_lock_acquisitions_queue() {
|
||||
call(&h, "core.lock", json!({"path":"fifo","wait":false})).await,
|
||||
json!({"held":true})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn stop_closes_stdin_deadline_and_exit() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let hc = h.clone();
|
||||
let pending = tokio::spawn(async move { hc.call("probe.wait_eof", json!({})).await });
|
||||
let pid = tokio::time::timeout(std::time::Duration::from_secs(20), async {
|
||||
loop {
|
||||
if let Some((pid, _)) = h
|
||||
.processes()
|
||||
.into_iter()
|
||||
.find(|(_, p)| p.method == "wait_eof")
|
||||
{
|
||||
break pid;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
call(&h, "core.stop", json!({"pid":pid})).await;
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(w.0.join("state/stopped")).unwrap(),
|
||||
"yes"
|
||||
);
|
||||
assert_eq!(pending.await.unwrap().unwrap_err()["code"], -32002);
|
||||
assert_eq!(call(&h, "core.exit", json!({})).await, json!({}));
|
||||
tokio::time::timeout(std::time::Duration::from_secs(1), h.exit_requested())
|
||||
.await
|
||||
.unwrap();
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
/// PROTOCOL 3.8: tcp udp and dns.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn tcp_udp_and_dns() {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let tcp = format!("localhost:{}", listener.local_addr().unwrap().port());
|
||||
let udp = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
@@ -346,11 +500,16 @@ async fn tcp_udp_and_dns() {
|
||||
udp.recv_from(&mut buf).await.unwrap();
|
||||
assert_eq!(&buf, b"udp");
|
||||
server.await.unwrap();
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
/// PROTOCOL 6: native kill drops future and releases locks.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn native_kill_drops_future_and_releases_locks() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -361,8 +520,6 @@ async fn native_kill_drops_future_and_releases_locks() {
|
||||
self.0.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let dropped = Arc::new(AtomicBool::new(false));
|
||||
let flag = dropped.clone();
|
||||
h.register_native(
|
||||
@@ -405,13 +562,16 @@ async fn native_kill_drops_future_and_releases_locks() {
|
||||
json!({"held":true})
|
||||
);
|
||||
assert_eq!(pending.await.unwrap().unwrap_err()["code"], -32002);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
/// PROTOCOL 3.6: shutdown stops busy processes in parallel after five seconds.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn shutdown_stops_busy_processes_in_parallel_after_five_seconds() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
for _ in 0..2 {
|
||||
let hc = h.clone();
|
||||
@@ -434,61 +594,16 @@ async fn shutdown_stops_busy_processes_in_parallel_after_five_seconds() {
|
||||
assert_eq!(task.await.unwrap().unwrap_err()["code"], -32002);
|
||||
}
|
||||
assert!(h.processes().is_empty());
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn journal_filters_persist_and_process_versions_are_hashes() {
|
||||
use sha2::{Digest, Sha256};
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let pid = call(&h, "probe.serve", json!({"version":1})).await;
|
||||
let pid = pid.as_str().unwrap();
|
||||
let info = h.processes()[pid].clone();
|
||||
assert_eq!(
|
||||
info.version,
|
||||
format!("{:x}", Sha256::digest(std::fs::read(fixture()).unwrap()))
|
||||
);
|
||||
assert_eq!(info.plugin, "probe");
|
||||
assert_eq!(info.method, "serve");
|
||||
assert_eq!(info.params, json!({"version":1}));
|
||||
assert_eq!(info.parent.as_deref(), Some("console"));
|
||||
assert_eq!(info.state, "running");
|
||||
assert!(!info.detached);
|
||||
assert!(info.start_time > 0);
|
||||
let uuid = uuid::Uuid::parse_str(pid.strip_prefix("probe-").unwrap()).unwrap();
|
||||
assert_eq!(uuid.get_version_num(), 7);
|
||||
call(&h, "core.stop", json!({"pid":pid})).await;
|
||||
let entries = call(
|
||||
&h,
|
||||
"core.log",
|
||||
json!({"plugin":"probe","pid":pid,"limit":2}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(entries.as_array().unwrap().len(), 2);
|
||||
let entry = &entries[0];
|
||||
let id = &entry["id"];
|
||||
let single = call(&h, "core.log", json!({"since":id,"until":id})).await;
|
||||
assert_eq!(single, json!([entry]));
|
||||
h.stop().await;
|
||||
drop(h);
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
call(&h, "core.log", json!({"since":id,"until":id})).await,
|
||||
single
|
||||
);
|
||||
h.stop().await;
|
||||
assert!(
|
||||
std::fs::read(w.0.join("state/core/journal.sqlite"))
|
||||
.unwrap()
|
||||
.starts_with(b"SQLite format 3")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
/// PROTOCOL 3.6: stopping rejects calls but allows notifications.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn stopping_rejects_calls_but_allows_notifications() {
|
||||
use std::sync::{Arc, Mutex};
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
let tx = Arc::new(Mutex::new(Some(tx)));
|
||||
let gate = Arc::new(tokio::sync::Notify::new());
|
||||
@@ -533,5 +648,465 @@ async fn stopping_rejects_calls_but_allows_notifications() {
|
||||
release.notify_one();
|
||||
assert_eq!(pending.await.unwrap().unwrap(), json!({}));
|
||||
assert_eq!(stop.await.unwrap().unwrap(), json!({}));
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 9: stop closes stdin and waits for exit.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn stop_closes_stdin_and_waits_for_exit() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let hc = h.clone();
|
||||
let pending = tokio::spawn(async move { hc.call("probe.wait_eof", json!({})).await });
|
||||
let pid = tokio::time::timeout(std::time::Duration::from_secs(20), async {
|
||||
loop {
|
||||
if let Some((pid, _)) = h
|
||||
.processes()
|
||||
.into_iter()
|
||||
.find(|(_, p)| p.method == "wait_eof")
|
||||
{
|
||||
break pid;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
call(&h, "core.stop", json!({"pid":pid})).await;
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(w.0.join("state/stopped")).unwrap(),
|
||||
"yes"
|
||||
);
|
||||
assert_eq!(pending.await.unwrap().unwrap_err()["code"], -32002);
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 9: core exit requests restart from its handler.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn core_exit_requests_restart_from_its_handler() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(call(&h, "core.exit", json!({})).await, json!({}));
|
||||
tokio::time::timeout(std::time::Duration::from_secs(1), h.exit_requested())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.3: process ids are uuidv7 and versions are sha256.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn process_ids_are_uuidv7_and_versions_are_sha256() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
use sha2::{Digest, Sha256};
|
||||
let pid = call(&h, "probe.serve", json!({"version":1})).await;
|
||||
let pid = pid.as_str().unwrap();
|
||||
let info = h.processes()[pid].clone();
|
||||
assert_eq!(
|
||||
info.version,
|
||||
format!("{:x}", Sha256::digest(std::fs::read(fixture()).unwrap()))
|
||||
);
|
||||
assert_eq!(info.plugin, "probe");
|
||||
assert_eq!(info.method, "serve");
|
||||
assert_eq!(info.params, json!({"version":1}));
|
||||
assert_eq!(info.parent.as_deref(), Some("console"));
|
||||
assert_eq!(info.state, "running");
|
||||
assert!(!info.detached);
|
||||
assert!(info.start_time > 0);
|
||||
let uuid = uuid::Uuid::parse_str(pid.strip_prefix("probe-").unwrap()).unwrap();
|
||||
assert_eq!(uuid.get_version_num(), 7);
|
||||
call(&h, "core.stop", json!({"pid":pid})).await;
|
||||
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 7: journal filters and entries persist in sqlite.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn journal_filters_and_entries_persist_in_sqlite() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let pid = call(&h, "probe.serve", json!({})).await;
|
||||
call(&h, "core.stop", json!({"pid":pid})).await;
|
||||
let entries = call(
|
||||
&h,
|
||||
"core.log",
|
||||
json!({"plugin":"probe","pid":pid,"limit":2}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(entries.as_array().unwrap().len(), 2);
|
||||
let entry = &entries[0];
|
||||
let id = &entry["id"];
|
||||
let single = call(&h, "core.log", json!({"since":id,"until":id})).await;
|
||||
assert_eq!(single, json!([entry]));
|
||||
h.stop().await;
|
||||
drop(h);
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
call(&h, "core.log", json!({"since":id,"until":id})).await,
|
||||
single
|
||||
);
|
||||
h.stop().await;
|
||||
assert!(
|
||||
std::fs::read(w.0.join("state/core/journal.sqlite"))
|
||||
.unwrap()
|
||||
.starts_with(b"SQLite format 3")
|
||||
);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.1: blank stdout lines do not warn.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn blank_stdout_lines_do_not_warn() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(&h, "probe.blank", json!({})).await;
|
||||
h.stop().await;
|
||||
let warnings = call(&h, "core.log", json!({"grep":"discarded stdout"})).await;
|
||||
assert_eq!(warnings, json!([]));
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.1: large requests and responses cross pipe capacity.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn large_requests_and_responses_cross_pipe_capacity() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let params = json!({"text":"x".repeat(192*1024)});
|
||||
assert_eq!(call(&h, "probe.echo", params.clone()).await, params);
|
||||
let outgoing = json!({"method":"probe.echo","params":params});
|
||||
assert_eq!(call(&h, "probe.call", outgoing).await, params);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.8: two plugins execute calls at the same time.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn two_plugins_execute_calls_at_the_same_time() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let other = w.0.join("plugins/other");
|
||||
std::fs::create_dir_all(&other).unwrap();
|
||||
std::fs::copy(fixture(), other.join("plugin.wasm")).unwrap();
|
||||
let hc = h.clone();
|
||||
let slow = tokio::spawn(async move { hc.call("probe.sleep", json!({"ms":1500})).await });
|
||||
while !h.processes().values().any(|p| p.method == "sleep") {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
assert_eq!(
|
||||
call(&h, "other.echo", json!({"ready":true})).await,
|
||||
json!({"ready":true})
|
||||
);
|
||||
assert!(!slow.is_finished());
|
||||
slow.await.unwrap().unwrap();
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.3: request id is the process id.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn request_id_is_the_process_id() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let pid = call(&h, "probe.request_id", json!({})).await;
|
||||
let uuid =
|
||||
uuid::Uuid::parse_str(pid.as_str().unwrap().strip_prefix("probe-").unwrap()).unwrap();
|
||||
assert_eq!(uuid.get_version_num(), 7);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 9: core call returns the target result.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn core_call_returns_the_target_result() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
call(
|
||||
&h,
|
||||
"core.call",
|
||||
json!({"method":"probe.echo","params":{"n":1}})
|
||||
)
|
||||
.await,
|
||||
json!({"n":1})
|
||||
);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 9: core unlock releases the named lock.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn core_unlock_releases_the_named_lock() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(&h, "core.lock", json!({"path":"unlock"})).await;
|
||||
call(&h, "core.unlock", json!({"path":"unlock"})).await;
|
||||
assert_eq!(
|
||||
call(&h, "core.lock", json!({"path":"unlock","wait":false})).await,
|
||||
json!({"held":true})
|
||||
);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 6: locks do not block file writes.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn locks_do_not_block_file_writes() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(&h, "core.lock", json!({"path":"/state/advisory"})).await;
|
||||
call(
|
||||
&h,
|
||||
"probe.write",
|
||||
json!({"path":"/state/advisory","text":"allowed"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(w.0.join("state/advisory")).unwrap(),
|
||||
"allowed"
|
||||
);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 6: crashes release all process locks.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn crashes_release_all_process_locks() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
h.register_native(
|
||||
"crash",
|
||||
std::sync::Arc::new(|ctx, _, _| {
|
||||
Box::pin(async move {
|
||||
ctx.call("core.lock", json!({"path":"crash"}))
|
||||
.await
|
||||
.unwrap();
|
||||
panic!("test crash");
|
||||
})
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
h.call("crash.run", json!({})).await.unwrap_err()["code"],
|
||||
-32002
|
||||
);
|
||||
assert_eq!(
|
||||
call(&h, "core.lock", json!({"path":"crash","wait":false})).await,
|
||||
json!({"held":true})
|
||||
);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 7: journal entries have uuid time identity stream and line.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn journal_entries_have_uuid_time_identity_stream_and_line() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(&h, "probe.junk", json!({})).await;
|
||||
h.stop().await;
|
||||
let entries = call(&h, "core.log", json!({"plugin":"probe"})).await;
|
||||
for entry in entries.as_array().unwrap() {
|
||||
assert_eq!(
|
||||
uuid::Uuid::parse_str(entry["id"].as_str().unwrap())
|
||||
.unwrap()
|
||||
.get_version_num(),
|
||||
7
|
||||
);
|
||||
assert!(entry["time"].as_str().unwrap().contains('T'));
|
||||
assert!(entry["time"].as_str().unwrap().ends_with('Z'));
|
||||
assert!(entry["pid"].as_str().unwrap().starts_with("probe-"));
|
||||
assert!(["stderr", "core"].contains(&entry["stream"].as_str().unwrap()));
|
||||
assert!(entry["line"].is_string());
|
||||
}
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 7: journal has wal and identity indexes.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn journal_has_wal_and_identity_indexes() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
h.stop().await;
|
||||
let db = rusqlite::Connection::open(w.0.join("state/core/journal.sqlite")).unwrap();
|
||||
assert_eq!(
|
||||
db.query_row("PRAGMA journal_mode", [], |r| r.get::<_, String>(0))
|
||||
.unwrap(),
|
||||
"wal"
|
||||
);
|
||||
for name in ["journal_pid", "journal_plugin"] {
|
||||
assert_eq!(
|
||||
db.query_row(
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='index' AND name=?1",
|
||||
[name],
|
||||
|r| r.get::<_, i64>(0)
|
||||
)
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
}
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 9: core exit works through core call.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn core_exit_works_through_core_call() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
call(&h, "core.call", json!({"method":"core.exit","params":{}})).await,
|
||||
json!({})
|
||||
);
|
||||
assert!(h.restart_requested());
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 6: acquisitions from different processes keep their FIFO positions.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn lock_acquisitions_from_different_processes_are_fifo() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let gate = std::sync::Arc::new(tokio::sync::Semaphore::new(0));
|
||||
let release = gate.clone();
|
||||
h.register_native(
|
||||
"queued",
|
||||
std::sync::Arc::new(move |ctx, _, _| {
|
||||
let tx = tx.clone();
|
||||
let gate = gate.clone();
|
||||
Box::pin(async move {
|
||||
tx.send(ctx).unwrap();
|
||||
gate.acquire().await.unwrap().forget();
|
||||
Ok(json!({}))
|
||||
})
|
||||
}),
|
||||
);
|
||||
let first_host = h.clone();
|
||||
let first = tokio::spawn(async move { first_host.call("queued.wait", json!({})).await });
|
||||
let second_host = h.clone();
|
||||
let second = tokio::spawn(async move { second_host.call("queued.wait", json!({})).await });
|
||||
let first_ctx = rx.recv().await.unwrap();
|
||||
let second_ctx = rx.recv().await.unwrap();
|
||||
call(&h, "core.lock", json!({"path":"ordered"})).await;
|
||||
let mut first_lock = Box::pin(first_ctx.call("core.lock", json!({"path":"ordered"})));
|
||||
let mut second_lock = Box::pin(second_ctx.call("core.lock", json!({"path":"ordered"})));
|
||||
std::future::poll_fn(|cx| {
|
||||
assert!(first_lock.as_mut().poll(cx).is_pending());
|
||||
assert!(second_lock.as_mut().poll(cx).is_pending());
|
||||
std::task::Poll::Ready(())
|
||||
})
|
||||
.await;
|
||||
call(&h, "core.unlock", json!({"path":"ordered"})).await;
|
||||
assert_eq!(first_lock.await.unwrap(), json!({"held":true}));
|
||||
std::future::poll_fn(|cx| {
|
||||
assert!(second_lock.as_mut().poll(cx).is_pending());
|
||||
std::task::Poll::Ready(())
|
||||
})
|
||||
.await;
|
||||
first_ctx
|
||||
.call("core.unlock", json!({"path":"ordered"}))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(second_lock.await.unwrap(), json!({"held":true}));
|
||||
release.add_permits(2);
|
||||
first.await.unwrap().unwrap();
|
||||
second.await.unwrap().unwrap();
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.5: notifications after the result do not reach the caller.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn notifications_after_the_result_are_dropped() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
let tx = std::sync::Mutex::new(Some(tx));
|
||||
h.register_native(
|
||||
"native",
|
||||
std::sync::Arc::new(move |ctx, _, _| {
|
||||
tx.lock().unwrap().take().unwrap().send(ctx).ok();
|
||||
Box::pin(async { Ok(json!({})) })
|
||||
}),
|
||||
);
|
||||
let mut notifications = h.subscribe();
|
||||
call(&h, "native.answer", json!({})).await;
|
||||
let ctx = rx.await.unwrap();
|
||||
ctx.notify("late", json!({}));
|
||||
assert!(matches!(
|
||||
notifications.try_recv(),
|
||||
Err(tokio::sync::broadcast::error::TryRecvError::Empty)
|
||||
));
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.1: non-UTF-8 stdout is discarded, not treated as blank framing.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn non_utf8_stdout_is_not_blank_framing() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(&h, "probe.invalid_utf8", json!({})).await;
|
||||
h.stop().await;
|
||||
let warnings = call(&h, "core.log", json!({"grep":"discarded stdout"})).await;
|
||||
assert_eq!(warnings.as_array().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
/// PROTOCOL 8: a failed service enters the journal and does not prevent later services.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn failed_boot_services_are_logged_and_boot_continues() {
|
||||
let w = TestWorld::new();
|
||||
let services = json!([
|
||||
{"method":"absent.start","params":{}},
|
||||
{"method":"probe.write","params":{"path":"/state/continued","text":"yes"}}
|
||||
]);
|
||||
std::fs::write(
|
||||
w.0.join("config/boot.json"),
|
||||
serde_json::to_vec(&services).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(w.0.join("state/continued")).unwrap(),
|
||||
"yes"
|
||||
);
|
||||
let errors = call(
|
||||
&h,
|
||||
"core.log",
|
||||
json!({"plugin":"core","grep":"no such plugin"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(errors.as_array().unwrap().len(), 1);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 8: boot services are detached and remain alive after their readiness result.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn boot_services_are_detached() {
|
||||
let w = TestWorld::new();
|
||||
std::fs::write(
|
||||
w.0.join("config/boot.json"),
|
||||
br#"[{"method":"probe.serve","params":{}}]"#,
|
||||
)
|
||||
.unwrap();
|
||||
let h = w.start().await;
|
||||
let processes = h.processes();
|
||||
assert_eq!(processes.len(), 1);
|
||||
assert!(processes.values().next().unwrap().detached);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 7: the core records start parameters, stop, kill, and exit status.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn core_lifetime_events_enter_the_journal() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let stopped = call(&h, "probe.serve", json!({"n":1})).await;
|
||||
call(&h, "core.stop", json!({"pid":stopped})).await;
|
||||
let killed = call(&h, "probe.serve", json!({"n":2})).await;
|
||||
call(&h, "core.kill", json!({"pid":killed})).await;
|
||||
let entries = call(&h, "core.log", json!({"plugin":"probe","limit":100})).await;
|
||||
let lines: Vec<_> = entries
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|entry| entry["line"].as_str().unwrap())
|
||||
.collect();
|
||||
assert!(lines.contains(&"start serve {\"n\":1}"));
|
||||
for line in ["stop", "kill", "exit 0", "killed"] {
|
||||
assert!(lines.contains(&line), "missing {line}");
|
||||
}
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
@@ -1,47 +1,219 @@
|
||||
//! A raw JSON-RPC guest for host integration tests.
|
||||
use std::io::{self,BufRead,Write};
|
||||
use serde_json::{Value,json};
|
||||
fn send(v:Value) { let mut out=io::stdout().lock();writeln!(out,"\n{v}").unwrap();out.flush().unwrap(); }
|
||||
//! Raw JSON-RPC methods used by the host's protocol tests.
|
||||
use serde_json::{Value, json};
|
||||
use std::io::{self, BufRead, Read, Write};
|
||||
type Lines = io::Lines<io::BufReader<io::Stdin>>;
|
||||
|
||||
fn send(value: Value) {
|
||||
let mut output = io::stdout().lock();
|
||||
writeln!(output, "\n{value}").unwrap();
|
||||
output.flush().unwrap();
|
||||
}
|
||||
fn receive(lines: &mut Lines) -> Value {
|
||||
serde_json::from_str(&lines.next().unwrap().unwrap()).unwrap()
|
||||
}
|
||||
fn echo(params: &Value) -> Value {
|
||||
params.clone()
|
||||
}
|
||||
fn environment() -> Value {
|
||||
json!({
|
||||
"pid": std::env::var("SAUSAGE_PID").unwrap(),
|
||||
"plugin": std::env::var("SAUSAGE_PLUGIN").unwrap(),
|
||||
"parent": std::env::var("SAUSAGE_PARENT").unwrap(),
|
||||
})
|
||||
}
|
||||
fn multicall(lines: &mut Lines) -> Value {
|
||||
send(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "probe.echo",
|
||||
"params": {"n": 1}
|
||||
}));
|
||||
send(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": "1",
|
||||
"method": "probe.echo",
|
||||
"params": {"n": 2}
|
||||
}));
|
||||
for _ in 0..2 {
|
||||
let response = receive(lines);
|
||||
if response["id"] == 1 {
|
||||
assert_eq!(response["result"]["n"], 1);
|
||||
} else {
|
||||
assert_eq!(response["id"], "1");
|
||||
assert_eq!(response["result"]["n"], 2);
|
||||
}
|
||||
}
|
||||
json!({"count":2})
|
||||
}
|
||||
fn notification(params: &Value) -> Value {
|
||||
send(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "probe.write",
|
||||
"params": params
|
||||
}));
|
||||
for _ in 0..200 {
|
||||
let contents = std::fs::read_to_string(params["path"].as_str().unwrap()).ok();
|
||||
if contents.as_deref() == params["text"].as_str() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
json!({})
|
||||
}
|
||||
fn unknown_id() {
|
||||
send(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": "late",
|
||||
"result": 7
|
||||
}));
|
||||
}
|
||||
fn wait_eof(lines: &mut Lines) {
|
||||
for _ in lines {}
|
||||
std::fs::write("/state/stopped", "yes").unwrap();
|
||||
}
|
||||
fn busy() -> Value {
|
||||
loop {
|
||||
std::hint::spin_loop();
|
||||
}
|
||||
}
|
||||
fn sleep(params: &Value) -> Value {
|
||||
let duration = std::time::Duration::from_millis(params["ms"].as_u64().unwrap_or(100));
|
||||
std::thread::sleep(duration);
|
||||
json!({})
|
||||
}
|
||||
fn exit_without_answer() {
|
||||
eprintln!("last line");
|
||||
}
|
||||
fn junk() -> Value {
|
||||
io::stdout().write_all(&[255, 10]).unwrap();
|
||||
println!("junk\n{{}}\n[]");
|
||||
eprintln!("stderr line");
|
||||
json!({})
|
||||
}
|
||||
fn invalid_utf8() -> Value {
|
||||
io::stdout().write_all(&[255, 10]).unwrap();
|
||||
json!({})
|
||||
}
|
||||
fn blank() -> Value {
|
||||
println!("\n \t\n");
|
||||
json!({})
|
||||
}
|
||||
fn notify() -> Value {
|
||||
send(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "core.notify",
|
||||
"params": {
|
||||
"name": "progress",
|
||||
"params": {"n": 1}
|
||||
}
|
||||
}));
|
||||
std::thread::sleep(std::time::Duration::from_millis(30));
|
||||
json!({})
|
||||
}
|
||||
fn serve(request: &Value, lines: &mut Lines) {
|
||||
send(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": request["id"],
|
||||
"result": std::env::var("SAUSAGE_PID").unwrap()
|
||||
}));
|
||||
for _ in lines {}
|
||||
}
|
||||
fn network(params: &Value) -> Value {
|
||||
let mut socket = std::net::TcpStream::connect(params["tcp"].as_str().unwrap()).unwrap();
|
||||
let mut text = String::new();
|
||||
socket.read_to_string(&mut text).unwrap();
|
||||
let udp = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
udp.send_to(b"udp", params["udp"].as_str().unwrap())
|
||||
.unwrap();
|
||||
json!(text)
|
||||
}
|
||||
fn read_file(params: &Value) -> Value {
|
||||
json!(std::fs::read_to_string(params["path"].as_str().unwrap()).unwrap())
|
||||
}
|
||||
fn write_file(params: &Value) -> Value {
|
||||
std::fs::write(
|
||||
params["path"].as_str().unwrap(),
|
||||
params["text"].as_str().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
json!({})
|
||||
}
|
||||
fn outgoing_call(params: &Value, lines: &mut Lines) -> Value {
|
||||
send(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": {"nested": 7},
|
||||
"method": params["method"],
|
||||
"params": params["params"]
|
||||
}));
|
||||
loop {
|
||||
let response = receive(lines);
|
||||
if response["id"] == json!({"nested":7}) {
|
||||
return response["result"].clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
fn lock(params: &Value, lines: &mut Lines) -> Value {
|
||||
outgoing_call(&json!({"method":"core.lock", "params":params}), lines)
|
||||
}
|
||||
fn dispatch(request: &Value, lines: &mut Lines) -> Option<Value> {
|
||||
let params = &request["params"];
|
||||
let result = match request["method"].as_str().unwrap() {
|
||||
"echo" | "rpc.discover" => echo(params),
|
||||
"env" => environment(),
|
||||
"multicall" => multicall(lines),
|
||||
"notification" => notification(params),
|
||||
"unknownid" => {
|
||||
unknown_id();
|
||||
return None;
|
||||
}
|
||||
"request_id" => request["id"].clone(),
|
||||
"wait_eof" => {
|
||||
wait_eof(lines);
|
||||
return None;
|
||||
}
|
||||
"busy" => busy(),
|
||||
"sleep" => sleep(params),
|
||||
"exit" => {
|
||||
exit_without_answer();
|
||||
return None;
|
||||
}
|
||||
"junk" => junk(),
|
||||
"blank" => blank(),
|
||||
"invalid_utf8" => invalid_utf8(),
|
||||
"notify" => notify(),
|
||||
"serve" => {
|
||||
serve(request, lines);
|
||||
return None;
|
||||
}
|
||||
"network" => network(params),
|
||||
"read" => read_file(params),
|
||||
"write" => write_file(params),
|
||||
"call" => outgoing_call(params, lines),
|
||||
"lock" => lock(params, lines),
|
||||
_ => {
|
||||
send(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": request["id"],
|
||||
"error": {
|
||||
"code": -32601,
|
||||
"message": "method not found"
|
||||
}
|
||||
}));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(result)
|
||||
}
|
||||
fn main() {
|
||||
let mut lines=io::stdin().lock().lines();
|
||||
let request:Value=serde_json::from_str(&lines.next().unwrap().unwrap()).unwrap();
|
||||
let id=request.get("id").cloned();let p=&request["params"];
|
||||
let result=match request["method"].as_str().unwrap() {
|
||||
"echo"|"rpc.discover"=>p.clone(),
|
||||
"env"=>json!({"pid":std::env::var("SAUSAGE_PID").unwrap(),"plugin":std::env::var("SAUSAGE_PLUGIN").unwrap(),"parent":std::env::var("SAUSAGE_PARENT").unwrap()}),
|
||||
"multicall"=>{
|
||||
send(json!({"jsonrpc":"2.0","id":1,"method":"probe.echo","params":{"n":1}}));
|
||||
send(json!({"jsonrpc":"2.0","id":"1","method":"probe.echo","params":{"n":2}}));
|
||||
let mut seen=Vec::new();while seen.len()<2 {let r:Value=serde_json::from_str(&lines.next().unwrap().unwrap()).unwrap();if r["id"]==1 {assert_eq!(r["result"]["n"],1);} else {assert_eq!(r["id"],"1");assert_eq!(r["result"]["n"],2);}seen.push(r["id"].clone());}json!({"count":seen.len()})
|
||||
},
|
||||
"notification"=>{
|
||||
send(json!({"jsonrpc":"2.0","method":"probe.write","params":p}));
|
||||
for _ in 0..200 {if std::fs::exists(p["path"].as_str().unwrap()).unwrap() {break;}std::thread::sleep(std::time::Duration::from_millis(10));}json!({})
|
||||
},
|
||||
"unknownid"=>{send(json!({"jsonrpc":"2.0","id":"late","result":7}));return;},
|
||||
"request_id"=>request["id"].clone(),
|
||||
"wait_eof"=>{for _ in lines {} std::fs::write("/state/stopped","yes").unwrap(); return;},
|
||||
"busy"=>loop {std::hint::spin_loop();},
|
||||
"sleep"=>{std::thread::sleep(std::time::Duration::from_millis(p["ms"].as_u64().unwrap_or(100)));json!({})},
|
||||
"exit"=>{eprintln!("last line");return;},
|
||||
"junk"=>{io::stdout().write_all(&[255,10]).unwrap();println!("junk\n{{}}\n[]");eprintln!("stderr line");json!({})},
|
||||
"notify"=>{send(json!({"jsonrpc":"2.0","method":"core.notify","params":{"name":"progress","params":{"n":1}}}));std::thread::sleep(std::time::Duration::from_millis(30));json!({})},
|
||||
"serve"=>{send(json!({"jsonrpc":"2.0","id":id,"result":std::env::var("SAUSAGE_PID").unwrap()}));for _ in lines {} return;},
|
||||
"network"=>{
|
||||
use std::io::Read;
|
||||
let mut socket=std::net::TcpStream::connect(p["tcp"].as_str().unwrap()).unwrap();
|
||||
let mut text=String::new();socket.read_to_string(&mut text).unwrap();
|
||||
let udp=std::net::UdpSocket::bind("127.0.0.1:0").unwrap();udp.send_to(b"udp",p["udp"].as_str().unwrap()).unwrap();json!(text)
|
||||
},
|
||||
"read"=>json!(std::fs::read_to_string(p["path"].as_str().unwrap()).unwrap()),
|
||||
"write"=>{std::fs::write(p["path"].as_str().unwrap(),p["text"].as_str().unwrap()).unwrap();json!({})},
|
||||
"call"|"lock"=>{
|
||||
let method=if request["method"]=="lock" {"core.lock"} else {p["method"].as_str().unwrap()};
|
||||
let params=if request["method"]=="lock" {p.clone()} else {p["params"].clone()};
|
||||
send(json!({"jsonrpc":"2.0","id":{"nested":7},"method":method,"params":params}));
|
||||
loop {let response:Value=serde_json::from_str(&lines.next().unwrap().unwrap()).unwrap();if response["id"]==json!({"nested":7}) {break response["result"].clone();}}
|
||||
},
|
||||
_=>{send(json!({"jsonrpc":"2.0","id":id,"error":{"code":-32601,"message":"method not found"}}));return;}
|
||||
};
|
||||
if let Some(id)=id {send(json!({"jsonrpc":"2.0","id":id,"result":result}));}
|
||||
let mut lines = io::BufReader::new(io::stdin()).lines();
|
||||
let request = receive(&mut lines);
|
||||
if let Some(result) = dispatch(&request, &mut lines)
|
||||
&& let Some(id) = request.get("id")
|
||||
{
|
||||
send(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"result": result
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user