mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Move protocol to workspace root.
This commit is contained in:
Generated
+1
-9
@@ -851,11 +851,11 @@ name = "unshell"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"crossbeam-channel",
|
||||||
"rkyv",
|
"rkyv",
|
||||||
"static_init",
|
"static_init",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"unshell-macros",
|
"unshell-macros",
|
||||||
"unshell-protocol",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -874,14 +874,6 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unshell-protocol"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-channel",
|
|
||||||
"rkyv",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ush-obfuscate"
|
name = "ush-obfuscate"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
+5
-3
@@ -7,7 +7,7 @@ members = [
|
|||||||
"unshell-macros-core",
|
"unshell-macros-core",
|
||||||
"unshell-macros",
|
"unshell-macros",
|
||||||
|
|
||||||
"unshell-protocol",
|
# "unshell-protocol",
|
||||||
|
|
||||||
"unshell-leaves/leaf-pty",
|
"unshell-leaves/leaf-pty",
|
||||||
]
|
]
|
||||||
@@ -33,7 +33,7 @@ portable-pty = "0.9.0"
|
|||||||
crossbeam-channel = "0.5.15"
|
crossbeam-channel = "0.5.15"
|
||||||
|
|
||||||
unshell = { path = "." }
|
unshell = { path = "." }
|
||||||
unshell-protocol = { path = "./unshell-protocol" }
|
# unshell-protocol = { path = "./unshell-protocol" }
|
||||||
unshell-macros-core = { path = "./unshell-macros-core" }
|
unshell-macros-core = { path = "./unshell-macros-core" }
|
||||||
unshell-macros = { path = "./unshell-macros" }
|
unshell-macros = { path = "./unshell-macros" }
|
||||||
|
|
||||||
@@ -70,10 +70,12 @@ chrono = { workspace = true, optional = true }
|
|||||||
static_init = { workspace = true }
|
static_init = { workspace = true }
|
||||||
|
|
||||||
unshell-macros = { workspace = true }
|
unshell-macros = { workspace = true }
|
||||||
unshell-protocol = { workspace = true }
|
# unshell-protocol = { workspace = true }
|
||||||
# unshell-runtime = { workspace = true }
|
# unshell-runtime = { workspace = true }
|
||||||
# unshell-leaves = { workspace = true }
|
# unshell-leaves = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
crossbeam-channel.workspace = true
|
||||||
|
|
||||||
[profile.minimize]
|
[profile.minimize]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
|
|||||||
@@ -0,0 +1,810 @@
|
|||||||
|
# Macro-Generated Leaf Interface Design
|
||||||
|
|
||||||
|
**Status:** Draft
|
||||||
|
**Last updated:** 2026-05-28
|
||||||
|
**Primary use case:** Remote PTY sessions over hook-backed UnShell packets
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This document proposes a generated leaf interface for UnShell. The goal is to make
|
||||||
|
stateful leaves, such as a remote PTY leaf, easy to write without forcing every leaf
|
||||||
|
author to hand-code packet draining, procedure dispatch, session lookup, hook
|
||||||
|
lifetime handling, and retry-safe final-frame cleanup.
|
||||||
|
|
||||||
|
The user writes the application logic:
|
||||||
|
|
||||||
|
- the leaf state struct
|
||||||
|
- one or more session types for long-running hook-backed conversations
|
||||||
|
- one or more procedure types for one-packet operations
|
||||||
|
- payload encoding and decoding
|
||||||
|
- OS-specific behavior, such as spawning or polling a PTY
|
||||||
|
|
||||||
|
The macro generates the plumbing:
|
||||||
|
|
||||||
|
- the `Leaf` implementation
|
||||||
|
- the generated wrapper that owns session stores and retry queues
|
||||||
|
- inbound packet filtering for the leaf's procedure ids
|
||||||
|
- per-packet procedure dispatch
|
||||||
|
- per-hook session dispatch
|
||||||
|
- retry-safe outbound flushing
|
||||||
|
- final-frame session removal only after routing succeeds
|
||||||
|
|
||||||
|
The macro should generate ordinary Rust. No runtime registry, no boxed procedure
|
||||||
|
objects, and no hidden dynamic dispatch in the hot path.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The current `Leaf` trait is deliberately small:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait Leaf {
|
||||||
|
fn get_id(&self) -> u32;
|
||||||
|
|
||||||
|
fn update(&mut self, endpoint: &mut Endpoint);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That makes the protocol runtime flexible, but it also means every non-trivial leaf
|
||||||
|
has to solve the same set of problems by hand:
|
||||||
|
|
||||||
|
- select only the inbound packets that belong to this leaf
|
||||||
|
- distinguish one-shot procedures from long-running sessions
|
||||||
|
- group session packets by `hook_id`
|
||||||
|
- keep per-session application state
|
||||||
|
- build response packets with the right `hook_id`, path, procedure id, and `end_hook`
|
||||||
|
- retry failed outbound packets without losing stream progress
|
||||||
|
- remove session state only after final packets route successfully
|
||||||
|
- avoid consuming packets intended for other leaves
|
||||||
|
|
||||||
|
A remote PTY leaf makes this pain obvious. A PTY session is bidirectional and long
|
||||||
|
lived. The same hook carries `Open`, `Input`, `Resize`, `StdinEof`, `Output`, `Exit`,
|
||||||
|
and errors. The endpoint already owns hook authorization, but the leaf still needs a
|
||||||
|
safe session state machine.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Automate the repetitive `Leaf::update` machinery.
|
||||||
|
- Keep application logic explicit and testable.
|
||||||
|
- Keep `unshell-protocol` minimal and no_std-friendly.
|
||||||
|
- Keep OS-specific PTY code outside `unshell-protocol`.
|
||||||
|
- Preserve the new endpoint hook model: downward packets pave hooks, final packets close hooks.
|
||||||
|
- Make final-frame retries hard to get wrong.
|
||||||
|
- Keep generated runtime code static and size-conscious.
|
||||||
|
- Let sessions and procedures mutate shared leaf state without directly accessing each other.
|
||||||
|
- Make multiple sessions of the same type cheap and predictable.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Do not define a full actor framework.
|
||||||
|
- Do not add async requirements to the protocol layer.
|
||||||
|
- Do not make sessions discover or mutate other sessions directly.
|
||||||
|
- Do not introduce `Vec<Box<dyn Procedure>>` or `Vec<Box<dyn Session>>` runtime registries.
|
||||||
|
- Do not hide PTY business logic inside the macro.
|
||||||
|
- Do not add source-path fields to `Packet` just for PTY. The PTY `Open` payload can carry the reply path.
|
||||||
|
|
||||||
|
## Current Protocol Assumptions
|
||||||
|
|
||||||
|
The endpoint routing layer now owns hook lifetime:
|
||||||
|
|
||||||
|
```text
|
||||||
|
validated downward packet, end_hook=false -> open or refresh peer-bound hook
|
||||||
|
validated downward packet, end_hook=true -> close hook after successful route or delivery
|
||||||
|
validated upward packet -> require matching hook
|
||||||
|
validated upward packet, end_hook=true -> close hook after successful route or delivery
|
||||||
|
```
|
||||||
|
|
||||||
|
For PTY, this means:
|
||||||
|
|
||||||
|
- `Open` uses `end_hook = false` because it expects returned output.
|
||||||
|
- `Input`, `Resize`, `StdinEof`, and `Terminate` use `end_hook = false`.
|
||||||
|
- `StdinEof` is not `end_hook`. EOF closes stdin, not the whole PTY session.
|
||||||
|
- `Abort` may use `end_hook = true` if no acknowledgement is expected.
|
||||||
|
- `Output` uses `end_hook = false`.
|
||||||
|
- `Exit` or fatal `Error` uses `end_hook = true`.
|
||||||
|
|
||||||
|
## Crate Boundary
|
||||||
|
|
||||||
|
```text
|
||||||
|
unshell-protocol
|
||||||
|
Endpoint
|
||||||
|
Packet
|
||||||
|
Leaf
|
||||||
|
hook routing rules
|
||||||
|
filtered inbound drain API
|
||||||
|
|
||||||
|
unshell-runtime or unshell-leaves
|
||||||
|
real PTY worker implementation
|
||||||
|
std-only integrations
|
||||||
|
portable-pty adapter
|
||||||
|
|
||||||
|
unshell-macros
|
||||||
|
tiny proc-macro shim
|
||||||
|
|
||||||
|
unshell-macros-core
|
||||||
|
syn, quote, proc-macro2, deluxe or darling
|
||||||
|
parser and code generator tests
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated code runs in the final binary. Macro parsing dependencies do not.
|
||||||
|
That means `syn`, `quote`, `deluxe`, and `darling` are acceptable in the macro crates,
|
||||||
|
but not in the protocol runtime.
|
||||||
|
|
||||||
|
## Required Endpoint Addition
|
||||||
|
|
||||||
|
The generated leaf must be able to drain only the packets it owns. The current
|
||||||
|
`take_inbound_clear` drains a whole local queue, which is unsafe once multiple
|
||||||
|
application leaves share an endpoint.
|
||||||
|
|
||||||
|
Add a filtered drain API:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl Endpoint {
|
||||||
|
/// Drains packets from `local_id` that match `predicate`, preserving all other
|
||||||
|
/// packets in their original relative order.
|
||||||
|
pub fn take_inbound_matching<P, F>(&mut self, local_id: u32, predicate: P, f: F)
|
||||||
|
where
|
||||||
|
P: FnMut(&Packet) -> bool,
|
||||||
|
F: FnMut(&Packet);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For generated leaves, matching usually means:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
packet.procedure_id == PROC_PTY
|
||||||
|
|| packet.procedure_id == PROC_PING
|
||||||
|
|| packet.procedure_id == PROC_CAPABILITIES
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the one endpoint API the macro needs before it can be safe in mixed-leaf endpoints.
|
||||||
|
|
||||||
|
## User-Facing Macro
|
||||||
|
|
||||||
|
Use an attribute macro that wraps a user-owned state struct and generates a leaf type.
|
||||||
|
A derive macro alone cannot add storage fields to the original struct, so the macro
|
||||||
|
must generate a companion wrapper.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[unshell_leaf(
|
||||||
|
leaf = RemotePtyLeaf,
|
||||||
|
id = LEAF_REMOTE_PTY,
|
||||||
|
sessions(PtySession),
|
||||||
|
procedures(PingProcedure, CapabilitiesProcedure)
|
||||||
|
)]
|
||||||
|
pub struct RemotePtyState {
|
||||||
|
max_sessions: usize,
|
||||||
|
default_rows: u16,
|
||||||
|
default_cols: u16,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The macro emits roughly:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct RemotePtyLeaf {
|
||||||
|
state: RemotePtyState,
|
||||||
|
pty_sessions: SessionStore<PtySession>,
|
||||||
|
retry: PacketQueue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RemotePtyLeaf {
|
||||||
|
pub fn new(state: RemotePtyState) -> Self { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Leaf for RemotePtyLeaf {
|
||||||
|
fn get_id(&self) -> u32 {
|
||||||
|
LEAF_REMOTE_PTY
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, endpoint: &mut Endpoint) {
|
||||||
|
... generated dispatch ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The leaf wrapper is the object stored in `Endpoint::leaves`. The state struct stays
|
||||||
|
small and owned by the user.
|
||||||
|
|
||||||
|
## Core Model
|
||||||
|
|
||||||
|
```text
|
||||||
|
Endpoint inbound queue
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+-------------------------------+
|
||||||
|
| generated RemotePtyLeaf |
|
||||||
|
| |
|
||||||
|
| state: RemotePtyState |
|
||||||
|
| sessions: SessionStore |
|
||||||
|
| retry: PacketQueue |
|
||||||
|
+-------------------------------+
|
||||||
|
|
|
||||||
|
+--> Procedure packet -> Procedure::handle(...)
|
||||||
|
|
|
||||||
|
+--> Session packet -> by hook_id
|
||||||
|
create or update session
|
||||||
|
session queues outbound frames
|
||||||
|
generated flush handles retry
|
||||||
|
```
|
||||||
|
|
||||||
|
Procedures and sessions can both mutate `RemotePtyState`. They cannot directly
|
||||||
|
borrow or mutate each other. Communication between them must happen through leaf
|
||||||
|
state or through packets.
|
||||||
|
|
||||||
|
## Sessions
|
||||||
|
|
||||||
|
A session is a long-running hook-backed conversation. PTY is the main example.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait Session<L> {
|
||||||
|
/// All packets for this session type use this outer procedure id.
|
||||||
|
const PROCEDURE_ID: u32;
|
||||||
|
|
||||||
|
/// State owned for one active hook/session.
|
||||||
|
type State;
|
||||||
|
|
||||||
|
/// Attempts to create a new session from an incoming packet.
|
||||||
|
fn init(
|
||||||
|
leaf: &mut L,
|
||||||
|
packet: Packet,
|
||||||
|
ctx: &mut SessionInit,
|
||||||
|
) -> SessionInitResult<Self::State>;
|
||||||
|
|
||||||
|
/// Advances one session. The generated leaf passes all queued packets for this
|
||||||
|
/// hook and one context that can enqueue outbound frames.
|
||||||
|
fn update(
|
||||||
|
leaf: &mut L,
|
||||||
|
session: &mut Self::State,
|
||||||
|
incoming: &mut PacketQueue,
|
||||||
|
ctx: &mut SessionCtx,
|
||||||
|
) -> SessionStatus;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`L` is the user state type, for example `RemotePtyState`.
|
||||||
|
|
||||||
|
### Session Init Context
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct SessionInit {
|
||||||
|
hook_id: HookID,
|
||||||
|
packet_path: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SessionInitResult<S> {
|
||||||
|
Created(S),
|
||||||
|
Rejected,
|
||||||
|
RejectedWith(Packet),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`RejectedWith(Packet)` is intended for cases where the initializer can build a
|
||||||
|
protocol-level failure response, such as "too many PTY sessions". The generated leaf
|
||||||
|
still owns routing and retry for that packet.
|
||||||
|
|
||||||
|
The PTY `Open` payload should include the caller reply path because `Packet` does
|
||||||
|
not currently carry source path.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Open payload:
|
||||||
|
opcode
|
||||||
|
reply_path_len
|
||||||
|
reply_path segments
|
||||||
|
rows
|
||||||
|
cols
|
||||||
|
command/env/options
|
||||||
|
```
|
||||||
|
|
||||||
|
The session stores that reply path and uses it for upward output packets.
|
||||||
|
|
||||||
|
### Session Update Context
|
||||||
|
|
||||||
|
Sessions should use a context wrapper rather than directly constructing packets.
|
||||||
|
The context can still carry restricted endpoint access when absolutely necessary,
|
||||||
|
but the normal output path should be helper methods.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct SessionCtx<'a> {
|
||||||
|
endpoint: &'a mut Endpoint,
|
||||||
|
hook_id: HookID,
|
||||||
|
reply_path: &'a [u32],
|
||||||
|
procedure_id: u32,
|
||||||
|
outbox: &'a mut PacketQueue,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Helpers:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl<'a> SessionCtx<'a> {
|
||||||
|
pub fn send(&mut self, opcode: u8, data: &[u8]);
|
||||||
|
|
||||||
|
pub fn send_final(&mut self, opcode: u8, data: &[u8]);
|
||||||
|
|
||||||
|
pub fn error(&mut self, code: u8, data: &[u8]);
|
||||||
|
|
||||||
|
pub fn error_final(&mut self, code: u8, data: &[u8]);
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These helpers build packets like:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Packet {
|
||||||
|
hook_id: self.hook_id,
|
||||||
|
end_hook,
|
||||||
|
path: self.reply_path.to_vec(),
|
||||||
|
procedure_id: self.procedure_id,
|
||||||
|
data: encode_frame(opcode, payload),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The helper only queues packets. It does not route them immediately. The generated
|
||||||
|
leaf owns flushing and retry.
|
||||||
|
|
||||||
|
### Session Status
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum SessionStatus {
|
||||||
|
Running,
|
||||||
|
Closing,
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Closed` means the session has no more application work. The generated leaf still
|
||||||
|
must keep the session until any final packet has routed successfully.
|
||||||
|
|
||||||
|
## Procedures
|
||||||
|
|
||||||
|
A procedure is a one-packet operation. It is appropriate for introspection, ping,
|
||||||
|
capabilities, and simple state queries.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait Procedure<L> {
|
||||||
|
const PROCEDURE_ID: u32;
|
||||||
|
|
||||||
|
fn handle(
|
||||||
|
leaf: &mut L,
|
||||||
|
endpoint: &mut Endpoint,
|
||||||
|
packet: Packet,
|
||||||
|
out: &mut ProcedureOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Procedure helpers mirror session helpers but operate on one incoming packet:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct ProcedureOut {
|
||||||
|
hook_id: HookID,
|
||||||
|
reply_path: Vec<u32>,
|
||||||
|
procedure_id: u32,
|
||||||
|
outbox: PacketQueue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcedureOut {
|
||||||
|
pub fn send(&mut self, data: &[u8]);
|
||||||
|
|
||||||
|
pub fn send_final(&mut self, data: &[u8]);
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Procedures do not directly access sessions. If a procedure needs information about
|
||||||
|
sessions, that information should be mirrored into leaf state by the session code.
|
||||||
|
|
||||||
|
## PTY Binary Protocol
|
||||||
|
|
||||||
|
One PTY session uses one hook and one outer procedure id. The inner PTY protocol is
|
||||||
|
a tiny binary frame in `Packet::data`.
|
||||||
|
|
||||||
|
If API names use `end_state` at the session layer, it maps directly to
|
||||||
|
`Packet::end_hook` at the protocol layer.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Packet {
|
||||||
|
hook_id: session hook,
|
||||||
|
procedure_id: PROC_PTY,
|
||||||
|
data: [opcode, payload...],
|
||||||
|
end_hook: session lifetime marker,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Suggested opcodes:
|
||||||
|
|
||||||
|
| Opcode | Direction | Meaning | end_hook |
|
||||||
|
|---:|---|---|---|
|
||||||
|
| 0 | Downward | Open PTY | false |
|
||||||
|
| 1 | Upward | Opened | false |
|
||||||
|
| 2 | Downward | Input bytes | false |
|
||||||
|
| 3 | Downward | Resize | false |
|
||||||
|
| 4 | Downward | Stdin EOF | false |
|
||||||
|
| 5 | Downward | Terminate process | false |
|
||||||
|
| 6 | Downward | Abort session without acknowledgement | true |
|
||||||
|
| 7 | Upward | Output bytes | false |
|
||||||
|
| 8 | Upward | Exit status | true |
|
||||||
|
| 9 | Upward | Fatal error | true |
|
||||||
|
|
||||||
|
`StdinEof` must not set `end_hook = true`. The remote process may still emit output
|
||||||
|
after stdin closes.
|
||||||
|
|
||||||
|
## Generated Update Loop
|
||||||
|
|
||||||
|
The generated `Leaf::update` should follow this shape:
|
||||||
|
|
||||||
|
```text
|
||||||
|
update(endpoint)
|
||||||
|
1. flush retry queue first
|
||||||
|
2. drain matching inbound packets only
|
||||||
|
3. dispatch procedure packets
|
||||||
|
4. group session packets by hook_id
|
||||||
|
5. create sessions for open packets
|
||||||
|
6. update active sessions
|
||||||
|
7. flush outbound frames
|
||||||
|
8. remove sessions whose final packet routed successfully
|
||||||
|
```
|
||||||
|
|
||||||
|
More concrete flow:
|
||||||
|
|
||||||
|
```text
|
||||||
|
+----------------------------+
|
||||||
|
| generated update |
|
||||||
|
+----------------------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
flush retry packets
|
||||||
|
|
|
||||||
|
v
|
||||||
|
take_inbound_matching(local_id, owns_packet)
|
||||||
|
|
|
||||||
|
+--> procedure id match -> Procedure::handle
|
||||||
|
|
|
||||||
|
+--> session id match -> session inbox by hook_id
|
||||||
|
|
|
||||||
|
v
|
||||||
|
for each session inbox
|
||||||
|
|
|
||||||
|
+--> existing hook -> Session::update
|
||||||
|
|
|
||||||
|
+--> no hook -> Session::init, then Session::update
|
||||||
|
|
|
||||||
|
v
|
||||||
|
flush session/procedure outbox
|
||||||
|
|
|
||||||
|
+--> route success -> advance/remove when safe
|
||||||
|
|
|
||||||
|
+--> route failure -> keep retry packet and keep state
|
||||||
|
```
|
||||||
|
|
||||||
|
The retry rule is the most important generated invariant:
|
||||||
|
|
||||||
|
```text
|
||||||
|
If endpoint.add_outbound(packet) fails, the generated leaf must not:
|
||||||
|
- drop the packet
|
||||||
|
- advance a final frame
|
||||||
|
- remove the session
|
||||||
|
- consume state that cannot be reconstructed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generated Dispatch
|
||||||
|
|
||||||
|
The macro should emit static matches, not runtime registries.
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
match packet.procedure_id {
|
||||||
|
PtySession::PROCEDURE_ID => self.dispatch_pty_session(packet),
|
||||||
|
PingProcedure::PROCEDURE_ID => PingProcedure::handle(...),
|
||||||
|
CapabilitiesProcedure::PROCEDURE_ID => CapabilitiesProcedure::handle(...),
|
||||||
|
_ => self.handle_unknown(packet),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Vec<Box<dyn Procedure>>
|
||||||
|
Vec<Box<dyn Session>>
|
||||||
|
```
|
||||||
|
|
||||||
|
Static dispatch keeps the generated code visible to LLVM and avoids registry setup
|
||||||
|
costs in constrained binaries.
|
||||||
|
|
||||||
|
## Session Store
|
||||||
|
|
||||||
|
The first implementation can use a small `Vec` or `VecDeque` store.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct SessionEntry<S> {
|
||||||
|
hook_id: HookID,
|
||||||
|
state: S,
|
||||||
|
inbox: PacketQueue,
|
||||||
|
outbox: PacketQueue,
|
||||||
|
retry: Option<Packet>,
|
||||||
|
closing: bool,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For minimal binaries, a later implementation can replace this with a fixed-capacity
|
||||||
|
table under a feature flag.
|
||||||
|
|
||||||
|
Required operations:
|
||||||
|
|
||||||
|
- find by `hook_id`
|
||||||
|
- insert if capacity allows
|
||||||
|
- push incoming packet to inbox
|
||||||
|
- enqueue outbound packet
|
||||||
|
- retain session while retry packet exists
|
||||||
|
- remove only after session is closed and outbox/retry are empty
|
||||||
|
|
||||||
|
## Unknown Packets
|
||||||
|
|
||||||
|
The generated leaf should have configurable unknown-packet behavior.
|
||||||
|
|
||||||
|
Default behavior:
|
||||||
|
|
||||||
|
- unknown procedure id remains in the endpoint queue because `take_inbound_matching` does not drain it
|
||||||
|
- unknown session opcode for a known session produces a fatal error frame and closes the session
|
||||||
|
- packet for unknown hook under a session procedure produces a fatal error frame if the payload is not a valid `Open`
|
||||||
|
|
||||||
|
For PTY:
|
||||||
|
|
||||||
|
```text
|
||||||
|
unknown hook + Open opcode -> create session
|
||||||
|
unknown hook + non-Open opcode -> Error end_hook=true
|
||||||
|
known hook + known opcode -> session update
|
||||||
|
known hook + unknown opcode -> Error end_hook=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Access Boundaries
|
||||||
|
|
||||||
|
Sessions and procedures can mutate leaf state:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn update(leaf: &mut RemotePtyState, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
They should not directly access each other:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Procedure -> no direct SessionStore access
|
||||||
|
Session -> no direct Procedure access
|
||||||
|
```
|
||||||
|
|
||||||
|
If cross-cutting data is needed, mirror it into `RemotePtyState`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct RemotePtyState {
|
||||||
|
active_count: usize,
|
||||||
|
total_spawned: u64,
|
||||||
|
max_sessions: usize,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps borrowing simple and avoids turning the generated leaf into a shared
|
||||||
|
mutable object graph.
|
||||||
|
|
||||||
|
## Endpoint Access
|
||||||
|
|
||||||
|
The user proposed passing `&mut Endpoint` to sessions and procedures. The safest
|
||||||
|
design is slightly narrower:
|
||||||
|
|
||||||
|
- procedures may receive `&mut Endpoint` because they handle one packet and return immediately
|
||||||
|
- sessions should receive `SessionCtx`, which can expose narrow endpoint helpers
|
||||||
|
- a raw endpoint escape hatch can be added later if a real leaf proves it needs one
|
||||||
|
|
||||||
|
Rationale: sessions are long-lived and retry-sensitive. If session code calls
|
||||||
|
`endpoint.add_outbound` directly, it can bypass generated retry handling and lose a
|
||||||
|
final packet. The helper path keeps the dangerous part centralized.
|
||||||
|
|
||||||
|
## Remote PTY Example
|
||||||
|
|
||||||
|
User code:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[unshell_leaf(
|
||||||
|
leaf = RemotePtyLeaf,
|
||||||
|
id = LEAF_REMOTE_PTY,
|
||||||
|
sessions(PtySession),
|
||||||
|
procedures(PtyCapabilities)
|
||||||
|
)]
|
||||||
|
pub struct RemotePtyState {
|
||||||
|
max_sessions: usize,
|
||||||
|
default_rows: u16,
|
||||||
|
default_cols: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PtySession;
|
||||||
|
|
||||||
|
pub struct PtyState {
|
||||||
|
hook_id: HookID,
|
||||||
|
reply_path: Vec<u32>,
|
||||||
|
worker: PtyWorker,
|
||||||
|
saw_stdin_eof: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session<RemotePtyState> for PtySession {
|
||||||
|
const PROCEDURE_ID: u32 = PROC_PTY;
|
||||||
|
|
||||||
|
type State = PtyState;
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
leaf: &mut RemotePtyState,
|
||||||
|
packet: Packet,
|
||||||
|
ctx: &mut SessionInit,
|
||||||
|
) -> SessionInitResult<Self::State> {
|
||||||
|
let open = decode_open(&packet.data)?;
|
||||||
|
|
||||||
|
if leaf.active_count >= leaf.max_sessions {
|
||||||
|
return SessionInitResult::RejectedWith(pty_error_busy(packet.hook_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let worker = PtyWorker::spawn(open.command, open.rows, open.cols)?;
|
||||||
|
|
||||||
|
SessionInitResult::Created(PtyState {
|
||||||
|
hook_id: ctx.hook_id(),
|
||||||
|
reply_path: open.reply_path,
|
||||||
|
worker,
|
||||||
|
saw_stdin_eof: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
leaf: &mut RemotePtyState,
|
||||||
|
session: &mut Self::State,
|
||||||
|
incoming: &mut PacketQueue,
|
||||||
|
ctx: &mut SessionCtx,
|
||||||
|
) -> SessionStatus {
|
||||||
|
while let Some(packet) = incoming.pop_front() {
|
||||||
|
match decode_pty_frame(&packet.data) {
|
||||||
|
PtyFrame::Input(bytes) => session.worker.write(&bytes),
|
||||||
|
PtyFrame::Resize { rows, cols } => session.worker.resize(rows, cols),
|
||||||
|
PtyFrame::StdinEof => {
|
||||||
|
session.saw_stdin_eof = true;
|
||||||
|
session.worker.close_stdin();
|
||||||
|
}
|
||||||
|
PtyFrame::Terminate => session.worker.terminate(),
|
||||||
|
PtyFrame::Abort => return SessionStatus::Closed,
|
||||||
|
_ => ctx.error_final(ERR_BAD_OPCODE, b"bad pty opcode"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(bytes) = session.worker.poll_output() {
|
||||||
|
ctx.send(OP_OUTPUT, &bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(status) = session.worker.poll_exit() {
|
||||||
|
ctx.send_final(OP_EXIT, &encode_exit(status));
|
||||||
|
leaf.active_count -= 1;
|
||||||
|
return SessionStatus::Closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionStatus::Running
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The exact error handling syntax above is illustrative. The final API should avoid
|
||||||
|
`?` unless `SessionInitResult` has a clear conversion story.
|
||||||
|
|
||||||
|
## Generated Remote PTY Flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Caller Remote endpoint RemotePtyLeaf
|
||||||
|
------ --------------- -------------
|
||||||
|
allocate hook
|
||||||
|
Open end=false -------------> downward route opens hook ----> init PtyState
|
||||||
|
Input false -------------> hook remains active -----------> write PTY stdin
|
||||||
|
Resize false -------------> hook remains active -----------> resize PTY
|
||||||
|
StdinEof false -------------> hook remains active -----------> close PTY stdin
|
||||||
|
|
||||||
|
Output false <------------- upward route requires hook <---- ctx.send
|
||||||
|
Output false <------------- upward route requires hook <---- ctx.send
|
||||||
|
Exit true <------------- upward route closes hook <---- ctx.send_final
|
||||||
|
|
||||||
|
caller cleanup remove session
|
||||||
|
```
|
||||||
|
|
||||||
|
## Proc Macro Implementation
|
||||||
|
|
||||||
|
Rust still requires a proc-macro crate for real attribute macros. Keep that crate
|
||||||
|
thin and put the real logic in a normal testable crate.
|
||||||
|
|
||||||
|
```text
|
||||||
|
unshell-macros
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn unshell_leaf(attr, item) -> TokenStream
|
||||||
|
|
||||||
|
unshell-macros-core
|
||||||
|
parse UnshellLeafArgs
|
||||||
|
parse state struct
|
||||||
|
generate wrapper
|
||||||
|
generate Leaf impl
|
||||||
|
generate dispatch methods
|
||||||
|
tests over proc_macro2::TokenStream
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended parser helper:
|
||||||
|
|
||||||
|
- `deluxe` for attribute parsing if the desired syntax uses richer Rust tokens
|
||||||
|
- `darling` if the syntax stays close to normal Rust meta attributes
|
||||||
|
|
||||||
|
Current recommendation: use `deluxe` for attribute parsing and `syn`/`quote` for
|
||||||
|
code generation.
|
||||||
|
|
||||||
|
## Generated Code Requirements
|
||||||
|
|
||||||
|
- Must compile without requiring std in `unshell-protocol`.
|
||||||
|
- Must not allocate handler trait objects.
|
||||||
|
- Must preserve generic parameters and where clauses on the state struct.
|
||||||
|
- Must emit useful compile errors for duplicate procedure ids.
|
||||||
|
- Must emit useful compile errors for duplicate session procedure ids.
|
||||||
|
- Must reject session and procedure id collisions unless explicitly allowed.
|
||||||
|
- Must preserve user attributes that are not consumed by the macro.
|
||||||
|
- Must not require users to manually implement `Leaf` for the generated wrapper.
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
Phase 1 tests should use a fake PTY session, not `portable-pty`.
|
||||||
|
|
||||||
|
Protocol tests:
|
||||||
|
|
||||||
|
- `open_pty_paves_hook_and_creates_session`
|
||||||
|
- `input_and_output_share_one_hook`
|
||||||
|
- `stdin_eof_keeps_hook_until_exit`
|
||||||
|
- `exit_end_hook_cleans_route_and_session`
|
||||||
|
- `failed_final_exit_route_retries_without_losing_session`
|
||||||
|
- `abort_downward_end_hook_closes_without_ack`
|
||||||
|
- `unknown_session_input_returns_error_end_hook`
|
||||||
|
- `two_pty_sessions_interleave_without_crossing_hooks`
|
||||||
|
- `pty_leaf_does_not_consume_other_leaf_packets`
|
||||||
|
|
||||||
|
Macro tests:
|
||||||
|
|
||||||
|
- generated leaf implements `Leaf`
|
||||||
|
- duplicate procedure ids fail at compile time
|
||||||
|
- duplicate session procedure ids fail at compile time
|
||||||
|
- generic state structs expand correctly
|
||||||
|
- generated code routes final frames retry-safely
|
||||||
|
- generated code preserves unmatched inbound packets
|
||||||
|
|
||||||
|
Use `trybuild` for compile-fail macro tests once `unshell-macros` exists.
|
||||||
|
|
||||||
|
## Rollout Plan
|
||||||
|
|
||||||
|
1. Add `Endpoint::take_inbound_matching`.
|
||||||
|
2. Write a manual fake PTY leaf that follows the exact generated shape.
|
||||||
|
3. Add PTY session tests against the manual fake leaf.
|
||||||
|
4. Create `unshell-macros` and `unshell-macros-core`.
|
||||||
|
5. Generate the same shape as the manual fake leaf.
|
||||||
|
6. Port fake PTY tests to the generated leaf.
|
||||||
|
7. Add compile-fail macro tests.
|
||||||
|
8. Implement the real std-only PTY worker in `unshell-leaves` or `unshell-runtime`.
|
||||||
|
9. Add integration tests for the real worker where the platform supports PTYs.
|
||||||
|
|
||||||
|
This avoids writing a macro against an unproven design. First make the generated
|
||||||
|
shape real by hand, then teach the macro to emit it.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Should `SessionCtx` expose any raw `Endpoint` access, or only narrow helpers?
|
||||||
|
- Should session stores be `Vec` first, or should fixed-capacity storage be designed immediately?
|
||||||
|
- Should unknown opcodes produce an error packet by default, or should each session type decide?
|
||||||
|
- Should `Open` always carry `reply_path`, or should the packet format eventually add source path?
|
||||||
|
- Should `ProcedureOut` be retry-safe like session output, or are procedures allowed to fail fast?
|
||||||
|
- Should macro-generated leaves expose counters for active sessions and retry queue depth?
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
Implement this as static generated code with a narrow context API.
|
||||||
|
|
||||||
|
Do not build a runtime plugin system. Do not give sessions raw access to session
|
||||||
|
stores. Do not let session code bypass generated output flushing. The macro should
|
||||||
|
make the correct routing and hook behavior the easiest path, especially for final
|
||||||
|
frames.
|
||||||
|
|
||||||
|
The first proof point should be fake PTY. If fake PTY works cleanly, real PTY is
|
||||||
|
mostly OS plumbing.
|
||||||
@@ -6,7 +6,7 @@ macro_rules! hashtest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAP: [(&'static str, u32); 6] = [
|
const MAP: [(&str, u32); 6] = [
|
||||||
hashtest!("abc123"),
|
hashtest!("abc123"),
|
||||||
hashtest!("abc124"),
|
hashtest!("abc124"),
|
||||||
hashtest!("abc125"),
|
hashtest!("abc125"),
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
//! Temporary hash function
|
//! Temporary hash function
|
||||||
|
|
||||||
const fn hash_recursive<'a>(state: &mut [u8; 4], input: &'a [u8]) {
|
const fn hash_recursive(state: &mut [u8; 4], input: &[u8]) {
|
||||||
match input.len() {
|
match input.len() {
|
||||||
3 => {
|
3 => {
|
||||||
state[0] ^= input[0];
|
state[0] ^= input[0];
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
+3
-6
@@ -6,6 +6,7 @@
|
|||||||
//! ## Architecture
|
//! ## Architecture
|
||||||
//!
|
//!
|
||||||
//! - [`protocol`] - Wire types, framing, stateless validation, and routing/runtime.
|
//! - [`protocol`] - Wire types, framing, stateless validation, and routing/runtime.
|
||||||
|
//! - [`interface`] - Typed control surfaces used by UI adapters and control leaves.
|
||||||
//!
|
//!
|
||||||
//! The library requires `alloc` for path and payload management.
|
//! The library requires `alloc` for path and payload management.
|
||||||
|
|
||||||
@@ -16,12 +17,8 @@
|
|||||||
pub extern crate alloc;
|
pub extern crate alloc;
|
||||||
|
|
||||||
mod hash;
|
mod hash;
|
||||||
|
pub mod interface;
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
|
pub mod protocol;
|
||||||
pub mod protocol {
|
|
||||||
pub use unshell_protocol::*;
|
|
||||||
|
|
||||||
pub use unshell_macros::unshell_leaf;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use hash::hash;
|
pub use hash::hash;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Endpoint, EndpointError, EndpointName};
|
use crate::protocol::{Endpoint, EndpointError, EndpointName};
|
||||||
|
|
||||||
/// Compact identifier for one routed return channel.
|
/// Compact identifier for one routed return channel.
|
||||||
///
|
///
|
||||||
@@ -5,7 +5,7 @@ pub use hooks::HookID;
|
|||||||
|
|
||||||
use alloc::{boxed::Box, vec::Vec};
|
use alloc::{boxed::Box, vec::Vec};
|
||||||
|
|
||||||
use crate::{ConnectionSet, HookMap, Leaf, Packet, Path, RouteMap};
|
use crate::protocol::{ConnectionSet, HookMap, Leaf, Packet, Path, RouteMap};
|
||||||
|
|
||||||
pub struct Endpoint {
|
pub struct Endpoint {
|
||||||
// This endpoint's identifier
|
// This endpoint's identifier
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Endpoint, EndpointError, Packet, RouteDirection};
|
use crate::protocol::{Endpoint, EndpointError, Packet, RouteDirection};
|
||||||
|
|
||||||
impl Endpoint {
|
impl Endpoint {
|
||||||
/// Register an inbound packet from legacy trusted code.
|
/// Register an inbound packet from legacy trusted code.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Endpoint, HookID, Packet, PacketQueue};
|
use crate::protocol::{Endpoint, HookID, Packet, PacketQueue};
|
||||||
|
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
#![no_std]
|
|
||||||
|
|
||||||
pub extern crate alloc;
|
|
||||||
|
|
||||||
mod endpoint;
|
mod endpoint;
|
||||||
mod error;
|
mod error;
|
||||||
mod leaf;
|
mod leaf;
|
||||||
@@ -11,6 +7,7 @@ pub use endpoint::{Endpoint, HookID};
|
|||||||
pub use error::*;
|
pub use error::*;
|
||||||
pub use leaf::*;
|
pub use leaf::*;
|
||||||
pub use packet::Packet;
|
pub use packet::Packet;
|
||||||
|
pub use unshell_macros::unshell_leaf;
|
||||||
|
|
||||||
// Various named types used for brevity
|
// Various named types used for brevity
|
||||||
use alloc::{
|
use alloc::{
|
||||||
@@ -2,7 +2,7 @@ extern crate alloc;
|
|||||||
|
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
use crate::{DeserializeError, SerializeError};
|
use crate::protocol::{DeserializeError, SerializeError};
|
||||||
|
|
||||||
/// Fully decoded UnShell test packet.
|
/// Fully decoded UnShell test packet.
|
||||||
///
|
///
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
use alloc::{boxed::Box, rc::Rc, vec};
|
use alloc::{boxed::Box, rc::Rc, vec};
|
||||||
use core::cell::RefCell;
|
use core::cell::RefCell;
|
||||||
|
|
||||||
use crate::Endpoint;
|
use crate::protocol::Endpoint;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
constants::{ENDPOINT_CALLER, ENDPOINT_RESPONDENT},
|
constants::{ENDPOINT_CALLER, ENDPOINT_RESPONDENT},
|
||||||
+1
-1
@@ -3,7 +3,7 @@ use core::cell::RefCell;
|
|||||||
|
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
|
|
||||||
use crate::{Endpoint, Leaf, Packet};
|
use crate::protocol::{Endpoint, Leaf, Packet};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
codec::{decode_block_chunk, decode_child_summary, decode_u32},
|
codec::{decode_block_chunk, decode_child_summary, decode_u32},
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
use alloc::{vec, vec::Vec};
|
use alloc::{vec, vec::Vec};
|
||||||
|
|
||||||
use crate::Packet;
|
use crate::protocol::Packet;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
codec::{encode_block_chunk, encode_child_summary, encode_u32},
|
codec::{encode_block_chunk, encode_child_summary, encode_u32},
|
||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
|
use crate::protocol::Packet;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
rpc::OutgoingFrame,
|
rpc::OutgoingFrame,
|
||||||
tree::{BlockChunk, ChildSummary},
|
tree::{BlockChunk, ChildSummary},
|
||||||
@@ -76,7 +78,7 @@ impl ResponseStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the next packet without advancing the stream.
|
/// Builds the next packet without advancing the stream.
|
||||||
pub(super) fn next_packet(&self) -> Option<crate::Packet> {
|
pub(super) fn next_packet(&self) -> Option<Packet> {
|
||||||
let frame = self.frames.get(self.next_index)?;
|
let frame = self.frames.get(self.next_index)?;
|
||||||
Some(frame.to_packet(self.hook_id, self.next_index + 1 == self.frames.len()))
|
Some(frame.to_packet(self.hook_id, self.next_index + 1 == self.frames.len()))
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
mod streams;
|
mod streams;
|
||||||
mod support;
|
mod support;
|
||||||
|
|
||||||
use crate::{Endpoint, EndpointError, RouteDirection};
|
use crate::protocol::{Endpoint, EndpointError, RouteDirection};
|
||||||
|
|
||||||
use alloc::{boxed::Box, vec};
|
use alloc::{boxed::Box, vec};
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ fn test_oneshot() {
|
|||||||
let (tx_a, rx_a) = crossbeam_channel::unbounded();
|
let (tx_a, rx_a) = crossbeam_channel::unbounded();
|
||||||
let (tx_b, rx_b) = crossbeam_channel::unbounded();
|
let (tx_b, rx_b) = crossbeam_channel::unbounded();
|
||||||
|
|
||||||
let mut endpoint_a = crate::endpoint::Endpoint::new(
|
let mut endpoint_a = Endpoint::new(
|
||||||
ENDPOINT_A,
|
ENDPOINT_A,
|
||||||
vec![
|
vec![
|
||||||
Box::new(ControllerLeaf { has_run: false }),
|
Box::new(ControllerLeaf { has_run: false }),
|
||||||
@@ -31,7 +31,7 @@ fn test_oneshot() {
|
|||||||
);
|
);
|
||||||
endpoint_a.path = vec![ENDPOINT_A];
|
endpoint_a.path = vec![ENDPOINT_A];
|
||||||
|
|
||||||
let mut endpoint_b = crate::endpoint::Endpoint::new(
|
let mut endpoint_b = Endpoint::new(
|
||||||
ENDPOINT_B,
|
ENDPOINT_B,
|
||||||
vec![
|
vec![
|
||||||
Box::new(ResponderLeaf),
|
Box::new(ResponderLeaf),
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Endpoint, Leaf, Packet};
|
use crate::protocol::{Endpoint, Leaf, Packet};
|
||||||
|
|
||||||
use alloc::{boxed::Box, format, vec, vec::Vec};
|
use alloc::{boxed::Box, format, vec, vec::Vec};
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Endpoint, Leaf, Packet};
|
use crate::protocol::{Endpoint, Leaf, Packet};
|
||||||
|
|
||||||
use alloc::{vec, vec::Vec};
|
use alloc::{vec, vec::Vec};
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use alloc::{vec, vec::Vec};
|
use alloc::{vec, vec::Vec};
|
||||||
|
|
||||||
use crate::{DeserializeError, EndpointError, Packet, SerializeError};
|
use crate::protocol::{DeserializeError, EndpointError, Packet, SerializeError};
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ impl LeafGenerator {
|
|||||||
|
|
||||||
fn __unshell_parent_reply_path(
|
fn __unshell_parent_reply_path(
|
||||||
endpoint: &::unshell::protocol::Endpoint,
|
endpoint: &::unshell::protocol::Endpoint,
|
||||||
) -> ::unshell::protocol::alloc::vec::Vec<u32> {
|
) -> ::unshell::alloc::vec::Vec<u32> {
|
||||||
if endpoint.path.len() > 1 {
|
if endpoint.path.len() > 1 {
|
||||||
endpoint.path[..endpoint.path.len() - 1].to_vec()
|
endpoint.path[..endpoint.path.len() - 1].to_vec()
|
||||||
} else {
|
} else {
|
||||||
@@ -135,7 +135,7 @@ impl LeafGenerator {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut __unshell_packets = ::unshell::protocol::alloc::vec::Vec::new();
|
let mut __unshell_packets = ::unshell::alloc::vec::Vec::new();
|
||||||
endpoint.take_inbound_matching(
|
endpoint.take_inbound_matching(
|
||||||
__unshell_local_id,
|
__unshell_local_id,
|
||||||
Self::__unshell_packet_is_owned,
|
Self::__unshell_packet_is_owned,
|
||||||
@@ -177,7 +177,7 @@ impl LeafGenerator {
|
|||||||
let field = &store.field;
|
let field = &store.field;
|
||||||
let session_ty = &store.ty;
|
let session_ty = &store.ty;
|
||||||
quote! {
|
quote! {
|
||||||
#field: ::unshell::protocol::alloc::vec::Vec<
|
#field: ::unshell::alloc::vec::Vec<
|
||||||
::unshell::protocol::SessionEntry<
|
::unshell::protocol::SessionEntry<
|
||||||
<#session_ty as ::unshell::protocol::Session<#state_type>>::State
|
<#session_ty as ::unshell::protocol::Session<#state_type>>::State
|
||||||
>
|
>
|
||||||
@@ -193,7 +193,7 @@ impl LeafGenerator {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|store| {
|
.map(|store| {
|
||||||
let field = &store.field;
|
let field = &store.field;
|
||||||
quote!(#field: ::unshell::protocol::alloc::vec::Vec::new())
|
quote!(#field: ::unshell::alloc::vec::Vec::new())
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "unshell-protocol"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
description = "Wire protocol, framing, validation, and endpoint runtime for UnShell"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
doctest = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rkyv = { workspace = true }
|
|
||||||
# unshell-macros = { path = "../unshell-macros" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
crossbeam-channel.workspace = true
|
|
||||||
|
|
||||||
[lints.rust]
|
|
||||||
elided_lifetimes_in_paths = "warn"
|
|
||||||
future_incompatible = { level = "warn", priority = -1 }
|
|
||||||
nonstandard_style = { level = "warn", priority = -1 }
|
|
||||||
rust_2018_idioms = { level = "warn", priority = -1 }
|
|
||||||
rust_2021_prelude_collisions = "warn"
|
|
||||||
semicolon_in_expressions_from_macros = "warn"
|
|
||||||
unsafe_op_in_unsafe_fn = "warn"
|
|
||||||
unused_import_braces = "warn"
|
|
||||||
unused_lifetimes = "warn"
|
|
||||||
trivial_casts = "allow"
|
|
||||||
# missing_docs = "warn"
|
|
||||||
Reference in New Issue
Block a user