patch-cms
A Rust reimplementation of the IBM VM/CMS environment — XEDIT editor, CMS file system, REXX scripting, spool subsystem, Hartmann pipelines, and VM inter-machine messaging.
Overview
VM/CMS was IBM’s interactive mainframe operating system — a single-user virtual machine with a powerful command environment, a programmable full-screen editor (XEDIT), and REXX as its scripting language. This project recreates those semantics in modern Rust as a set of embeddable libraries with a terminal UI.
The REXX interpreter lives in a companion project, patch-rexx.
See ROADMAP.md for the full vision and current progress.
Workspace Structure
patch-cms/
├── crates/
│ ├── xedit-core/ # Editor model — pure logic, no I/O dependencies
│ ├── xedit-tui/ # Terminal UI — 3270-style block-mode rendering
│ ├── cms-core/ # CMS file system (fn ft fm), commands, EXEC processor
│ ├── cms-spool/ # Reader/punch/printer spool subsystem
│ ├── cms-pipelines/ # Hartmann pipelines
│ ├── vm-iucv/ # Inter-machine messaging (actor framework)
│ └── cms-machine/ # Interactive CMS machine binary
xedit-core is a standalone library with zero I/O dependencies. The editor is
a pure state machine driven by commands, making it embeddable in other
applications. With the rexx feature enabled, macros can query and drive the
editor via EXTRACT variables and ADDRESS XEDIT commands.
xedit-tui provides the interactive terminal experience: prefix area editing, command line, PF keys, and screen editing with overtype/insert modes.
cms-core implements the CMS file system (fn ft fm naming), command
processor with IBM-style abbreviation matching, GLOBALV variable storage, and
EXEC resolution. Trait seams (ExecHandler, SmsgSender, ExtCommandHandler)
decouple it from the REXX interpreter, actor framework, and extension commands.
cms-spool provides virtual reader/punch/printer queues with SPOOL, QUERY, PURGE, SENDFILE, and RECEIVE commands.
cms-pipelines implements Hartmann pipelines — the PIPE command with
built-in stages (literal, console, locate, nlocate) and a two-pass
executor.
vm-iucv is a Tokio-based actor framework modeled after VM/CMS inter-machine communication: Supervisor lifecycle management, SMSG messaging, and IUCV bidirectional data paths.
cms-machine wires everything together into an interactive CMS console with REXX scripting, spool commands, and pipelines — all programmable from the REPL.
Building and Running
# Build the workspace
cargo build --workspace
# Run the TUI editor
cargo run -p xedit-tui -- <filename>
# Run the interactive CMS machine
cargo run -p cms-machine -- --userid ALICE --disk /tmp/cms
# Run all tests
cargo test --workspace
Current Status
906 tests passing, zero clippy warnings.
Phases 1-13 complete. The editor is fully functional with REXX macros, CMS file system, spool subsystem, Hartmann pipelines, actor-based inter-machine messaging, and an interactive CMS console. Everything is programmable from REXX/REPL.
License
MIT — Ed Sweeney, 2026
Quickstart
Launch a CMS Machine
# Clone and build
git clone https://github.com/navicore/patch-cms.git
cd patch-cms
cargo build -p cms-machine --release
# Create a disk directory and launch
mkdir -p /tmp/cms/a
cargo run -p cms-machine -- --userid ALICE --disk /tmp/cms
You get an interactive CMS prompt with REXX scripting, spool commands, pipelines, and inter-machine messaging.
Your First REXX Program
Create a file /tmp/cms/a/GREET.exec:
/* REXX — send a greeting to another machine */
parse arg userid .
if userid = '' then do
say 'Usage: GREET userid'
exit 24
end
'SMSG' userid 'Hello from CMS!'
if rc = 0 then say 'Sent.'
else say 'Failed, RC='rc
At the CMS prompt:
GREET BOB
Any file named *.exec on your A-disk is callable as a command.
Try the Built-in Commands
GLOBALV SET COLOR blue Set a persistent variable
GLOBALV GET COLOR Retrieve it
PIPE literal hello | console Run a pipeline
SP PRT CLASS B Configure the printer spool
QUERY PRT Show printer queue
LISTFILE * EXEC A List all EXECs on A-disk
Persistent State with GLOBALV
REXX EXECs get a fresh interpreter each time, but GLOBALV variables persist across invocations:
/* COUNTER EXEC */
'GLOBALV SELECT COUNTER'
'GLOBALV GET COUNT'
if rc \= 0 then count = 0
count = count + 1
'GLOBALV SET COUNT' count
say 'Counter:' count
Run COUNTER multiple times — the value increments.
Composing EXECs
EXECs can call other EXECs:
/* DISPATCH EXEC */
do i = 1 to 3
'EXEC COUNTER'
end
'EXEC GREET BOB'
Embedding the Library (Rust API)
For embedding vm-iucv as a Rust library:
[dependencies]
vm-iucv = { git = "https://github.com/navicore/patch-cms" }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
use vm_iucv::handler::{MachineContext, MachineHandler};
use vm_iucv::machine_id::MachineId;
use vm_iucv::message::SmsgMessage;
use vm_iucv::supervisor::Supervisor;
struct PrintHandler;
impl MachineHandler for PrintHandler {
fn on_smsg(&mut self, _ctx: &MachineContext, msg: SmsgMessage) {
println!("Received: {}", msg.text());
}
}
#[tokio::main]
async fn main() {
let sup = Supervisor::new();
let alice = MachineId::new("ALICE").unwrap();
let bob = MachineId::new("BOB").unwrap();
sup.ipl(&alice, vm_iucv::collector::collector().0).await.unwrap();
sup.ipl(&bob, PrintHandler).await.unwrap();
sup.smsg(&alice, &bob, "Hello from ALICE!").await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
sup.logoff(&alice).await.unwrap();
sup.logoff(&bob).await.unwrap();
sup.shutdown().await;
}
See the Examples page for more Rust library examples (echo server, IUCV chat, connection gating, multi-machine pipeline).
Next steps
- Examples — REXX and Rust examples with walkthroughs
- vm-iucv Overview — the actor framework
- cms-core Overview — CMS command processor
- cms-machine Overview — interactive console
- API Quick Reference — method tables
Examples
REXX Examples
These examples run inside the CMS machine interactive console. Launch a machine and try them at the prompt:
mkdir -p /tmp/cms/a
cargo run -p cms-machine -- --userid ALICE --disk /tmp/cms
The example EXECs are in crates/cms-machine/execs/ and are loaded
automatically from the A-disk.
GREET — Send an SMSG
Send a greeting message to another machine. Demonstrates SMSG from REXX with return code checking.
/* GREET EXEC — at the CMS prompt: GREET BOB */
parse arg userid .
if userid = '' then do
say 'Usage: GREET userid'
exit 24
end
say 'Sending greeting to' userid '...'
'SMSG' userid 'Hello from CMS!'
if rc = 0 then
say 'Greeting sent successfully.'
else
say 'Could not reach' userid '— RC='rc
exit rc
Ready; T=0.01/0.01
GREET BOB
Sending greeting to BOB ...
Greeting sent successfully.
COUNTER — Persistent State with GLOBALV
A counter that survives across EXEC invocations. Demonstrates GLOBALV groups for persistent state management.
/* COUNTER EXEC */
parse upper arg action .
'GLOBALV SELECT COUNTER'
if action = 'RESET' then do
'GLOBALV SET COUNT 0'
say 'Counter reset to 0.'
exit 0
end
'GLOBALV GET COUNT'
if rc \= 0 then count = 0
count = count + 1
'GLOBALV SET COUNT' count
say 'Counter:' count
exit 0
Ready; T=0.01/0.01
COUNTER
Counter: 1
Ready; T=0.01/0.01
COUNTER
Counter: 2
Ready; T=0.01/0.01
COUNTER RESET
Counter reset to 0.
UPPER — Data Transformation with PIPE
Uses CMS Pipelines to transform text. Demonstrates the PIPE command
from REXX.
/* UPPER EXEC */
parse arg text
if text = '' then do
say 'Usage: UPPER some text here'
exit 24
end
'PIPE literal' text '| console'
Ready; T=0.01/0.01
UPPER hello world
hello world
SPOOLQ — Spool Queue Status
Queries the virtual reader, printer, and punch queues. Demonstrates spool commands from REXX.
/* SPOOLQ EXEC */
parse upper arg device .
if device = '' then do
say '--- Reader ---'
'QUERY RDR'
say ''
say '--- Printer ---'
'QUERY PRT'
say ''
say '--- Punch ---'
'QUERY PUN'
end
Ready; T=0.01/0.01
SPOOLQ
--- Reader ---
No spool files.
--- Printer ---
No spool files.
--- Punch ---
No spool files.
DISPATCH — Multi-EXEC Composition
Composes COUNTER, GREET, and SPOOLQ into a single workflow. Demonstrates REXX EXECs calling other EXECs.
/* DISPATCH EXEC */
parse arg userid .
say '=== Running COUNTER three times ==='
do i = 1 to 3
'EXEC COUNTER'
end
if userid \= '' then do
say ''
say '=== Sending greeting to' userid '==='
'EXEC GREET' userid
end
say ''
say '=== Spool status ==='
'EXEC SPOOLQ'
say ''
say '=== Done ==='
exit 0
Ready; T=0.01/0.01
DISPATCH BOB
=== Running COUNTER three times ===
Counter: 1
Counter: 2
Counter: 3
=== Sending greeting to BOB ===
Sending greeting to BOB ...
Greeting sent successfully.
=== Spool status ===
--- Reader ---
No spool files.
...
=== Done ===
Rust Library Examples
These examples demonstrate the vm-iucv library API for embedding
the actor framework in your own Rust programs. They require Rust and
are run with cargo:
hello_smsg
Basic machine lifecycle: IPL two machines, send an SMSG, shut down.
cargo run -p vm-iucv --example hello_smsg --features examples
echo_server
Request/reply pattern: an ECHO machine replies to every SMSG with
the same text. Uses ctx.try_send_smsg() inside a handler callback.
cargo run -p vm-iucv --example echo_server --features examples
multi_machine_pipeline
Multi-machine coordination: PRODUCER → TRANSFORM → SINK chain where each machine forwards modified messages via SMSG.
cargo run -p vm-iucv --example multi_machine_pipeline --features examples
connection_gating
IUCV connection security: a SECURE machine accepts connections only
from an allowlist. Demonstrates on_connection_pending callback.
cargo run -p vm-iucv --example connection_gating --features examples
iucv_chat
Full IUCV path lifecycle: CONNECT → ESTABLISHED → SEND → SEVER between two machines.
cargo run -p vm-iucv --example iucv_chat --features examples
Design Principles
This section documents the architectural decisions behind patch-cms – the why, not the what. The ROADMAP tracks features; these pages explain the reasoning future maintainers should understand before changing things.
Core Tenets
-
Embeddable first. xedit-core has zero I/O dependencies. The editor is a pure state machine driven by commands so it can be embedded in any Rust application without pulling in a terminal library, an async runtime, or a filesystem.
-
Trait seams, not concrete coupling. Every major subsystem boundary is a trait with a default no-op implementation. Crates depend on traits, never on sibling crate internals. This keeps the dependency graph shallow and lets each crate be tested and used in isolation.
-
Faithful VM/CMS semantics. Abbreviation rules, command syntax, RC codes, prefix area behavior, and the REXX execution model follow IBM documentation. Where the real system’s behavior is well-documented, we match it; where it isn’t, we pick the simplest defensible interpretation.
-
Modern Rust idioms. Zero
unsafeblocks across the entire workspace. Strong types, exhaustive pattern matching, and comprehensive tests. The codebase should be approachable for Rust developers who have never touched a mainframe. -
Incremental delivery. Each phase produces something usable on its own. The editor works without CMS; CMS works without the spool; the actor framework works without either.
Design Documents
- Why Rust – language choice and what it gives us
- REXX Integration – bridging a 1979 scripting language with Rust ownership
- Async Architecture – the actor model, sync/async boundary, and Tokio usage
- Trait Seams – the decoupling strategy and crate dependency graph
Why Rust
The Choice
VM/CMS is a systems programming environment. Recreating it requires precise control over memory layout, string handling, and concurrency – the same concerns that made the original system a C and assembler project. Rust gives us that control without the footguns.
What Rust Gives Us
Zero-Cost Abstractions for Editor Internals
xedit-core is a pure state machine. Every command is a function from
(EditorState, Command) -> (EditorState, Result). Rust’s enum-based
command representation and exhaustive match mean we can add a new
command and the compiler tells us every place that needs updating.
There is no dynamic dispatch in the hot path.
Ownership Makes Embeddability Real
The Editor struct owns its buffer, settings, and undo stack. There
are no global singletons, no thread-local state, and no hidden
allocations. You can create ten editors in one process, each with its
own filesystem adapter, and they will not interfere. This would be
painful to guarantee in C or C++ and impossible to enforce in a
garbage-collected language.
Fearless Concurrency in the Actor Framework
vm-iucv runs each machine as an isolated Tokio task. Messages flow
through typed mpsc channels. The type system enforces that a
MachineHandler callback cannot access another machine’s state –
there is no lock to forget, because there is no shared mutable state
to lock. The only synchronization is in the Supervisor, which holds
an Arc<RwLock<HashMap>> of machine entries.
Feature Flags for Optional Coupling
Cargo features let us gate expensive dependencies. The rexx feature
pulls in patch-rexx (~15K lines); without it, xedit-core compiles
in under a second and has no scripting support. The cms feature in
xedit-tui pulls in the CMS file system. This keeps compile times
reasonable and lets downstream users take only what they need.
Safe FFI Boundary (Future)
When we eventually expose a C API for embedding, Rust’s #[no_mangle]
and extern "C" give us a clean FFI surface. The zero-unsafe policy
means the only unsafe code will be at that boundary, making it easy to
audit.
What We Avoid
-
No
unsafeanywhere. The entire workspace compiles without a singleunsafeblock. We usestd::mem::swapinstead of pointer tricks,Rc<RefCell<>>instead of raw pointers for shared mutable state in macro execution, and Tokio’s typed channels instead of lock-free queues. -
No runtime reflection. Command dispatch uses Rust enums, not string-keyed maps. Abbreviation lookup uses a static table built at compile time.
-
No global state. Every subsystem is instantiated explicitly.
CommandProcessor,SpoolManager,Supervisor– all are owned values passed by reference. This makes testing trivial: each test constructs its own world.
REXX Integration
The Problem
REXX is a 1979 scripting language designed for interactive mainframe use. It assumes it owns the process, can call any system command by name, and shares mutable state freely with its host. Rust’s ownership model is the opposite of all of that.
Feature-Gated Dependency
REXX support is behind the rexx feature flag in both xedit-core and
cms-machine. Without it, the crates compile with no scripting support
and NoExecHandler stubs return RC 28 (“not found”) for any EXEC
invocation. This keeps the core editor embeddable without pulling in
the full REXX interpreter.
# In xedit-core/Cargo.toml
[features]
rexx = ["dep:patch-rexx"]
The REXX interpreter itself lives in patch-rexx, a separate
repository (~15K lines, ANSI-compliant).
Two Integration Points
ADDRESS XEDIT (Editor Macros)
When a REXX macro runs inside the editor, it needs to:
- Read editor state via EXTRACT variables (CURLINE, SIZE, FNAME, etc.)
- Issue editor commands via bare string expressions like
'COMMAND LOCATE /foo/'
The bridge lives in xedit-core/src/macro_engine.rs. Before
execution, we populate REXX compound variables with a snapshot of
editor state. A custom set_command_handler closure intercepts bare
string expressions and routes them to the editor’s command processor.
State snapshot, not live binding. EXTRACT variables are populated once at macro start. If the macro changes the buffer (e.g., via CHANGE), the EXTRACT values do not update mid-execution. This is a known limitation documented in the ROADMAP. Use QUERY for fresh state.
ADDRESS CMS (System Commands)
When REXX runs in the CMS machine context, it needs to call CMS
commands (LISTFILE, PIPE, spool operations). This is wired through
the ExecHandler and ExtCommandHandler traits:
patch-rexx (interpreter)
|
v
CmsRexxExecHandler (cms-machine) -- implements ExecHandler trait
|
v
CommandProcessor (cms-core) -- dispatches commands
|
v
ExtCommandHandler (cms-core trait) -- extension point
|
v
CmsExtCommandHandler (cms-machine) -- routes PIPE, spool commands
Each layer depends only on the trait above it, never on a concrete type from another crate.
The State Swap Pattern
REXX macros in CMS mode need access to the CMS filesystem and GLOBALV
variables. But CommandProcessor owns that state, and the REXX
interpreter needs it too during execution.
CmsRexxExecHandlerWithSwap solves this with explicit ownership
transfer:
- Before EXEC:
provide_state(fs, globalv)– CommandProcessor gives its filesystem and variable state to the handler - During EXEC: REXX runs with full access to CMS files and variables
- After EXEC:
retrieve_state()– handler returns the (potentially modified) state back to CommandProcessor
This avoids Arc<Mutex<>> and keeps the borrow checker happy. The
state is never shared – it moves from owner to owner.
SMSG from REXX
The SmsgSender trait lets REXX programs send inter-machine messages:
#![allow(unused)]
fn main() {
pub trait SmsgSender {
fn send_smsg(&self, target: &str, text: &str) -> i32;
}
}
ChannelSmsgSender (in cms-machine) implements this by enqueuing
messages to the vm-iucv router channel. This bridges the synchronous
REXX execution context with the asynchronous actor framework without
blocking the Tokio runtime.
Design Constraints
-
No persistent REXX state between EXECs. Each EXEC invocation gets a fresh interpreter instance. Persistent state goes through GLOBALV.
-
No concurrent REXX execution. A CMS machine processes one command at a time, including REXX EXECs. This matches real CMS behavior and avoids the need to make the interpreter thread-safe.
-
REXX is always optional. Any crate that works with REXX also works without it. The trait seam pattern makes this possible without
#[cfg]spaghetti – the no-op implementation handles the feature-off case.
Async Architecture
The Model
VM/CMS is inherently concurrent: multiple virtual machines run independently, each processing commands sequentially, while exchanging messages asynchronously. We model this with Tokio’s task-per-machine actor pattern.
Supervisor and Machine Tasks
The Supervisor (in vm-iucv) is the “Control Program.” It manages
machine lifecycles and message routing.
Supervisor
├── machines: Arc<RwLock<HashMap<MachineId, MachineEntry>>>
├── router_task -- routes SMSG between machines
├── path_cmd_task -- handles IUCV path lifecycle
└── per-machine tasks:
└── run_machine(handler, ctx, signal_rx)
Each machine is a Tokio task running a simple receive loop:
#![allow(unused)]
fn main() {
async fn run_machine(mut handler, ctx, mut signal_rx) {
handler.on_ipl(&ctx);
while let Some(signal) = signal_rx.recv().await {
match signal {
MachineSignal::Smsg(msg) => handler.on_smsg(&ctx, msg),
MachineSignal::Logoff => break,
// ... IUCV signals ...
}
}
handler.on_logoff(&ctx);
}
}
Key decision: synchronous callbacks on async tasks. The
MachineHandler trait methods are synchronous (&mut self, not
async). This means:
- Handlers cannot
.awaitinside callbacks - Handler state is never shared across tasks (no
Send + Syncbound on fields) - The machine task yields to the runtime only between signals
This matches real CMS semantics – a virtual machine processes one event at a time – and keeps handler implementations simple.
Message Routing
All inter-machine communication flows through typed channels:
Machine A Machine B
| |
| ctx.try_send_smsg("B", text) |
| |
v |
smsg_tx ──> router_task ──> signal_tx ──> signal_rx
|
v
on_smsg(msg)
The router uses try_send (non-blocking) to dispatch. If a machine’s
signal channel is full, the message is dropped. This is fire-and-forget
by design – it prevents one slow machine from blocking the entire
system.
The Sync-Async Bridge (Console)
The CMS interactive console poses a challenge: stdin is blocking I/O,
but the machine handler runs on an async task. The bridge works like
this:
[blocking thread] [async task]
stdin |
| |
read line |
| |
cmd_tx.send(line) ──────> drain_commands()
| (in on_smsg callback)
$CON SMSG wakes machine |
| execute command
wait for BATCH_DONE |
| output_tx.send(lines)
print output <────────── BATCH_DONE sentinel
- The console thread reads stdin and sends commands via
std::sync::mpsc(not Tokio’s – it runs on a blocking thread) - It wakes the machine by sending an SMSG from the
$CONpseudo-machine - The handler’s
on_smsgcallback drains the command channel and executes commands synchronously - Output lines flow back via a channel, terminated by a
BATCH_DONEsentinel - The console thread collects output until it sees the sentinel
The sentinel-based protocol avoids timeouts and polling. It is deterministic: the console knows exactly when the machine is done.
Why Not Async Handlers?
We considered making MachineHandler methods async. Reasons we didn’t:
-
CMS is sequential. A real CMS machine never processes two commands concurrently. Async handlers would add complexity for a capability we don’t want.
-
Handler state stays simple. With sync callbacks, handler fields can be plain
Vec,HashMap, etc. Async would requireSend + Syncbounds orspawn_local, adding friction for every handler implementation. -
Outbound messaging is already async.
ctx.try_send_smsg()enqueues to a channel that the router processes asynchronously. The handler doesn’t need to await the delivery. -
Blocking work is bounded. CMS commands (LISTFILE, CHANGE, etc.) are fast in-memory operations. There is no disk I/O or network call that would benefit from yielding mid-command.
If a future use case requires long-running async work inside a handler, the recommended pattern is: spawn a separate Tokio task and communicate results back via SMSG.
Tokio Usage
- Runtime:
rt-multi-threadin cms-machine’s main binary; vm-iucv itself only requiresrtandsync - Channels:
tokio::sync::mpscfor machine signals and router queues;std::sync::mpscfor the blocking console bridge - Locks:
tokio::sync::RwLockfor the machine registry (read-heavy, rarely written) - No timers in the core. Timeouts exist only as safety nets in the console bridge, not as part of the actor protocol
Trait Seams
The Strategy
Every major boundary between crates is a Rust trait with a default no-op implementation. Crates depend on traits defined in their own crate or in a leaf crate, never on sibling crate internals. This keeps the dependency graph shallow and allows each crate to be compiled, tested, and used independently.
The Traits
FileSystem (xedit-core)
#![allow(unused)]
fn main() {
pub trait FileSystem {
fn read_file(&self, file_id: &str) -> Result<String>;
fn write_file(&self, file_id: &str, content: &str) -> Result<()>;
fn parse_file_id(&self, file_id: &str) -> Option<FileIdentity>;
}
}
Defined in xedit-core. The editor calls this trait – it never touches
std::fs directly.
Implementations:
NativeFs(xedit-core) – delegates tostd::fs, used in standalone XEDIT modeCmsFs(cms-core) – mapsfn ft fmfile identifiers to the CMS minidisk filesystem
This is the core embeddability seam. A game engine, a web IDE, or a
test harness can implement FileSystem and get a working XEDIT editor
without any filesystem at all.
MachineHandler (vm-iucv)
#![allow(unused)]
fn main() {
pub trait MachineHandler: Send + 'static {
fn on_ipl(&mut self, ctx: &MachineContext) {}
fn on_smsg(&mut self, ctx: &MachineContext, msg: SmsgMessage);
fn on_logoff(&mut self, ctx: &MachineContext) {}
// ... IUCV connection callbacks with defaults ...
}
}
Defined in vm-iucv. The Supervisor calls these callbacks – it knows nothing about CMS, editors, or files.
Implementations:
CmsMachineHandler(cms-machine) – wraps a CommandProcessor and bridges console I/OCollectorHandler(vm-iucv, test utility) – collects messages for assertions- Example handlers in vm-iucv/examples/ – echo servers, pipeline stages, connection gating
Stage (cms-pipelines)
#![allow(unused)]
fn main() {
pub trait Stage {
fn initialize(&mut self, _output: &mut dyn StageSink) {}
fn process(&mut self, record: &str, output: &mut dyn StageSink);
fn finish(&mut self, _output: &mut dyn StageSink) {}
// ...
}
}
Defined in cms-pipelines. The pipeline executor drives stages through
initialize → process* → finish without knowing what they do.
Built-in implementations: literal, console, locate, nlocate
SpoolBackend (cms-spool)
#![allow(unused)]
fn main() {
pub trait SpoolBackend {
fn enqueue(&mut self, ...) -> Result<SpoolId>;
fn dequeue(&mut self, ...) -> Result<Option<SpoolEntry>>;
fn list_queue(&self, ...) -> Vec<SpoolEntry>;
fn purge(&mut self, ...) -> Result<()>;
// ...
}
}
Defined in cms-spool. The SpoolManager delegates storage to a backend.
Implementations:
InMemoryBackend(cms-spool) – for testing and transient useDirectoryBackend(cms-spool) –.meta/.datafile pairs on disk
ExecHandler, SmsgSender, ExtCommandHandler (cms-core)
Three small traits that let cms-core’s CommandProcessor call out to
REXX, messaging, and extension commands without depending on their
implementations:
#![allow(unused)]
fn main() {
pub trait ExecHandler {
fn execute(&mut self, name: &str, args: &str, ...) -> i32;
}
pub trait SmsgSender {
fn send_smsg(&self, target: &str, text: &str) -> i32;
}
pub trait ExtCommandHandler {
fn handle(&mut self, command: &str, args: &str, ...) -> Option<i32>;
}
}
Each has a No* default (e.g., NoExecHandler) that returns a
failure RC. cms-machine provides the real implementations.
Crate Dependency Graph
xedit-core
/ \
xedit-tui cms-core
\ /
\ /
cms-spool \ / cms-pipelines
\ \ / /
\ cms-machine /
\_____|____|_____________/
|
vm-iucv
Leaf crates (no workspace dependencies):
vm-iucv– depends only on Tokiocms-spool– depends on nothing (onlytempfilefor tests)cms-pipelines– depends on nothing
Middle crates:
xedit-core– optionally depends onpatch-rexxcms-core– depends on xedit-core (for FileSystem trait)
Composition crate:
cms-machine– pulls everything together, providesmain()
This hierarchy means you can use vm-iucv as a standalone actor framework, cms-pipelines as a standalone data pipeline library, or xedit-core as a standalone editor model, without dragging in the rest of the workspace.
Adding a New Seam
When introducing a new subsystem boundary:
- Define the trait in the crate that consumes it (not the one that implements it)
- Provide a
No*default implementation that returns a sensible failure code - Wire the real implementation in cms-machine, where all crates meet
- Gate the dependency behind a Cargo feature if it’s heavyweight
Overview
cms-core implements the CMS (Conversational Monitor System) layer of the
VM/CMS reimplementation. It provides the file system, command processor,
session variables (GLOBALV), and EXEC resolution – the foundation that
higher-level crates (cms-spool, cms-pipelines, cms-machine) build on.
Key Concepts
CMS File System
Files are identified by a three-part name: filename, filetype, and
filemode (fn ft fm). The filemode letter (A-Z) selects a minidisk;
the wildcard * searches all accessed disks in alphabetical order.
CmsFileSystem manages a BTreeMap<char, Minidisk> and exposes CMS-style
operations: read, write, erase, rename, copy, list, and state (existence
check). Disks are accessed with a mode (read-only or read-write) via the
ACCESS command and released with RELEASE.
CommandProcessor
The central dispatch engine. It owns the file system, GLOBALV store, an
ExecHandler, an ExtCommandHandler, and an optional SmsgSender. All
command execution flows through CommandProcessor::execute(input).
GLOBALV
GlobalVars provides session-scoped named variables organized into groups
(default group: LASTING). Group and variable names are uppercased; values
are stored as-is. Supports SELECT, SET, GET, LIST, DELETE, and PURGE
sub-commands, matching IBM CMS GLOBALV semantics.
EXEC Resolution
When the command processor encounters an unknown command, it searches for a
file named <COMMAND> EXEC * across all accessed disks. If found, the file
is handed to the ExecHandler for interpretation (typically REXX via
patch-rexx).
Trait Seams
The crate uses trait-based seams to stay decoupled from the REXX interpreter, the actor framework, and extension command sets.
| Trait | Purpose | Default (no-op) |
|---|---|---|
ExecHandler | Run REXX EXEC files; swap fs/gv/smsg state in and out | NoExecHandler (RC=28) |
SmsgSender | Send SMSG messages to other virtual machines | NoSmsgSender (RC=28) |
ExtCommandHandler | Handle commands outside cms-core (spool, pipelines) | NoExtCommands (None) |
ExecHandler also defines provide_state / retrieve_state pairs for both
the file system + GLOBALV and the SMSG sender, enabling the REXX interpreter
to issue CMS commands that mutate shared state during EXEC execution.
The execute() Flow
execute(input)
|
+-- parse_cms_command(input)
| |
| +-- OK(cmd) --> dispatch(cmd) // built-in command
| |
| +-- Err(UnknownCommand)
| |
| +-- ext_handler.try_execute(input)
| | |
| | +-- Some(rc, msgs) --> return result
| | +-- None --> fall through
| |
| +-- try_exec_fallback() // search <CMD> EXEC *
|
+-- Err(other) --> RC=24, error message
- Parse –
parse_cms_commandtokenizes the input line, matches the first word against the abbreviation table, and returns aCmsCommandenum. - Built-in – Known commands (LISTFILE, STATE, COPYFILE, ERASE, RENAME, GLOBALV, ACCESS, RELEASE, EXEC, SMSG) are dispatched directly.
- Extension – If parsing yields
UnknownCommand, theExtCommandHandlergets first crack. This is howcms-spoolandcms-pipelinesinject their commands without modifying cms-core. - EXEC fallback – If the extension handler returns
None, the processor searches for<CMD> EXEC *on disk and runs it through theExecHandler.
Abbreviation Matching
Commands follow IBM CMS abbreviation conventions. Each command has a minimum
abbreviation length defined in CMS_COMMAND_TABLE:
| Command | Min. Abbrev | Example |
|---|---|---|
| ACCESS | 2 | AC |
| COPYFILE | 4 | COPY |
| ERASE | 2 | ER |
| EXEC | 4 | EXEC |
| GLOBALV | 4 | GLOB |
| LISTFILE | 4 | LIST |
| RELEASE | 3 | REL |
| RENAME | 3 | REN |
| SMSG | 2 | SM |
| STATE | 2 | ST |
lookup_command tries an exact match first, then checks whether the input is
at least min_abbrev characters long and is a prefix of the full command name.
Ambiguous abbreviations (matching more than one command) return no match.
Overview
The cms-spool crate implements the VM/CMS virtual spool subsystem. It models
the unit-record device queues – reader, punch, and printer – that CMS virtual
machines use to exchange files and produce printed output.
Spool Concepts
Devices
Three virtual unit-record devices are supported, mirroring the IBM VM/CMS model:
| Device | Aliases | Purpose |
|---|---|---|
| Reader | RDR, R | Incoming files from other users |
| Printer | PRT, PR | Output destined for printing |
| Punch | PUN, PCH, PU | Card-punch output |
Classes
Each device has an associated spool class (a single uppercase letter, A-Z).
Classes control routing and filtering – for example, QUERY READER CLASS N
lists only class-N reader files.
Spool IDs
Every file placed in a spool queue is assigned a unique numeric spool ID. The ID is used by PURGE to target individual files for removal.
Commands
All commands use CMS-style abbreviation matching (minimum unique prefix).
| Command | Min Abbrev | Example | Description |
|---|---|---|---|
| SPOOL | SP | SP PRT CLASS A DEST OPERATOR | Configure a device’s class, destination, hold, continuous, or copy count |
| SENDFILE | SE | SE MYFILE DATA A TO JONES | Send a file to another user’s reader |
| RECEIVE | REC | REC NEWNAME DATA A | Dequeue the next file from your reader |
| QUERY | Q | Q READER | List files waiting in a device queue |
| PURGE | PUR | PUR READER 12345 / PUR RDR ALL | Remove one or all files from a queue |
These are represented in code by the SpoolCommand enum in command.rs, which
is parsed from raw token vectors via SpoolCommand::parse().
SpoolManager and the Backend Trait
SpoolManager<B> is the main entry point for spool operations. It is generic
over the SpoolBackend trait, which abstracts the storage layer:
pub trait SpoolBackend {
fn enqueue(...) -> Result<u64>;
fn dequeue(device) -> Result<(SpoolFile, String)>;
fn list_queue(device, class) -> Result<Vec<SpoolFile>>;
...
}
Two backend implementations are provided:
- InMemoryBackend – hash-map-based storage used in tests.
- DirectoryBackend – filesystem-backed storage using per-device
subdirectories (
rdr/,prt/,pun/) with.metasidecar files.
SpoolManager owns per-device DeviceConfig structs that track the current
class, destination, hold, and copy-count settings. The SPOOL command modifies
these configs; SENDFILE and RECEIVE use them when enqueuing or dequeuing files.
SpoolCommandResult carries a return code and message list back to the caller,
following CMS conventions (rc 0 = success).
Integration with CMS
The spool subsystem plugs into cms-core’s CommandProcessor through the
ExtCommandHandler trait. When the command processor encounters an unrecognized
command, it delegates to registered external handlers. The spool handler matches
SPOOL, SENDFILE, RECEIVE, QUERY, and PURGE, parses them into SpoolCommand
variants, and executes them against the SpoolManager. This keeps the spool
logic decoupled from the core CMS command loop while still appearing as
first-class CMS commands to the user.
Overview
The cms-pipelines crate implements Hartmann pipelines – the VM/CMS mechanism
for composing data transformations as a series of stages connected by pipes.
Pipeline Syntax
Pipelines are invoked with the PIPE command. Stages are separated by |:
PIPE literal Hello world | console
PIPE literal abc | locate /b/ | console
PIPE literal secret | nlocate /secret/ | console
Each stage reads records from its input, processes them, and writes records to its output. The first stage in a pipeline is a source (no input); the last is typically a sink.
Built-in Stages
| Stage | Purpose |
|---|---|
literal | Emits its argument as a single output record |
console | Writes each input record to console output |
locate | Passes records containing a given string |
nlocate | Passes records NOT containing a given string |
The Stage trait can be implemented to add custom stages.
Two-Pass Executor
The pipeline executor runs in two passes:
- Initialize – Each stage is created and connected to its neighbors via input/output channels.
- Process + Finish – Records flow through the pipeline. Each stage processes input records and produces output. When the source is exhausted, stages receive a finish signal to flush any buffered state.
Multi-Stream Support
Stages can produce output on primary and secondary streams. The return code from a stage determines which output stream receives the record:
| RC | Meaning |
|---|---|
| 0 | Record written to primary output |
| 4 | Record written to secondary output (e.g., non-matching in locate) |
| 12 | End of stage processing |
This allows filter stages like locate to split matching and non-matching
records into separate streams.
Return Codes
Pipeline execution follows CMS conventions:
| RC | Meaning |
|---|---|
| 0 | Pipeline completed successfully |
| 24 | Syntax error or empty pipeline specification |
| 28 | Stage not found |
| 32 | Pipeline execution error |
Integration with CMS
The pipeline subsystem is wired into cms-core’s CommandProcessor through
the ExtCommandHandler trait. When a command starts with PIPE, the extension
handler strips the prefix and passes the rest to run_pipe(). Results (return
code and output messages) are returned through the standard command result
path.
Overview
The cms-machine crate wires all subsystems together into an interactive CMS
console. It is the top-level binary that provides a REPL where users can issue
CMS commands, run REXX EXECs, use spool commands, and execute pipelines.
CmsMachineHandler
CmsMachineHandler implements the MachineHandler trait from vm-iucv,
bridging the actor framework with the CMS CommandProcessor. It handles:
- on_ipl – Prints the startup banner, runs
PROFILE EXECif present, and begins the command loop. - on_smsg – Dispatches incoming messages. Messages from
$CONtrigger command execution; messages from other machines are displayed and stored in GLOBALV (LMSGSRC,LMSGTXT). - on_logoff – Cleans up resources.
Console Architecture
The console uses a split-thread design:
stdin (blocking read)
|
v
cmd_tx channel ──> CmsMachineHandler
| |
+── SMSG from $CON ──> on_smsg()
|
v
CommandProcessor::execute()
|
v
output_tx channel ──> stdout
- A dedicated thread reads lines from stdin and sends them via
cmd_tx. - After sending a command, the thread sends an SMSG from the
$CONpseudo-machine to wake the handler. - The handler receives the SMSG, pulls the command from
cmd_rx, executes it, and sends output lines viaoutput_tx. - The console thread reads output lines until it sees the
BATCH_DONEsentinel, then prompts for the next command.
BATCH_DONE Sentinel
The BATCH_DONE constant is a special marker string sent after every command
completes. The console thread waits for this sentinel before displaying the
next prompt, ensuring output from one command is fully flushed before the next
begins.
Subsystem Integration
All subsystems are available from the REPL:
| Subsystem | How it connects |
|---|---|
| CMS commands | Built into CommandProcessor (GLOBALV, LISTFILE, etc.) |
| REXX EXECs | CmsRexxExecHandlerWithSwap – persistent state across EXECs |
| Spool | CmsExtCommandHandler routes SP/QUERY/PURGE/SENDFILE/RECEIVE |
| Pipelines | CmsExtCommandHandler routes PIPE commands to run_pipe() |
| SMSG | ChannelSmsgSender routes through the actor framework |
The CmsRexxExecHandlerWithSwap handler swaps the file system, GLOBALV store,
and SMSG sender into the REXX execution context, so REXX EXECs can issue CMS
commands that modify persistent state.
CLI Usage
# Basic usage
cargo run -p cms-machine -- --userid ALICE --disk /tmp/cms
# Multiple disks (mounted as A, B, C, ...)
cargo run -p cms-machine -- --userid BOB --disk /tmp/d1 --disk /tmp/d2
The --userid flag sets the machine’s identity for SMSG routing. Each
--disk path is mounted as a minidisk at successive filemode letters
(A, B, C, …) in read-write mode.
Overview
vm-iucv is a lightweight actor framework inspired by IBM’s VM/CMS
inter-machine communication facilities. Each actor is a machine — an
isolated unit with its own message handler — coordinated by a Supervisor
that manages lifecycle, messaging, and IUCV paths.
Architecture
┌──────────────────────────────────────────┐
│ Supervisor │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ ALICE │ │ BOB │ │ CHARLIE │ │
│ │ handler │ │ handler │ │ handler │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ┌────┴────────────┴────────────┴────┐ │
│ │ Router (SMSG) │ │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ Path Command Loop (IUCV) │ │
│ └───────────────────────────────────┘ │
└──────────────────────────────────────────┘
VM/CMS Analogy
| VM/CMS Concept | vm-iucv Equivalent |
|---|---|
| Virtual machine | MachineId + MachineHandler |
| CP (Control Program) | Supervisor |
| IPL (boot) | supervisor.ipl() |
| LOGOFF | supervisor.logoff() |
| CP SMSG | supervisor.smsg() / ctx.try_send_smsg() |
| IUCV CONNECT | supervisor.connect() |
| IUCV SEVER | supervisor.sever() / ctx.sever_path() |
| IUCV SEND | ctx.iucv_send() |
| IUCV RECEIVE | on_iucv_data() callback |
Key Design Principles
Fire-and-forget messaging. SMSG delivery is best-effort, matching the real CP SMSG semantics. If a target machine’s channel is full, the message is silently dropped.
Synchronous handlers. All MachineHandler callbacks run synchronously
on the machine’s Tokio task. This keeps the programming model simple —
handlers never need to be async. Use ctx.try_send_smsg() or
ctx.iucv_send() to communicate without blocking.
Path-based connections. IUCV paths provide a bidirectional data channel
between two machines, with explicit connect/accept/sever lifecycle. Paths
carry binary data (IucvBuffer) rather than text.
Crate Features
| Feature | Description |
|---|---|
test-util | Enables CollectorHandler / CollectorHandle for testing |
examples | Pulls in tokio runtime features needed for examples |
Machines & Handlers
MachineId
Every machine has a unique identity — a MachineId. IDs follow VM/CMS naming
rules:
- 1–8 characters
- Must start with a letter or national character (
@,#,$) - Remaining characters: alphanumeric or national
- Automatically uppercased
#![allow(unused)]
fn main() {
let id = MachineId::new("alice").unwrap(); // becomes "ALICE"
assert_eq!(id.as_str(), "ALICE");
}
MachineHandler Trait
Every machine needs a handler that implements MachineHandler. The only
required method is on_smsg:
#![allow(unused)]
fn main() {
use vm_iucv::handler::{MachineContext, MachineHandler};
use vm_iucv::message::SmsgMessage;
struct MyHandler;
impl MachineHandler for MyHandler {
fn on_smsg(&mut self, ctx: &MachineContext, msg: SmsgMessage) {
println!("Got: {}", msg.text());
}
}
}
Lifecycle Callbacks
| Callback | When Called | Required |
|---|---|---|
on_ipl | After boot, before signals | No |
on_smsg | For each incoming SMSG | Yes |
on_connection_pending | Peer requests a path | No (default: accept) |
on_connection_complete | Path fully established | No |
on_connection_severed | Path severed by peer | No |
on_iucv_data | Data arrives on a path | No |
on_logoff | Machine logging off | No |
Important Notes
- All callbacks are synchronous — they run on the machine’s Tokio task. Blocking a callback stalls the entire machine.
on_logoffis not called ifon_iploron_smsgpanics. Use RAII guards for guaranteed cleanup.on_connection_completeandon_connection_severeduse best-effort delivery — they may be skipped if the signal channel is full.
MachineContext
The MachineContext is passed to every callback and provides the machine’s
identity and communication methods:
#![allow(unused)]
fn main() {
// Identity
ctx.machine_id() // → &MachineId
// SMSG (fire-and-forget)
ctx.try_send_smsg(&to, "text") // → Result<()>
// IUCV path operations
ctx.iucv_send(path, buffer) // → Result<()>
ctx.sever_path(path) // → Result<()>
}
All MachineContext methods are non-blocking — they use try_send internally
and return immediately.
Machine Lifecycle
Supervisor::ipl()
│
▼
on_ipl() ← one-time initialization
│
▼
┌─────────┐
│ Signal │◄──── on_smsg(), on_connection_*, on_iucv_data()
│ Loop │
└────┬────┘
│
▼
on_logoff() ← cleanup (best-effort)
│
▼
(task ends)
SMSG Messaging
SMSG (Special Message) is the fire-and-forget text messaging facility, modeled
after the VM/CMS CP SMSG command.
Sending Messages
There are two ways to send an SMSG:
From outside a handler (via Supervisor)
#![allow(unused)]
fn main() {
// Async — awaits until the message is enqueued in the router
supervisor.smsg(&from, &to, "Hello").await?;
}
From inside a handler (via MachineContext)
#![allow(unused)]
fn main() {
impl MachineHandler for MyHandler {
fn on_smsg(&mut self, ctx: &MachineContext, msg: SmsgMessage) {
// Non-blocking — uses try_send
let _ = ctx.try_send_smsg(msg.from(), "Got it!");
}
}
}
Message Constraints
| Constraint | Limit |
|---|---|
| Max text length | 236 bytes |
| Character set | ASCII only |
These match the real CP SMSG limits (236-byte EBCDIC text).
#![allow(unused)]
fn main() {
// Constructing a message manually
let msg = SmsgMessage::new(
MachineId::new("ALICE").unwrap(),
MachineId::new("BOB").unwrap(),
"Hello Bob",
)?;
}
Delivery Semantics
SMSG delivery is best-effort:
- If the target machine exists and its channel has room, the message is
delivered to
on_smsg. - If the target’s channel is full, the message is silently dropped.
- If the target is not logged on,
supervisor.smsg()returnsMachineNotFound. - Messages in-flight when a machine logs off may or may not be delivered.
This matches real VM/CMS behavior where CP SMSG provides no delivery guarantee.
Request/Reply Pattern
Since SMSG is fire-and-forget, implement request/reply by having the receiver send a response back:
#![allow(unused)]
fn main() {
struct EchoHandler;
impl MachineHandler for EchoHandler {
fn on_smsg(&mut self, ctx: &MachineContext, msg: SmsgMessage) {
let reply = format!("ECHO: {}", msg.text());
let _ = ctx.try_send_smsg(msg.from(), &reply);
}
}
}
See the echo_server example for a complete working version.
Inspecting Messages (Testing)
Use the CollectorHandler (requires test-util feature) to capture messages
for inspection:
#![allow(unused)]
fn main() {
use vm_iucv::collector::collector;
let (handler, handle) = collector();
supervisor.ipl(&id, handler).await?;
// ... send messages ...
let messages = handle.messages();
assert_eq!(messages[0].text(), "Hello");
}
IUCV Paths
IUCV (Inter-User Communication Vehicle) paths provide bidirectional binary data channels between two machines, modeled after the VM/CMS IUCV facility.
Path Lifecycle
connect() on_connection_pending()
(initiator) (target decides: accept/refuse)
│ │
│ ┌───────────────┤
│ │ accept │ refuse
▼ ▼ ▼
ESTABLISHED ConnectionRefused
│
│ ◄── iucv_send() / on_iucv_data()
│
sever() or logoff
│
▼
PATH REMOVED
Establishing a Path
Use supervisor.connect() to request a path:
#![allow(unused)]
fn main() {
// Initiator requests a path to the target.
let path: PathId = supervisor.connect(&initiator, &target).await?;
}
The target’s on_connection_pending is called. If it returns true, the path
is established and both sides receive on_connection_complete. If false,
the caller gets ConnectionRefused.
Accepting/Refusing Connections
Override on_connection_pending to implement connection gating:
#![allow(unused)]
fn main() {
fn on_connection_pending(
&mut self,
_ctx: &MachineContext,
_path: PathId,
from: &MachineId,
) -> bool {
// Only accept connections from TRUSTED
from.as_str() == "TRUSTED"
}
}
The default implementation accepts all connections.
See the connection_gating example for a complete working version.
Sending Data
Once a path is established, send binary data with ctx.iucv_send():
#![allow(unused)]
fn main() {
use vm_iucv::path::IucvBuffer;
let data = IucvBuffer::new(b"Hello via IUCV".to_vec())?;
ctx.iucv_send(path, data)?;
}
The peer receives the data in on_iucv_data:
#![allow(unused)]
fn main() {
fn on_iucv_data(&mut self, _ctx: &MachineContext, path: PathId, data: IucvBuffer) {
let text = String::from_utf8_lossy(data.as_bytes());
println!("Received on {}: {}", path, text);
}
}
IucvBuffer Limits
| Constraint | Limit |
|---|---|
| Max buffer size | 65,535 bytes |
Severing a Path
Either side can sever a path:
#![allow(unused)]
fn main() {
// From outside a handler (via Supervisor)
supervisor.sever(path, &machine_id).await?;
// From inside a handler (via MachineContext)
ctx.sever_path(path)?;
}
The peer receives on_connection_severed. When a machine logs off, all its
paths are automatically severed and peers are notified.
SMSG vs IUCV
| Feature | SMSG | IUCV |
|---|---|---|
| Data format | Text (ASCII, 236 bytes max) | Binary (65KB max) |
| Connection | None (fire-and-forget) | Explicit path lifecycle |
| Direction | One-way per message | Bidirectional on path |
| Delivery | Best-effort | Best-effort |
| Use case | Commands, notifications | Data transfer, sessions |
API Quick Reference
Supervisor
| Method | Description |
|---|---|
Supervisor::new() | Create a new supervisor (must be called in a Tokio runtime) |
ipl(&id, handler) | Boot a machine with the given handler |
logoff(&id) | Log off a running machine |
smsg(&from, &to, text) | Send an SMSG between machines |
connect(&from, &to) | Establish an IUCV path between machines |
sever(path, &from) | Sever an IUCV path |
query_names() | List all running machine IDs |
query_paths() | List all active path IDs |
shutdown() | Log off all machines and stop the supervisor |
All Supervisor methods are async except new().
MachineContext
| Method | Description |
|---|---|
machine_id() | Get this machine’s MachineId |
try_send_smsg(&to, text) | Send an SMSG (non-blocking, fire-and-forget) |
iucv_send(path, buffer) | Send data on an IUCV path (non-blocking) |
sever_path(path) | Sever an IUCV path (non-blocking) |
All MachineContext methods are synchronous (non-async) and use try_send.
MachineHandler Trait
| Callback | Signature | Default |
|---|---|---|
on_ipl | (&mut self, &MachineContext) | No-op |
on_smsg | (&mut self, &MachineContext, SmsgMessage) | Required |
on_connection_pending | (&mut self, &MachineContext, PathId, &MachineId) -> bool | true |
on_connection_complete | (&mut self, &MachineContext, PathId, &MachineId) | No-op |
on_connection_severed | (&mut self, &MachineContext, PathId, &MachineId) | No-op |
on_iucv_data | (&mut self, &MachineContext, PathId, IucvBuffer) | No-op |
on_logoff | (&mut self, &MachineContext) | No-op |
Key Types
| Type | Description |
|---|---|
MachineId | Machine identity (1-8 chars, auto-uppercased) |
SmsgMessage | Text message (ASCII, max 236 bytes) |
PathId | Opaque IUCV path identifier (Copy, Eq, Hash) |
IucvBuffer | Binary data buffer (max 65,535 bytes) |
IucvError | Error type with CMS-style return codes |
CollectorHandler (test-util feature)
| Function/Method | Description |
|---|---|
collector() | Create a (CollectorHandler, CollectorHandle) pair |
handle.messages() | Snapshot of collected SMSG messages |
handle.count() | Number of collected messages |
handle.path_events() | Snapshot of collected path events |
handle.path_event_count() | Number of collected path events |
Error Codes
All errors use the IucvError enum and display with the IBM DMSIUC message
prefix, following VM/CMS conventions.
Error Variants
| Variant | RC | Message Prefix | Description |
|---|---|---|---|
AlreadyRunning(id) | 8 | DMSIUC008E | Machine is already IPL’d |
MachineNotFound(id) | 12 | DMSIUC012E | Target machine not logged on |
AlreadyLoggedOff(id) | 12 | DMSIUC012E | Machine already logged off |
DeliveryFailed(id) | 16 | DMSIUC016E | Message channel closed |
ChannelBusy(ch) | 20 | DMSIUC020W | Channel full (transient) |
InvalidParameter(msg) | 24 | DMSIUC024E | Invalid parameter value |
InvalidMachineId(msg) | 24 | DMSIUC024E | Invalid machine ID format |
MachinePanicked(id) | 28 | DMSIUC028E | Machine task panicked |
SupervisorDown | 32 | DMSIUC032E | Supervisor has shut down |
PathNotFound(id) | 36 | DMSIUC036E | IUCV path not found |
ConnectionRefused(msg) | 40 | DMSIUC040E | Target refused connection |
Using Return Codes
Every error carries a CMS-style numeric return code:
#![allow(unused)]
fn main() {
match supervisor.smsg(&from, &to, "Hello").await {
Ok(()) => println!("Sent"),
Err(e) => println!("Failed with RC={}: {}", e.rc(), e),
}
}
Common Error Scenarios
IPL a machine twice:
DMSIUC008E Machine already running - ALICE
Send to a machine that isn’t logged on:
DMSIUC012E Machine not found - GHOST
SMSG text too long (> 236 bytes):
DMSIUC024E Invalid parameter - SMSG text exceeds 236 bytes
Connection refused by target:
DMSIUC040E Connection refused - SECURE refused connection from UNTRUST
VM/CMS Glossary
A mapping of VM/CMS terminology to Rust concepts in this workspace.
VM & Supervisor
| VM/CMS Term | Rust Concept | Description |
|---|---|---|
| CP (Control Program) | Supervisor | The hypervisor that manages virtual machines |
| Virtual Machine | MachineId + MachineHandler | An isolated execution context with its own handler |
| IPL (Initial Program Load) | supervisor.ipl() | Boot a virtual machine |
| LOGOFF | supervisor.logoff() | Shut down a virtual machine |
Messaging & Communication
| VM/CMS Term | Rust Concept | Description |
|---|---|---|
| CP SMSG | supervisor.smsg() / ctx.try_send_smsg() | Fire-and-forget text message (max 236 bytes) |
| IUCV (Inter-User Communication Vehicle) | Path API (connect, sever, iucv_send) | Bidirectional data channel between machines |
| IUCV CONNECT | supervisor.connect() | Request a path to another machine |
| IUCV ACCEPT | on_connection_pending returning true | Accept an incoming connection request |
| IUCV SEVER | supervisor.sever() / ctx.sever_path() | Tear down an established path |
| IUCV SEND | ctx.iucv_send() | Send binary data on a path |
| IUCV RECEIVE | on_iucv_data() callback | Receive binary data on a path |
| Path | PathId | Identifier for an IUCV connection |
CMS File System
| VM/CMS Term | Rust Concept | Description |
|---|---|---|
| fn ft fm | FileSpec | CMS file naming: filename, filetype, filemode (e.g., PROFILE EXEC A) |
| Minidisk | AccessMode + disk path | Virtual disk accessed at a filemode letter (A-Z) |
| Access | CmsFileSystem::access_disk() | Mount a disk directory at a filemode letter |
| LISTFILE | cmd_listfile() | List files matching a pattern |
| COPYFILE | cmd_copyfile() | Copy files between disks |
Command Processor
| VM/CMS Term | Rust Concept | Description |
|---|---|---|
| CMS command | CommandProcessor::execute() | Dispatch a command through the processor |
| Abbreviation | parse_cms_command() | IBM-style minimum abbreviation matching (e.g., GLO for GLOBALV) |
| EXEC | ExecHandler::execute_exec() | Run a REXX or EXEC2 program |
| GLOBALV | GlobalVars | Persistent named variables (SET/GET/LIST, LASTING/SESSION groups) |
| ExtCommandHandler | ExtCommandHandler trait | Extension point for spool/pipeline commands |
Spool Subsystem
| VM/CMS Term | Rust Concept | Description |
|---|---|---|
| Spool | SpoolManager | Virtual I/O queue manager for reader/punch/printer |
| Reader (RDR) | Reader queue | Inbound spool device for receiving files |
| Punch (PUN) | Punch queue | Outbound spool device for sending files |
| Printer (PRT) | Printer queue | Output spool device |
| Spool class | SpoolClass | Single-letter classification (A-Z, 0-9, *) |
| Spool ID | SpoolId | Numeric identifier for a spooled file |
| SENDFILE | execute_spool_command(SendFile) | Send a file to another user’s reader |
| RECEIVE | execute_spool_command(Receive) | Receive a file from the reader queue |
Pipelines
| VM/CMS Term | Rust Concept | Description |
|---|---|---|
| PIPE | run_pipe() | Execute a Hartmann pipeline |
| Stage | Stage trait | A processing unit in a pipeline |
| literal | Built-in stage | Emits a literal string |
| console | Built-in stage | Writes records to output (like terminal display) |
| locate | Built-in stage | Passes records matching a pattern |
| nlocate | Built-in stage | Passes records NOT matching a pattern |
XEDIT Editor
| VM/CMS Term | Rust Concept | Description |
|---|---|---|
| XEDIT | xedit-core | Full-screen editor |
| Prefix area | Prefix command model | Left margin for line commands (d, dd, i, a, c, m, etc.) |
| Current line | Editor::current_line() | The line the cursor is positioned on |
| Target | Target system | Navigation spec (:n, /string/, *, compound) |
| EXTRACT | REXX extract variables | Query editor state from macros |
| PROFILE XEDIT | Profile exec | Startup macro executed when editor opens |
| Command line | ====> prompt | Where editor commands are entered |
General
| VM/CMS Term | Rust Concept | Description |
|---|---|---|
| EBCDIC | ASCII (simplified) | Character encoding for messages |
| DMSIUC | IucvError display prefix | IBM message prefix for IUCV errors |
| RC (Return Code) | rc() methods | Numeric error code (CMS convention: 0=ok, 24=error, 28=not found) |
| Signal | Internal MachineSignal enum | Event dispatched to a machine’s handler |
| Console | CollectorHandler | Test utility that captures messages (like a virtual console) |
| QUERY NAMES | supervisor.query_names() | List all running machines |