refactor: route CLI through protocol dispatcher
Replace the CLI's direct ImageApi dependency with protocol commands dispatched through runa-server. Render typed protocol replies for behavior, actor, event, lifecycle, and scheduler operations, preserving the interactive workflow while exercising the same contract used by the local JSON-lines transport. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
3146423f78
commit
ce7bb5602a
|
|
@ -62,6 +62,8 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Interactive CLI commands now use the versioned protocol dispatcher instead
|
||||||
|
of calling the image API directly.
|
||||||
- Actor implementation IDs now identify compiled source/configuration content
|
- Actor implementation IDs now identify compiled source/configuration content
|
||||||
rather than using sequential process-local numbers.
|
rather than using sequential process-local numbers.
|
||||||
- Expanded the architecture specification with crate ownership and dependency
|
- Expanded the architecture specification with crate ownership and dependency
|
||||||
|
|
|
||||||
|
|
@ -392,12 +392,8 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
name = "runa"
|
name = "runa"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"runa-actor",
|
"runa-protocol",
|
||||||
"runa-code",
|
"runa-server",
|
||||||
"runa-core",
|
|
||||||
"runa-image-api",
|
|
||||||
"runa-rune",
|
|
||||||
"runa-runtime",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ registry, and the CLI exposes publishing, history, rollback, safe-boundary
|
||||||
installation, pause/resume, and cancellation through a trusted in-process image
|
installation, pause/resume, and cancellation through a trusted in-process image
|
||||||
API. A versioned transport-neutral protocol contract is now defined, but no
|
API. A versioned transport-neutral protocol contract is now defined, but no
|
||||||
network listener or authentication is implemented. The dispatcher is available
|
network listener or authentication is implemented. The dispatcher is available
|
||||||
through a local JSON-lines stdio process, with a bounded request size.
|
through a local JSON-lines stdio process, with a bounded request size. The
|
||||||
|
interactive CLI now uses the versioned protocol and dispatcher rather than
|
||||||
|
calling the image API directly.
|
||||||
|
|
||||||
The prototype uses worker-local invocation stacks and cooperative instruction
|
The prototype uses worker-local invocation stacks and cooperative instruction
|
||||||
slices, and actor snapshots report basic active-execution progress. Native
|
slices, and actor snapshots report basic active-execution progress. Native
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,5 @@ version = "0.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
runa-actor = { path = "../runa-actor" }
|
runa-protocol = { path = "../runa-protocol" }
|
||||||
runa-code = { path = "../runa-code" }
|
runa-server = { path = "../runa-server" }
|
||||||
runa-core = { path = "../runa-core" }
|
|
||||||
runa-image-api = { path = "../runa-image-api" }
|
|
||||||
runa-rune = { path = "../runa-rune" }
|
|
||||||
runa-runtime = { path = "../runa-runtime" }
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,26 @@
|
||||||
use std::io::{self, BufRead, Write};
|
use std::io::{self, BufRead, Write};
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use runa_actor::{ActorId, Message};
|
use runa_protocol::{Command, MessageDto, PROTOCOL_VERSION, Reply, Request, Response};
|
||||||
use runa_image_api::ImageApi;
|
use runa_server::Server;
|
||||||
use runa_rune::ExperimentResult;
|
|
||||||
|
|
||||||
const MAILBOX_CAPACITY: usize = 64;
|
const MAILBOX_CAPACITY: usize = 64;
|
||||||
const DEFAULT_TICK_BUDGET: usize = 32;
|
const DEFAULT_TICK_BUDGET: usize = 32;
|
||||||
const DEFAULT_BEHAVIOR: &str = "counter";
|
const DEFAULT_BEHAVIOR: &str = "counter";
|
||||||
|
|
||||||
fn main() -> ExperimentResult<()> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut image = ImageApi::new();
|
let mut server = Server::new();
|
||||||
let behavior = image.publish_behavior(
|
let behavior = match dispatch(
|
||||||
DEFAULT_BEHAVIOR,
|
&mut server,
|
||||||
None,
|
Command::PublishBehavior {
|
||||||
include_str!("../../../runa-std/actors/counter.rn"),
|
name: DEFAULT_BEHAVIOR.to_owned(),
|
||||||
)?;
|
expected_revision: None,
|
||||||
|
source: include_str!("../../../runa-std/actors/counter.rn").to_owned(),
|
||||||
|
},
|
||||||
|
)? {
|
||||||
|
Reply::Behavior(behavior) => behavior,
|
||||||
|
other => return Err(format!("unexpected publish response: {other:?}").into()),
|
||||||
|
};
|
||||||
|
|
||||||
println!("Runa — Rune Actors");
|
println!("Runa — Rune Actors");
|
||||||
println!(
|
println!(
|
||||||
|
|
@ -26,7 +31,7 @@ fn main() -> ExperimentResult<()> {
|
||||||
run_repl(
|
run_repl(
|
||||||
io::stdin().lock(),
|
io::stdin().lock(),
|
||||||
io::stdout().lock(),
|
io::stdout().lock(),
|
||||||
&mut image,
|
&mut server,
|
||||||
DEFAULT_BEHAVIOR,
|
DEFAULT_BEHAVIOR,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -35,7 +40,7 @@ fn main() -> ExperimentResult<()> {
|
||||||
fn run_repl<R: BufRead, W: Write>(
|
fn run_repl<R: BufRead, W: Write>(
|
||||||
mut input: R,
|
mut input: R,
|
||||||
mut output: W,
|
mut output: W,
|
||||||
image: &mut ImageApi,
|
server: &mut Server,
|
||||||
default_behavior: &str,
|
default_behavior: &str,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
|
|
@ -54,33 +59,48 @@ fn run_repl<R: BufRead, W: Write>(
|
||||||
let args: Vec<_> = words.collect();
|
let args: Vec<_> = words.collect();
|
||||||
let result = match command {
|
let result = match command {
|
||||||
"help" => print_help(&mut output),
|
"help" => print_help(&mut output),
|
||||||
"spawn" => spawn_command(&mut output, &args, default_behavior, image),
|
"spawn" => spawn_command(&mut output, &args, default_behavior, server),
|
||||||
"behaviors" if args.is_empty() => {
|
"behaviors" if args.is_empty() => match dispatch(server, Command::GetBehaviors) {
|
||||||
for behavior in image.behaviors() {
|
Ok(Reply::Behaviors(behaviors)) => {
|
||||||
|
for behavior in behaviors {
|
||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
"{} revision {} ({})",
|
"{} revision {} ({})",
|
||||||
behavior.name, behavior.revision, behavior.current
|
behavior.name, behavior.revision, behavior.current_implementation_id
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Ok(other) => unexpected_reply(&mut output, other),
|
||||||
|
Err(error) => writeln!(output, "error: {error}"),
|
||||||
|
},
|
||||||
"behaviors" => writeln!(output, "error: usage: behaviors"),
|
"behaviors" => writeln!(output, "error: usage: behaviors"),
|
||||||
"publish" => publish_command(&mut output, &args, image),
|
"publish" => publish_command(&mut output, &args, server),
|
||||||
"history" => history_command(&mut output, &args, image),
|
"history" => history_command(&mut output, &args, server),
|
||||||
"rollback" => rollback_command(&mut output, &args, image),
|
"rollback" => rollback_command(&mut output, &args, server),
|
||||||
"install" => install_command(&mut output, &args, image),
|
"install" => install_command(&mut output, &args, server),
|
||||||
"actors" if args.is_empty() => {
|
"actors" if args.is_empty() => match dispatch(server, Command::ListActors) {
|
||||||
for actor in image.actors() {
|
Ok(Reply::Actors(actors)) => {
|
||||||
|
for actor in actors {
|
||||||
writeln!(output, "{actor:#?}")?;
|
writeln!(output, "{actor:#?}")?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Ok(other) => unexpected_reply(&mut output, other),
|
||||||
|
Err(error) => writeln!(output, "error: {error}"),
|
||||||
|
},
|
||||||
"actors" => writeln!(output, "error: usage: actors"),
|
"actors" => writeln!(output, "error: usage: actors"),
|
||||||
"send" => match parse_send_args(&args) {
|
"send" => match parse_send_args(&args) {
|
||||||
Ok((actor, kind, value)) => {
|
Ok((actor, kind, value)) => {
|
||||||
match image.send(ActorId(actor), Message { kind, value }) {
|
match dispatch(
|
||||||
Ok(()) => writeln!(output, "message queued"),
|
server,
|
||||||
|
Command::Send {
|
||||||
|
actor_id: actor,
|
||||||
|
message: MessageDto { kind, value },
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(Reply::Acknowledged) => writeln!(output, "message queued"),
|
||||||
|
Ok(other) => unexpected_reply(&mut output, other),
|
||||||
Err(error) => writeln!(output, "error: {error}"),
|
Err(error) => writeln!(output, "error: {error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -88,32 +108,56 @@ fn run_repl<R: BufRead, W: Write>(
|
||||||
},
|
},
|
||||||
"tick" => match parse_tick_args(&args) {
|
"tick" => match parse_tick_args(&args) {
|
||||||
Ok((count, budget)) => {
|
Ok((count, budget)) => {
|
||||||
image.tick(count, budget);
|
match dispatch(
|
||||||
writeln!(output, "ran {count} scheduler tick(s)")
|
server,
|
||||||
|
Command::Tick {
|
||||||
|
ticks: count,
|
||||||
|
instruction_budget: budget.get(),
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(Reply::TickCompleted { ticks }) => {
|
||||||
|
writeln!(output, "ran {ticks} scheduler tick(s)")
|
||||||
|
}
|
||||||
|
Ok(other) => unexpected_reply(&mut output, other),
|
||||||
|
Err(error) => writeln!(output, "error: {error}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(error) => writeln!(output, "error: {error}"),
|
Err(error) => writeln!(output, "error: {error}"),
|
||||||
},
|
},
|
||||||
"inspect" => match parse_exact::<u64, 1>(&args) {
|
"inspect" => match parse_exact::<u64, 1>(&args) {
|
||||||
Ok([id]) => match image.inspect(ActorId(id)) {
|
Ok([id]) => match dispatch(server, Command::InspectActor { actor_id: id }) {
|
||||||
Ok(actor) => writeln!(output, "{actor:#?}"),
|
Ok(Reply::Actor(actor)) => writeln!(output, "{actor:#?}"),
|
||||||
|
Ok(other) => unexpected_reply(&mut output, other),
|
||||||
Err(error) => writeln!(output, "error: {error}"),
|
Err(error) => writeln!(output, "error: {error}"),
|
||||||
},
|
},
|
||||||
Err(error) => writeln!(output, "error: {error}"),
|
Err(error) => writeln!(output, "error: {error}"),
|
||||||
},
|
},
|
||||||
"events" if args.is_empty() => {
|
"events" if args.is_empty() => match dispatch(server, Command::GetEvents) {
|
||||||
for event in image.events() {
|
Ok(Reply::Events(events)) => {
|
||||||
|
for event in events {
|
||||||
writeln!(output, "{event:#?}")?;
|
writeln!(output, "{event:#?}")?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Ok(other) => unexpected_reply(&mut output, other),
|
||||||
|
Err(error) => writeln!(output, "error: {error}"),
|
||||||
|
},
|
||||||
"events" => writeln!(output, "error: usage: events"),
|
"events" => writeln!(output, "error: usage: events"),
|
||||||
"stop" => lifecycle_command(&mut output, &args, |image, id| image.stop(id), image),
|
"stop" => lifecycle_command(&mut output, &args, server, |actor_id| {
|
||||||
"restart" => {
|
Command::StopActor { actor_id }
|
||||||
lifecycle_command(&mut output, &args, |image, id| image.restart(id), image)
|
}),
|
||||||
}
|
"restart" => lifecycle_command(&mut output, &args, server, |actor_id| {
|
||||||
"pause" => lifecycle_command(&mut output, &args, |image, id| image.pause(id), image),
|
Command::RestartActor { actor_id }
|
||||||
"resume" => lifecycle_command(&mut output, &args, |image, id| image.resume(id), image),
|
}),
|
||||||
"cancel" => lifecycle_command(&mut output, &args, |image, id| image.cancel(id), image),
|
"pause" => lifecycle_command(&mut output, &args, server, |actor_id| {
|
||||||
|
Command::PauseActor { actor_id }
|
||||||
|
}),
|
||||||
|
"resume" => lifecycle_command(&mut output, &args, server, |actor_id| {
|
||||||
|
Command::ResumeActor { actor_id }
|
||||||
|
}),
|
||||||
|
"cancel" => lifecycle_command(&mut output, &args, server, |actor_id| {
|
||||||
|
Command::CancelActor { actor_id }
|
||||||
|
}),
|
||||||
"quit" | "exit" if args.is_empty() => break,
|
"quit" | "exit" if args.is_empty() => break,
|
||||||
"quit" | "exit" => writeln!(output, "error: usage: quit"),
|
"quit" | "exit" => writeln!(output, "error: usage: quit"),
|
||||||
_ => writeln!(output, "error: unknown command `{command}`; type `help`"),
|
_ => writeln!(output, "error: unknown command `{command}`; type `help`"),
|
||||||
|
|
@ -126,12 +170,13 @@ fn run_repl<R: BufRead, W: Write>(
|
||||||
fn lifecycle_command<W: Write>(
|
fn lifecycle_command<W: Write>(
|
||||||
output: &mut W,
|
output: &mut W,
|
||||||
args: &[&str],
|
args: &[&str],
|
||||||
operation: impl FnOnce(&mut ImageApi, ActorId) -> ExperimentResult<()>,
|
server: &mut Server,
|
||||||
image: &mut ImageApi,
|
operation: impl FnOnce(u64) -> Command,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
match parse_exact::<u64, 1>(args) {
|
match parse_exact::<u64, 1>(args) {
|
||||||
Ok([id]) => match operation(image, ActorId(id)) {
|
Ok([id]) => match dispatch(server, operation(id)) {
|
||||||
Ok(()) => writeln!(output, "ok"),
|
Ok(Reply::Acknowledged) => writeln!(output, "ok"),
|
||||||
|
Ok(other) => unexpected_reply(output, other),
|
||||||
Err(error) => writeln!(output, "error: {error}"),
|
Err(error) => writeln!(output, "error: {error}"),
|
||||||
},
|
},
|
||||||
Err(error) => writeln!(output, "error: {error}"),
|
Err(error) => writeln!(output, "error: {error}"),
|
||||||
|
|
@ -142,7 +187,7 @@ fn spawn_command<W: Write>(
|
||||||
output: &mut W,
|
output: &mut W,
|
||||||
args: &[&str],
|
args: &[&str],
|
||||||
default_behavior: &str,
|
default_behavior: &str,
|
||||||
image: &mut ImageApi,
|
server: &mut Server,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let (state_arg, behavior_name) = match args {
|
let (state_arg, behavior_name) = match args {
|
||||||
[state] => (*state, default_behavior),
|
[state] => (*state, default_behavior),
|
||||||
|
|
@ -158,28 +203,37 @@ fn spawn_command<W: Write>(
|
||||||
Ok(state) => state,
|
Ok(state) => state,
|
||||||
Err(_) => return writeln!(output, "error: invalid initial state: `{state_arg}`"),
|
Err(_) => return writeln!(output, "error: invalid initial state: `{state_arg}`"),
|
||||||
};
|
};
|
||||||
match image.spawn(
|
let response = dispatch(
|
||||||
behavior_name,
|
server,
|
||||||
|
Command::Spawn {
|
||||||
|
behavior_name: behavior_name.to_owned(),
|
||||||
state,
|
state,
|
||||||
NonZeroUsize::new(MAILBOX_CAPACITY).expect("capacity is nonzero"),
|
mailbox_capacity: MAILBOX_CAPACITY,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
match response {
|
||||||
|
Ok(Reply::ActorSpawned { actor_id }) => {
|
||||||
|
let revision = match dispatch(
|
||||||
|
server,
|
||||||
|
Command::GetBehavior {
|
||||||
|
name: behavior_name.to_owned(),
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
Ok(id) => writeln!(
|
Ok(Reply::Behavior(behavior)) => behavior.revision,
|
||||||
|
Ok(other) => return unexpected_reply(output, other),
|
||||||
|
Err(error) => return writeln!(output, "error: {error}"),
|
||||||
|
};
|
||||||
|
writeln!(
|
||||||
output,
|
output,
|
||||||
"spawned {id:?} with `{behavior_name}` revision {}",
|
"spawned ActorId({actor_id}) with `{behavior_name}` revision {revision}"
|
||||||
image
|
)
|
||||||
.behavior(behavior_name)
|
}
|
||||||
.expect("spawn resolved behavior")
|
Ok(other) => unexpected_reply(output, other),
|
||||||
.revision
|
|
||||||
),
|
|
||||||
Err(error) => writeln!(output, "error: {error}"),
|
Err(error) => writeln!(output, "error: {error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn publish_command<W: Write>(
|
fn publish_command<W: Write>(output: &mut W, args: &[&str], server: &mut Server) -> io::Result<()> {
|
||||||
output: &mut W,
|
|
||||||
args: &[&str],
|
|
||||||
image: &mut ImageApi,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
let [name, expected_revision, source_path] = args else {
|
let [name, expected_revision, source_path] = args else {
|
||||||
return writeln!(
|
return writeln!(
|
||||||
output,
|
output,
|
||||||
|
|
@ -200,23 +254,38 @@ fn publish_command<W: Write>(
|
||||||
return writeln!(output, "error: could not read `{source_path}`: {error}");
|
return writeln!(output, "error: could not read `{source_path}`: {error}");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let behavior = match image.publish_behavior(name, expected_revision, &source) {
|
let behavior = match dispatch(
|
||||||
Ok(behavior) => behavior,
|
server,
|
||||||
|
Command::PublishBehavior {
|
||||||
|
name: (*name).to_owned(),
|
||||||
|
expected_revision,
|
||||||
|
source,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(Reply::Behavior(behavior)) => behavior,
|
||||||
|
Ok(other) => return unexpected_reply(output, other),
|
||||||
Err(error) => return writeln!(output, "error: {error}"),
|
Err(error) => return writeln!(output, "error: {error}"),
|
||||||
};
|
};
|
||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
"published `{}` revision {} ({})",
|
"published `{}` revision {} ({})",
|
||||||
behavior.name, behavior.revision, behavior.current
|
behavior.name, behavior.revision, behavior.current_implementation_id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn history_command<W: Write>(output: &mut W, args: &[&str], image: &ImageApi) -> io::Result<()> {
|
fn history_command<W: Write>(output: &mut W, args: &[&str], server: &mut Server) -> io::Result<()> {
|
||||||
let [name] = args else {
|
let [name] = args else {
|
||||||
return writeln!(output, "error: usage: history <behavior-name>");
|
return writeln!(output, "error: usage: history <behavior-name>");
|
||||||
};
|
};
|
||||||
let Some(behavior) = image.behavior(name) else {
|
let behavior = match dispatch(
|
||||||
return writeln!(output, "error: unknown behavior `{name}`");
|
server,
|
||||||
|
Command::GetBehavior {
|
||||||
|
name: (*name).to_owned(),
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(Reply::Behavior(behavior)) => behavior,
|
||||||
|
Ok(other) => return unexpected_reply(output, other),
|
||||||
|
Err(error) => return writeln!(output, "error: {error}"),
|
||||||
};
|
};
|
||||||
for version in behavior.history {
|
for version in behavior.history {
|
||||||
let current = if version.revision == behavior.revision {
|
let current = if version.revision == behavior.revision {
|
||||||
|
|
@ -236,7 +305,7 @@ fn history_command<W: Write>(output: &mut W, args: &[&str], image: &ImageApi) ->
|
||||||
fn rollback_command<W: Write>(
|
fn rollback_command<W: Write>(
|
||||||
output: &mut W,
|
output: &mut W,
|
||||||
args: &[&str],
|
args: &[&str],
|
||||||
image: &mut ImageApi,
|
server: &mut Server,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let [name, expected_revision, target_revision] = args else {
|
let [name, expected_revision, target_revision] = args else {
|
||||||
return writeln!(
|
return writeln!(
|
||||||
|
|
@ -252,41 +321,74 @@ fn rollback_command<W: Write>(
|
||||||
Ok(revision) => revision,
|
Ok(revision) => revision,
|
||||||
Err(_) => return writeln!(output, "error: invalid target revision"),
|
Err(_) => return writeln!(output, "error: invalid target revision"),
|
||||||
};
|
};
|
||||||
match image.rollback_behavior(name, expected_revision, target_revision) {
|
match dispatch(
|
||||||
Ok(behavior) => writeln!(
|
server,
|
||||||
|
Command::RollbackBehavior {
|
||||||
|
name: (*name).to_owned(),
|
||||||
|
expected_revision,
|
||||||
|
target_revision,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(Reply::Behavior(behavior)) => writeln!(
|
||||||
output,
|
output,
|
||||||
"rolled back `{}` to implementation {} as revision {}",
|
"rolled back `{}` to implementation {} as revision {}",
|
||||||
behavior.name, behavior.current, behavior.revision
|
behavior.name, behavior.current_implementation_id, behavior.revision
|
||||||
),
|
),
|
||||||
|
Ok(other) => unexpected_reply(output, other),
|
||||||
Err(error) => writeln!(output, "error: {error}"),
|
Err(error) => writeln!(output, "error: {error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install_command<W: Write>(
|
fn install_command<W: Write>(output: &mut W, args: &[&str], server: &mut Server) -> io::Result<()> {
|
||||||
output: &mut W,
|
|
||||||
args: &[&str],
|
|
||||||
image: &mut ImageApi,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
let [actor_id, name] = args else {
|
let [actor_id, name] = args else {
|
||||||
return writeln!(output, "error: usage: install <actor-id> <behavior-name>");
|
return writeln!(output, "error: usage: install <actor-id> <behavior-name>");
|
||||||
};
|
};
|
||||||
let actor_id = match actor_id.parse::<u64>() {
|
let actor_id = match actor_id.parse::<u64>() {
|
||||||
Ok(id) => ActorId(id),
|
Ok(id) => id,
|
||||||
Err(_) => return writeln!(output, "error: invalid actor ID"),
|
Err(_) => return writeln!(output, "error: invalid actor ID"),
|
||||||
};
|
};
|
||||||
match image.install(actor_id, name) {
|
let implementation = match dispatch(
|
||||||
Ok(implementation) => {
|
server,
|
||||||
let behavior = image.behavior(name).expect("install resolved behavior");
|
Command::InstallBehavior {
|
||||||
|
actor_id,
|
||||||
|
behavior_name: (*name).to_owned(),
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(Reply::BehaviorInstalled { implementation_id }) => implementation_id,
|
||||||
|
Ok(other) => return unexpected_reply(output, other),
|
||||||
|
Err(error) => return writeln!(output, "error: {error}"),
|
||||||
|
};
|
||||||
|
match dispatch(
|
||||||
|
server,
|
||||||
|
Command::GetBehavior {
|
||||||
|
name: (*name).to_owned(),
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(Reply::Behavior(behavior)) => {
|
||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
"installed `{}` revision {} on actor {} ({implementation})",
|
"installed `{}` revision {} on actor {} ({implementation})",
|
||||||
behavior.name, behavior.revision, actor_id.0
|
behavior.name, behavior.revision, actor_id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Ok(other) => unexpected_reply(output, other),
|
||||||
Err(error) => writeln!(output, "error: {error}"),
|
Err(error) => writeln!(output, "error: {error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dispatch(server: &mut Server, command: Command) -> Result<Reply, String> {
|
||||||
|
let response: Response = server.dispatch(Request {
|
||||||
|
version: PROTOCOL_VERSION,
|
||||||
|
request_id: "runa-cli".to_owned(),
|
||||||
|
command,
|
||||||
|
});
|
||||||
|
response.result.map_err(|error| error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unexpected_reply<W: Write>(output: &mut W, reply: Reply) -> io::Result<()> {
|
||||||
|
writeln!(output, "error: unexpected protocol response: {reply:?}")
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_exact<T: std::str::FromStr, const N: usize>(args: &[&str]) -> Result<[T; N], String> {
|
fn parse_exact<T: std::str::FromStr, const N: usize>(args: &[&str]) -> Result<[T; N], String> {
|
||||||
if args.len() != N {
|
if args.len() != N {
|
||||||
return Err(format!("expected {N} argument(s), got {}", args.len()));
|
return Err(format!("expected {N} argument(s), got {}", args.len()));
|
||||||
|
|
@ -371,38 +473,43 @@ fn print_help<W: Write>(output: &mut W) -> io::Result<()> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn image() -> ExperimentResult<ImageApi> {
|
fn server() -> Server {
|
||||||
let mut image = ImageApi::new();
|
let mut server = Server::new();
|
||||||
image.publish_behavior(
|
dispatch(
|
||||||
DEFAULT_BEHAVIOR,
|
&mut server,
|
||||||
None,
|
Command::PublishBehavior {
|
||||||
include_str!("../../../runa-std/actors/counter.rn"),
|
name: DEFAULT_BEHAVIOR.to_owned(),
|
||||||
)?;
|
expected_revision: None,
|
||||||
Ok(image)
|
source: include_str!("../../../runa-std/actors/counter.rn").to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("default behavior publishes");
|
||||||
|
server
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repl_drives_actor_messages_and_inspection() -> ExperimentResult<()> {
|
fn repl_drives_actor_messages_and_inspection() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut image = image()?;
|
let mut server = server();
|
||||||
let input = io::Cursor::new(
|
let input = io::Cursor::new(
|
||||||
"spawn 0\nspawn 10\nsend 1 1 2\ntick 100\ninspect 1\ninspect 2\nevents\nquit\n",
|
"spawn 0\nspawn 10\nsend 1 1 2\ntick 100\ninspect 1\ninspect 2\nevents\nquit\n",
|
||||||
);
|
);
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
run_repl(input, &mut output, &mut image, DEFAULT_BEHAVIOR)?;
|
run_repl(input, &mut output, &mut server, DEFAULT_BEHAVIOR)?;
|
||||||
let output = String::from_utf8(output)?;
|
let output = String::from_utf8(output)?;
|
||||||
assert!(output.contains("ActorId(\n 1,"));
|
assert!(output.contains("actor_id: 1"));
|
||||||
assert!(output.contains("ActorId(\n 2,"));
|
assert!(output.contains("actor_id: 2"));
|
||||||
assert!(output.contains("state: 15"));
|
assert!(output.contains("state: 15"));
|
||||||
assert!(output.contains("Committed"));
|
assert!(output.contains("Committed {"));
|
||||||
|
assert!(output.contains("actor_id: 2"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repl_reports_bad_input_and_continues() -> ExperimentResult<()> {
|
fn repl_reports_bad_input_and_continues() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut image = image()?;
|
let mut server = server();
|
||||||
let input = io::Cursor::new("spawn nope\nsend 999 0 1\ntick 1 0\nunknown\nquit\n");
|
let input = io::Cursor::new("spawn nope\nsend 999 0 1\ntick 1 0\nunknown\nquit\n");
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
run_repl(input, &mut output, &mut image, DEFAULT_BEHAVIOR)?;
|
run_repl(input, &mut output, &mut server, DEFAULT_BEHAVIOR)?;
|
||||||
let output = String::from_utf8(output)?;
|
let output = String::from_utf8(output)?;
|
||||||
assert!(output.contains("invalid initial state"));
|
assert!(output.contains("invalid initial state"));
|
||||||
assert!(output.contains("unknown actor"));
|
assert!(output.contains("unknown actor"));
|
||||||
|
|
@ -412,8 +519,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repl_manages_named_behavior_revisions_and_installs_them() -> ExperimentResult<()> {
|
fn repl_manages_named_behavior_revisions_and_installs_them()
|
||||||
let mut image = image()?;
|
-> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut server = server();
|
||||||
let source_path = concat!(
|
let source_path = concat!(
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
"/../../runa-std/actors/counter.rn"
|
"/../../runa-std/actors/counter.rn"
|
||||||
|
|
@ -422,7 +530,7 @@ mod tests {
|
||||||
"behaviors\npublish counter 1 {source_path}\nhistory counter\nrollback counter 2 1\nspawn 0 counter\ninstall 1 counter\nsend 1 0 1\ntick 1 1\ncancel 1\npause 1\nresume 1\ninspect 1\nevents\nquit\n"
|
"behaviors\npublish counter 1 {source_path}\nhistory counter\nrollback counter 2 1\nspawn 0 counter\ninstall 1 counter\nsend 1 0 1\ntick 1 1\ncancel 1\npause 1\nresume 1\ninspect 1\nevents\nquit\n"
|
||||||
));
|
));
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
run_repl(input, &mut output, &mut image, DEFAULT_BEHAVIOR)?;
|
run_repl(input, &mut output, &mut server, DEFAULT_BEHAVIOR)?;
|
||||||
let output = String::from_utf8(output)?;
|
let output = String::from_utf8(output)?;
|
||||||
assert!(output.contains("published `counter` revision 2"));
|
assert!(output.contains("published `counter` revision 2"));
|
||||||
assert!(output.contains("revision 1:"));
|
assert!(output.contains("revision 1:"));
|
||||||
|
|
@ -430,14 +538,24 @@ mod tests {
|
||||||
assert!(output.contains("rolled back `counter`"));
|
assert!(output.contains("rolled back `counter`"));
|
||||||
assert!(output.contains("spawned ActorId(1) with `counter` revision 3"));
|
assert!(output.contains("spawned ActorId(1) with `counter` revision 3"));
|
||||||
assert!(output.contains("installed `counter` revision 3 on actor 1"));
|
assert!(output.contains("installed `counter` revision 3 on actor 1"));
|
||||||
assert!(output.contains("Paused("));
|
assert!(output.contains("Paused {"));
|
||||||
assert!(output.contains("Resumed("));
|
assert!(output.contains("Resumed {"));
|
||||||
assert!(output.contains("InvocationCancelled("));
|
assert!(output.contains("InvocationCancelled {"));
|
||||||
assert_eq!(image.behavior(DEFAULT_BEHAVIOR).unwrap().revision, 3);
|
let behavior = match dispatch(
|
||||||
assert_eq!(
|
&mut server,
|
||||||
image.inspect(ActorId(1))?.implementation,
|
Command::GetBehavior {
|
||||||
image.behavior(DEFAULT_BEHAVIOR).unwrap().current
|
name: DEFAULT_BEHAVIOR.to_owned(),
|
||||||
);
|
},
|
||||||
|
)? {
|
||||||
|
Reply::Behavior(behavior) => behavior,
|
||||||
|
other => return Err(format!("unexpected response: {other:?}").into()),
|
||||||
|
};
|
||||||
|
assert_eq!(behavior.revision, 3);
|
||||||
|
let actor = match dispatch(&mut server, Command::InspectActor { actor_id: 1 })? {
|
||||||
|
Reply::Actor(actor) => actor,
|
||||||
|
other => return Err(format!("unexpected response: {other:?}").into()),
|
||||||
|
};
|
||||||
|
assert_eq!(actor.implementation_id, behavior.current_implementation_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -438,7 +438,8 @@ inspection remain future work.
|
||||||
|
|
||||||
The first Phase 5 increments are a trusted in-process application API used by
|
The first Phase 5 increments are a trusted in-process application API used by
|
||||||
the CLI, a transport-neutral versioned protocol contract, and a dispatcher
|
the CLI, a transport-neutral versioned protocol contract, and a dispatcher
|
||||||
exposed through bounded local JSON-lines stdio. A network server, sessions,
|
exposed through bounded local JSON-lines stdio. The interactive CLI uses the
|
||||||
|
same protocol commands and dispatcher in-process. A network server, sessions,
|
||||||
authentication, and authorization are not yet implemented.
|
authentication, and authorization are not yet implemented.
|
||||||
|
|
||||||
### Phase 6 — Graphical world
|
### Phase 6 — Graphical world
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@ The default per-actor slice budget is 32 instructions.
|
||||||
- `runa-server` dispatches those requests through `runa-image-api` and maps
|
- `runa-server` dispatches those requests through `runa-image-api` and maps
|
||||||
results to protocol replies; its executable uses newline-delimited JSON over
|
results to protocol replies; its executable uses newline-delimited JSON over
|
||||||
stdin/stdout with a 1 MiB request-line limit, not a network listener.
|
stdin/stdout with a 1 MiB request-line limit, not a network listener.
|
||||||
- `runa` provides the local CLI client for that boundary.
|
- `runa` provides an interactive CLI client using protocol requests through the
|
||||||
|
dispatcher.
|
||||||
|
|
||||||
The image API is process-local and intended for trusted callers. It has no
|
The image API is process-local and intended for trusted callers. It has no
|
||||||
transport, authentication, or authorization layer; do not expose it directly
|
transport, authentication, or authorization layer; do not expose it directly
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue