Merge branch 'main' of git.arirex.me:iris/ai-app
# Conflicts: # AGENTS.md # PLAN.md # app/androidApp/src/main/kotlin/com/example/aiapp/SessionUsageBar.kt # app/androidApp/src/main/kotlin/com/example/aiapp/SpawnScreen.kt # server/src/config.rs # server/src/main.rs # server/src/routes.rs # server/src/session/echo.rs # server/src/session/llama.rs # server/src/session/transport.rs # server/src/ssh.rs # server/src/usage.rs
This commit is contained in:
commit
3c0214ece8
94 files changed
+8299
-9454
No files matched your search
@@ -1,26 +1,23 @@
|
||||
//! Where a session's process runs, and the only place that knows how.
|
||||
//!
|
||||
//! A driver says *what* to run -- a [`Launch`] -- and hands it here.
|
||||
//! Whether that becomes a child of this process or an `ssh host …`
|
||||
//! invocation is settled in this module, so a driver carries no transport
|
||||
//! knowledge and a second one cannot forget to handle the remote case. It
|
||||
//! also means the wrapping is honest about drivers that run nothing at
|
||||
//! all: `EchoDriver` builds no [`Launch`], so there is nothing to wrap and
|
||||
//! no host for it to appear to honour.
|
||||
//! A driver says *what* to run -- a [`Launch`] -- and hands it here. Whether
|
||||
//! that becomes a child of this process or an `ssh host …` invocation is
|
||||
//! settled in this module, so a driver carries no transport knowledge and a
|
||||
//! second one cannot forget to handle the remote case. It also means the
|
||||
//! wrapping is honest about drivers that run nothing at all: `EchoDriver`
|
||||
//! builds no [`Launch`], so there is no host for it to appear to honour.
|
||||
//!
|
||||
//! The quoting, the forced ssh options and the remote script are
|
||||
//! `crate::ssh`'s, which this dispatches to. That split is deliberate:
|
||||
//! this module decides *which* transport, that one knows what a correct
|
||||
//! ssh invocation is.
|
||||
//! `crate::ssh`'s: this module decides *which* transport, that one knows what a
|
||||
//! correct ssh invocation is.
|
||||
//!
|
||||
//! A transport is therefore two operations rather than one: **run this**,
|
||||
//! and **reach this port**. The second is what a managed `llama-server`
|
||||
//! needs -- it is spawned as a process and then spoken to over HTTP -- and
|
||||
//! it is a no-op locally, where the port a program binds is already a port
|
||||
//! this machine can dial. Over ssh it is an `-L` tunnel carried by the
|
||||
//! same connection that runs the command, so the model server binds
|
||||
//! loopback on the far machine and is never exposed to its network. See
|
||||
//! [`Transport::reserve_port`] and PLAN.md's SSH section.
|
||||
//! A transport is therefore two operations rather than one: **run this** and
|
||||
//! **reach this port**. The second is what a managed `llama-server` needs -- it
|
||||
//! is spawned as a process and then spoken to over HTTP -- and it is a no-op
|
||||
//! locally, where the port a program binds is already one this machine can
|
||||
//! dial. Over ssh it is an `-L` tunnel on the same connection that runs the
|
||||
//! command, so the model server binds loopback on the far machine and is never
|
||||
//! exposed to its network. See [`Transport::reserve_port`].
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
@@ -31,12 +28,10 @@ use tokio::process::Child;
|
||||
use crate::config::SshConfig;
|
||||
pub use crate::ssh::Forward;
|
||||
|
||||
/// What a driver needs run in order to exist as a process.
|
||||
///
|
||||
/// Deliberately just what every transport can carry: the command, where
|
||||
/// it runs, and a port the caller needs to reach. Anything a particular
|
||||
/// machine needs -- a key, extra ssh options, which address to dial -- is
|
||||
/// the transport's own configuration, not something a driver states.
|
||||
/// What a driver needs run in order to exist as a process. Deliberately just
|
||||
/// what every transport can carry -- the command, where it runs, and a port the
|
||||
/// caller needs to reach; anything a particular machine needs is the
|
||||
/// transport's own configuration, not something a driver states.
|
||||
pub struct Launch {
|
||||
pub program: String,
|
||||
pub args: Vec<String>,
|
||||
@@ -74,22 +69,20 @@ impl Launch {
|
||||
/// How a launched process's standard streams are connected.
|
||||
///
|
||||
/// The choice is not the transport's and not the driver's dialect: it is
|
||||
/// whether the process is expected to outlive this server. A probe is
|
||||
/// asked a question and answers within one call, so pipes this server
|
||||
/// drains are right and dying with it is right. A session is a
|
||||
/// whether the process is expected to outlive this server. A probe answers
|
||||
/// within one call, so pipes this server drains are right. A session is a
|
||||
/// conversation somebody is having, so its streams live in the session
|
||||
/// directory where a later run of this server can pick them up again --
|
||||
/// see `session::process`.
|
||||
/// directory where a later run of this server can pick them up.
|
||||
pub enum Streams {
|
||||
/// Pipes owned by this server; the child is killed when they drop.
|
||||
Piped,
|
||||
/// The same, except that stdin is already open on something this
|
||||
/// server holds -- the file being copied to another machine. Bytes
|
||||
/// this process has in memory do not need this: [`Streams::Piped`]
|
||||
/// gives a pipe to write them into as the child reads.
|
||||
/// The same, except that stdin is already open on something this server
|
||||
/// holds -- the file being copied to another machine. Bytes this process has
|
||||
/// in memory do not need this: [`Streams::Piped`] gives a pipe to write them
|
||||
/// into as the child reads.
|
||||
PipedFrom(Stdio),
|
||||
/// Files -- and, for stdin, a fifo the child itself holds open so it
|
||||
/// never reads EOF -- that outlast this process.
|
||||
/// Files -- and, for stdin, a fifo the child itself holds open so it never
|
||||
/// reads EOF -- that outlast this process.
|
||||
Detached {
|
||||
stdin: Stdio,
|
||||
stdout: Stdio,
|
||||
@@ -103,8 +96,7 @@ pub enum Transport {
|
||||
Here,
|
||||
/// Reached with the system `ssh` client. Owns its entry rather than
|
||||
/// borrowing it, so a session keeps working against the config it was
|
||||
/// spawned with even if the setup is edited afterwards. Carries the
|
||||
/// setup's name only to say where things are running.
|
||||
/// spawned with even if the setup is edited afterwards.
|
||||
Ssh { name: String, ssh: SshConfig },
|
||||
}
|
||||
|
||||
@@ -120,12 +112,10 @@ impl Transport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts `launch` with its streams connected as `streams` says.
|
||||
///
|
||||
/// The failure names what to check, and the two transports fail for
|
||||
/// genuinely different reasons -- a missing ssh client here versus a
|
||||
/// program that is not on the remote PATH -- so each says its own
|
||||
/// thing rather than one message hedging between them.
|
||||
/// Starts `launch` with its streams connected as `streams` says. The failure
|
||||
/// names what to check, and the two transports fail for genuinely different
|
||||
/// reasons -- a missing ssh client here versus a program not on the remote
|
||||
/// PATH -- so each says its own thing.
|
||||
pub fn spawn(&self, launch: &Launch, streams: Streams) -> Result<Child> {
|
||||
let host = match self {
|
||||
Self::Here => None,
|
||||
@@ -159,11 +149,9 @@ impl Transport {
|
||||
stderr,
|
||||
} => {
|
||||
command.stdin(stdin).stdout(stdout).stderr(stderr);
|
||||
// No `kill_on_drop`: outliving this server is the point.
|
||||
// Its own process group as well, so a signal sent to the
|
||||
// server's group -- which is how a terminal or a
|
||||
// supervisor stops it -- does not travel to a session that
|
||||
// is meant to survive being stopped.
|
||||
// No `kill_on_drop`: outliving this server is the point. Its own
|
||||
// process group as well, so a signal sent to the server's group
|
||||
// does not travel to a session meant to survive being stopped.
|
||||
command.process_group(0);
|
||||
}
|
||||
}
|
||||
@@ -181,13 +169,10 @@ impl Transport {
|
||||
})
|
||||
}
|
||||
|
||||
/// Runs `launch` to completion and returns its stdout, blocking.
|
||||
///
|
||||
/// The synchronous twin of `capture`, for callers that are already on a
|
||||
/// blocking task and would otherwise need a runtime to ask a machine a
|
||||
/// question. Both build the invocation the same way -- see
|
||||
/// `crate::ssh::command` -- so there is still only one description of
|
||||
/// what running something on another machine means.
|
||||
/// Runs `launch` to completion and returns its stdout, blocking. The
|
||||
/// synchronous twin of `capture`, for callers already on a blocking task that
|
||||
/// would otherwise need a runtime to ask a machine a question. Both build the
|
||||
/// invocation the same way.
|
||||
pub fn capture_blocking(&self, launch: &Launch) -> Result<String> {
|
||||
let host = match self {
|
||||
Self::Here => None,
|
||||
@@ -216,21 +201,19 @@ impl Transport {
|
||||
/// Runs `launch` with `input` on its stdin and reports everything it
|
||||
/// produced -- stdout as bytes, stderr as text, and the exit status.
|
||||
///
|
||||
/// The one description of "run this there, with this on stdin", so
|
||||
/// that shipping an attachment and writing a file through the explorer
|
||||
/// are the same operation rather than two. It is also the only capture
|
||||
/// that hands back the **status**: a script can then answer with an
|
||||
/// exit code the caller distinguishes (the explorer's write says
|
||||
/// `exit 3` for "this file is not the one you read"), which
|
||||
/// [`Transport::capture`] cannot express because it turns every
|
||||
/// failure into one error.
|
||||
/// The one description of "run this there, with this on stdin", so that
|
||||
/// shipping an attachment and writing a file through the explorer are the
|
||||
/// same operation rather than two. It is also the only capture that hands
|
||||
/// back the **status**: a script can answer with an exit code the caller
|
||||
/// distinguishes (the explorer's write says `exit 3` for "this file is not
|
||||
/// the one you read"), which [`Transport::capture`] cannot express.
|
||||
///
|
||||
/// Bytes rather than a `String`, because a file's contents are not
|
||||
/// text until something has checked, and lossy decoding would replace
|
||||
/// the evidence that they are not.
|
||||
/// Bytes rather than a `String`, because a file's contents are not text
|
||||
/// until something has checked, and lossy decoding would replace the
|
||||
/// evidence that they are not.
|
||||
///
|
||||
/// `Err` means the process could not be started at all; a process that
|
||||
/// ran and failed is a [`Captured`] with a status saying so.
|
||||
/// `Err` means the process could not be started at all; a process that ran
|
||||
/// and failed is a [`Captured`] with a status saying so.
|
||||
pub async fn capture_with_input(&self, launch: &Launch, input: Input) -> Result<Captured> {
|
||||
let (streams, to_write) = match input {
|
||||
Input::None => (Streams::Piped, None),
|
||||
@@ -239,13 +222,11 @@ impl Transport {
|
||||
};
|
||||
let mut child = self.spawn(launch, streams)?;
|
||||
if let Some(bytes) = to_write {
|
||||
// Written from a task rather than before the wait, because the
|
||||
// child may not read all of it -- the write script exits
|
||||
// without reading when the file has changed underneath -- and
|
||||
// a caller blocked on filling a pipe nobody is draining would
|
||||
// deadlock instead of getting that answer. The broken pipe is
|
||||
// the expected end of this write, so it is dropped: what
|
||||
// happened is the exit status below.
|
||||
// Written from a task rather than before the wait, because the child
|
||||
// may not read all of it -- the write script exits without reading
|
||||
// when the file has changed underneath -- and a caller blocked on
|
||||
// filling a pipe nobody is draining would deadlock instead of getting
|
||||
// that answer. The broken pipe is the expected end of this write.
|
||||
let mut stdin = child.stdin.take().context("the child has no stdin")?;
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -308,11 +289,11 @@ const FAR_PORTS: std::ops::Range<u16> = 20000..30000;
|
||||
|
||||
/// What a command is given on its standard input.
|
||||
///
|
||||
/// Three cases rather than an `Option<Stdio>` because they are three
|
||||
/// genuinely different arrangements and only this knows which: nothing to
|
||||
/// say, bytes this process is holding, or a file it has open. The last one
|
||||
/// is how a several-hundred-megabyte attachment reaches another machine
|
||||
/// without passing through this server's memory.
|
||||
/// Three cases rather than an `Option<Stdio>` because they are three genuinely
|
||||
/// different arrangements and only this knows which: nothing to say, bytes this
|
||||
/// process is holding, or a file it has open. The last is how a
|
||||
/// several-hundred-megabyte attachment reaches another machine without passing
|
||||
/// through this server's memory.
|
||||
pub enum Input {
|
||||
None,
|
||||
Bytes(Vec<u8>),
|
||||
@@ -323,10 +304,9 @@ pub enum Input {
|
||||
pub struct Captured {
|
||||
pub status: std::process::ExitStatus,
|
||||
pub stdout: Vec<u8>,
|
||||
/// Trimmed, and what a failure is reported as: ssh's own refusals and
|
||||
/// a tool's own message about the file it could not open are both the
|
||||
/// useful half of why something did not work, and both are written to
|
||||
/// name the thing.
|
||||
/// Trimmed, and what a failure is reported as: ssh's own refusals and a
|
||||
/// tool's own message about the file it could not open are both the useful
|
||||
/// half of why something did not work.
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user