Improve CLI and daemon lifecycle usability
This commit is contained in:
parent
afec7063db
commit
f61cb44dad
14 changed files with 1028 additions and 204 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1551,6 +1551,7 @@ dependencies = [
|
|||
"geth-node",
|
||||
"geth-pipe",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
|
|
|||
61
README.md
61
README.md
|
|
@ -8,28 +8,25 @@ still named `geth`.
|
|||
|
||||
## First 10 Minutes
|
||||
|
||||
Build the single binary, initialize an isolated home, start the daemon, and
|
||||
verify the local control path:
|
||||
Build the single binary and start a disposable daemon:
|
||||
|
||||
```sh
|
||||
cargo build -p geth
|
||||
export GETH_HOME="$(mktemp -d)"
|
||||
printf 'GETH_HOME=%s\n' "$GETH_HOME"
|
||||
./target/debug/geth init
|
||||
./target/debug/geth daemon run
|
||||
./target/debug/geth daemon run --ephemeral
|
||||
```
|
||||
|
||||
Keep the daemon running. In a second shell, reuse the printed `GETH_HOME` value:
|
||||
Keep the daemon running. In a second shell, reuse the home it printed:
|
||||
|
||||
```sh
|
||||
export GETH_HOME="<same directory>"
|
||||
./target/debug/geth wait daemon --timeout-ms 30000
|
||||
./target/debug/geth status --json
|
||||
./target/debug/geth doctor --json
|
||||
./target/debug/geth --home <printed-path> wait daemon
|
||||
./target/debug/geth --home <printed-path> status --json
|
||||
./target/debug/geth --home <printed-path> doctor --json
|
||||
```
|
||||
|
||||
This creates a local-only identity for evaluation. Before enrolling other
|
||||
machines, use the owner setup in `geth guide owner-setup`; it records an
|
||||
Ctrl-C stops the daemon and removes its temporary state. For a persistent
|
||||
background node, run `geth daemon install`; this initializes local state,
|
||||
installs a service for the current user, and starts it immediately. Before
|
||||
enrolling other machines, use `geth guide owner-setup`; it records an
|
||||
OpenSSH admin public key as the trust anchor and signs the initial keychain
|
||||
statements without copying the private key into geth state.
|
||||
|
||||
|
|
@ -43,6 +40,8 @@ Start with these documents when moving beyond the local smoke test:
|
|||
and signed-operation compatibility rules.
|
||||
- [`docs/automation-examples.md`](docs/automation-examples.md): shell, Python,
|
||||
and user-service examples.
|
||||
- [`docs/user-workflows.md`](docs/user-workflows.md): operator stories from
|
||||
first run through enrollment, sync, automation, and recovery.
|
||||
- [`docs/production-readiness-roadmap.md`](docs/production-readiness-roadmap.md):
|
||||
the pre-deployment gate and its current status.
|
||||
- [`docs/dogfood-checklist.md`](docs/dogfood-checklist.md): required
|
||||
|
|
@ -57,7 +56,7 @@ It has daemon mode and control mode:
|
|||
```sh
|
||||
geth init
|
||||
geth daemon run
|
||||
geth daemon service install
|
||||
geth daemon install
|
||||
geth status
|
||||
geth node id
|
||||
geth resource list
|
||||
|
|
@ -91,14 +90,15 @@ signed records.
|
|||
The daemon can also install itself as a user service:
|
||||
|
||||
```sh
|
||||
geth daemon service install
|
||||
geth daemon service status
|
||||
geth daemon service uninstall
|
||||
geth daemon install
|
||||
geth daemon status
|
||||
geth daemon uninstall
|
||||
```
|
||||
|
||||
The bootstrap service managers are systemd user units on Linux, launchd user
|
||||
agents on macOS, and per-user scheduled tasks on Windows. These are user-level
|
||||
services, not system services.
|
||||
services, not system services. The longer `geth daemon service ...` family is
|
||||
retained for compatibility and advanced options.
|
||||
|
||||
## Transport And SSH
|
||||
|
||||
|
|
@ -131,14 +131,15 @@ metadata from an authorized peer over Iroh.
|
|||
|
||||
The bootstrap implementation provides:
|
||||
|
||||
- `geth guide [init|owner-setup|enrollment|keys|overlay|service|completions|smoke-test]` for
|
||||
- `geth guide [quickstart|init|owner-setup|enrollment|keys|overlay|service|completions|smoke-test]` for
|
||||
embedded workflow help, including `--admin-key` / `--signing-key` setup
|
||||
examples
|
||||
- `geth completions <bash|zsh|fish|powershell|elvish>` for shell completion
|
||||
scripts generated from the live CLI command tree
|
||||
- `geth init`
|
||||
- `geth init --admin-key <public-key> --signing-key <private-key> --node-name <name>`
|
||||
- `geth daemon run`
|
||||
- `geth daemon run [--ephemeral]`
|
||||
- `geth daemon install|start|stop|status|uninstall`
|
||||
- `geth daemon service install|uninstall|start|stop|status|print`
|
||||
- `geth status`
|
||||
- `geth wait daemon|peer|sync --timeout-ms <ms>`
|
||||
|
|
@ -444,25 +445,25 @@ $GETH_HOME/
|
|||
|
||||
## Quick Start
|
||||
|
||||
In one shell:
|
||||
For a disposable evaluation, run this in one shell:
|
||||
|
||||
```sh
|
||||
export GETH_HOME="$(mktemp -d)"
|
||||
cargo run -p geth -- init
|
||||
cargo run -p geth -- daemon run
|
||||
cargo run -p geth -- daemon run --ephemeral
|
||||
```
|
||||
|
||||
In another shell:
|
||||
In another shell, use the home it prints:
|
||||
|
||||
```sh
|
||||
export GETH_HOME="<same dir>"
|
||||
cargo run -p geth -- status
|
||||
cargo run -p geth -- node id
|
||||
cargo run -p geth -- --home <printed-path> status
|
||||
cargo run -p geth -- --home <printed-path> node id
|
||||
echo "hello geth" > /tmp/hello-geth.txt
|
||||
cargo run -p geth -- cas add /tmp/hello-geth.txt
|
||||
cargo run -p geth -- cas list
|
||||
cargo run -p geth -- --home <printed-path> cas add /tmp/hello-geth.txt
|
||||
cargo run -p geth -- --home <printed-path> cas list
|
||||
```
|
||||
|
||||
For normal persistent use, `geth daemon install` initializes and starts a
|
||||
background user service. Run `geth guide quickstart` to compare startup modes.
|
||||
|
||||
## Backup And Restore
|
||||
|
||||
`geth backup create --out <dir>` creates an offline directory backup with a
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ base64.workspace = true
|
|||
clap.workspace = true
|
||||
clap_complete.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
tokio.workspace = true
|
||||
geth-config = { path = "../geth-config" }
|
||||
geth-control = { path = "../geth-control" }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -92,7 +92,7 @@ async fn serve_local_control(node: LocalNode, listener: UnixListener) -> Result<
|
|||
}
|
||||
});
|
||||
}
|
||||
signal = tokio::signal::ctrl_c() => {
|
||||
signal = shutdown_signal() => {
|
||||
signal?;
|
||||
tracing::info!("shutdown signal received");
|
||||
return Ok(());
|
||||
|
|
@ -101,6 +101,22 @@ async fn serve_local_control(node: LocalNode, listener: UnixListener) -> Result<
|
|||
}
|
||||
}
|
||||
|
||||
async fn shutdown_signal() -> Result<(), std::io::Error> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mut terminate =
|
||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
|
||||
tokio::select! {
|
||||
result = tokio::signal::ctrl_c() => result,
|
||||
_ = terminate.recv() => Ok(()),
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
tokio::signal::ctrl_c().await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn spawn_iroh_control_accept_loop(
|
||||
node: LocalNode,
|
||||
endpoint: GethIrohEndpoint,
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ pub struct ServiceReport {
|
|||
pub manager: ServiceManager,
|
||||
pub action: ServiceAction,
|
||||
pub service_name: String,
|
||||
pub state: Option<String>,
|
||||
pub definition_path: Option<PathBuf>,
|
||||
pub definition: Option<String>,
|
||||
pub commands: Vec<Vec<String>>,
|
||||
|
|
@ -223,39 +224,37 @@ pub fn status_user_service(manager: ServiceManager) -> Result<ServiceReport, Ser
|
|||
let manager = manager.resolve()?;
|
||||
match manager {
|
||||
ServiceManager::SystemdUser => {
|
||||
let command = run_command(
|
||||
let (command, code, detail) = inspect_command(
|
||||
"systemctl",
|
||||
&["--user", "status", SYSTEMD_UNIT, "--no-pager"],
|
||||
)?;
|
||||
Ok(report(
|
||||
Ok(status_report(
|
||||
manager,
|
||||
ServiceAction::Status,
|
||||
Some(systemd_unit_path()?),
|
||||
None,
|
||||
vec![command],
|
||||
"queried systemd user service",
|
||||
command,
|
||||
systemd_state(code),
|
||||
&detail,
|
||||
))
|
||||
}
|
||||
ServiceManager::LaunchdUser => {
|
||||
let command = run_command("launchctl", &["list", LAUNCHD_LABEL])?;
|
||||
Ok(report(
|
||||
let (command, code, detail) = inspect_command("launchctl", &["list", LAUNCHD_LABEL])?;
|
||||
Ok(status_report(
|
||||
manager,
|
||||
ServiceAction::Status,
|
||||
Some(launchd_plist_path()?),
|
||||
None,
|
||||
vec![command],
|
||||
"queried launchd user agent",
|
||||
command,
|
||||
launchd_state(code, &detail),
|
||||
&detail,
|
||||
))
|
||||
}
|
||||
ServiceManager::WindowsTask => {
|
||||
let command = run_command("schtasks", &["/Query", "/TN", WINDOWS_TASK_NAME])?;
|
||||
Ok(report(
|
||||
let (command, code, detail) =
|
||||
inspect_command("schtasks", &["/Query", "/TN", WINDOWS_TASK_NAME])?;
|
||||
Ok(status_report(
|
||||
manager,
|
||||
ServiceAction::Status,
|
||||
None,
|
||||
None,
|
||||
vec![command],
|
||||
"queried Windows per-user scheduled task",
|
||||
command,
|
||||
windows_task_state(code, &detail),
|
||||
&detail,
|
||||
))
|
||||
}
|
||||
ServiceManager::Auto => unreachable!("auto is resolved above"),
|
||||
|
|
@ -358,12 +357,12 @@ fn install_launchd_user(
|
|||
}
|
||||
let definition = launchd_plist(paths, executable);
|
||||
std::fs::write(&plist_path, &definition)?;
|
||||
let mut commands = vec![run_command(
|
||||
"launchctl",
|
||||
&["load", "-w", &plist_path.display().to_string()],
|
||||
)?];
|
||||
let mut commands = Vec::new();
|
||||
if start {
|
||||
commands.push(run_command("launchctl", &["start", LAUNCHD_LABEL])?);
|
||||
commands.push(run_command(
|
||||
"launchctl",
|
||||
&["load", "-w", &plist_path.display().to_string()],
|
||||
)?);
|
||||
}
|
||||
Ok(report(
|
||||
ServiceManager::LaunchdUser,
|
||||
|
|
@ -379,10 +378,9 @@ fn uninstall_launchd_user() -> Result<ServiceReport, ServiceError> {
|
|||
let plist_path = launchd_plist_path()?;
|
||||
let mut commands = Vec::new();
|
||||
if plist_path.exists() {
|
||||
commands.push(run_command(
|
||||
"launchctl",
|
||||
&["unload", &plist_path.display().to_string()],
|
||||
)?);
|
||||
let (command, _, _) =
|
||||
inspect_command("launchctl", &["unload", &plist_path.display().to_string()])?;
|
||||
commands.push(command);
|
||||
std::fs::remove_file(&plist_path)?;
|
||||
}
|
||||
Ok(report(
|
||||
|
|
@ -546,6 +544,83 @@ fn run_command(program: &str, args: &[&str]) -> Result<Vec<String>, ServiceError
|
|||
}
|
||||
}
|
||||
|
||||
fn inspect_command(
|
||||
program: &str,
|
||||
args: &[&str],
|
||||
) -> Result<(Vec<String>, Option<i32>, String), ServiceError> {
|
||||
let output = Command::new(program).args(args).output()?;
|
||||
let command = std::iter::once(program.to_owned())
|
||||
.chain(args.iter().map(|arg| (*arg).to_owned()))
|
||||
.collect::<Vec<_>>();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
|
||||
let detail = if stdout.is_empty() { stderr } else { stdout }
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
Ok((command, output.status.code(), detail))
|
||||
}
|
||||
|
||||
fn systemd_state(code: Option<i32>) -> &'static str {
|
||||
match code {
|
||||
Some(0) => "running",
|
||||
Some(3) => "not-running",
|
||||
Some(4) => "not-installed",
|
||||
_ => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
fn launchd_state(code: Option<i32>, detail: &str) -> &'static str {
|
||||
if code == Some(0) {
|
||||
"loaded"
|
||||
} else if detail
|
||||
.to_ascii_lowercase()
|
||||
.contains("could not find service")
|
||||
{
|
||||
"not-loaded"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
fn windows_task_state(code: Option<i32>, detail: &str) -> &'static str {
|
||||
if code == Some(0) {
|
||||
"installed"
|
||||
} else {
|
||||
let detail = detail.to_ascii_lowercase();
|
||||
if detail.contains("cannot find") || detail.contains("does not exist") {
|
||||
"not-installed"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn status_report(
|
||||
manager: ServiceManager,
|
||||
definition_path: Option<PathBuf>,
|
||||
command: Vec<String>,
|
||||
state: &str,
|
||||
detail: &str,
|
||||
) -> ServiceReport {
|
||||
let note = if detail.is_empty() {
|
||||
format!("user service is {state}")
|
||||
} else {
|
||||
format!("user service is {state}: {detail}")
|
||||
};
|
||||
let mut report = report(
|
||||
manager,
|
||||
ServiceAction::Status,
|
||||
definition_path,
|
||||
None,
|
||||
vec![command],
|
||||
¬e,
|
||||
);
|
||||
report.state = Some(state.to_owned());
|
||||
report
|
||||
}
|
||||
|
||||
fn report(
|
||||
manager: ServiceManager,
|
||||
action: ServiceAction,
|
||||
|
|
@ -563,6 +638,7 @@ fn report(
|
|||
},
|
||||
manager,
|
||||
action,
|
||||
state: None,
|
||||
definition_path,
|
||||
definition,
|
||||
commands,
|
||||
|
|
@ -619,4 +695,35 @@ mod tests {
|
|||
assert!(command.contains("GETH_HOME="));
|
||||
assert!(command.contains("daemon run"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_status_reports_non_running_state_without_an_action_error() {
|
||||
let report = status_report(
|
||||
ServiceManager::SystemdUser,
|
||||
Some(PathBuf::from("/tmp/geth.service")),
|
||||
vec!["systemctl".to_owned(), "status".to_owned()],
|
||||
"not-running",
|
||||
"Unit geth.service could not be found.",
|
||||
);
|
||||
assert_eq!(report.action, ServiceAction::Status);
|
||||
assert_eq!(report.state.as_deref(), Some("not-running"));
|
||||
assert!(report.note.contains("could not be found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_status_classification_preserves_manager_errors_as_unknown() {
|
||||
assert_eq!(systemd_state(Some(0)), "running");
|
||||
assert_eq!(systemd_state(Some(3)), "not-running");
|
||||
assert_eq!(systemd_state(Some(4)), "not-installed");
|
||||
assert_eq!(systemd_state(Some(1)), "unknown");
|
||||
assert_eq!(
|
||||
launchd_state(Some(1), "Could not find service local.geth.daemon"),
|
||||
"not-loaded"
|
||||
);
|
||||
assert_eq!(launchd_state(Some(1), "operation not permitted"), "unknown");
|
||||
assert_eq!(
|
||||
windows_task_state(Some(1), "ERROR: The system cannot find the file specified."),
|
||||
"not-installed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -325,6 +325,41 @@ fn cli_help_documents_owner_init_keys() {
|
|||
assert!(stdout.contains("geth guide owner-setup"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base_help_describes_lifecycle_and_resource_commands() {
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
let output = run_geth(home.path(), &["--help"]);
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("daemon Run, install, and manage the daemon"));
|
||||
assert!(stdout.contains("status Show daemon, storage, Iroh, and backend health"));
|
||||
assert!(stdout.contains("cas Store, fetch, pin, and synchronize"));
|
||||
assert!(stdout.contains("geth daemon install"));
|
||||
assert!(stdout.contains("geth daemon run --ephemeral"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_flag_overrides_geth_home_for_initialization() {
|
||||
let environment_home = tempfile::tempdir().expect("environment home");
|
||||
let selected_parent = tempfile::tempdir().expect("selected parent");
|
||||
let selected_home = selected_parent.path().join("selected-home");
|
||||
let output = run_geth(
|
||||
environment_home.path(),
|
||||
&[
|
||||
"--home",
|
||||
selected_home.to_str().expect("selected home"),
|
||||
"init",
|
||||
],
|
||||
);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(selected_home.join("geth.sqlite").exists());
|
||||
assert!(!environment_home.path().join("geth.sqlite").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guide_command_explains_key_roles() {
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
|
|
@ -535,6 +570,12 @@ fn json_errors_include_stable_code_for_common_failures() {
|
|||
.expect("error message")
|
||||
.contains("connect to daemon")
|
||||
);
|
||||
assert!(
|
||||
error["hint"]
|
||||
.as_str()
|
||||
.expect("daemon recovery hint")
|
||||
.contains("geth daemon install")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -6,8 +6,15 @@ Accepted.
|
|||
|
||||
## Decision
|
||||
|
||||
Geth provides `geth daemon service ...` commands to install, uninstall, start,
|
||||
stop, inspect, and print daemon service definitions.
|
||||
Geth provides direct `geth daemon install|start|stop|status|uninstall` commands
|
||||
for the common lifecycle. `daemon install` initializes the selected geth home,
|
||||
installs and enables the user service, and starts it immediately. The existing
|
||||
`geth daemon service ...` commands remain available for compatibility, explicit
|
||||
manager selection, definition previews, and install-without-start behavior.
|
||||
Status probes return a normalized state. An inactive or missing service is a
|
||||
successful inspection result, while a service-manager access problem is
|
||||
reported as `unknown` with the manager's diagnostic rather than mislabeled as a
|
||||
stopped daemon.
|
||||
|
||||
The service is always installed as a user service:
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ accept and live-sync tasks, releases native docs/gossip/blob handles, calls
|
|||
`Endpoint::close().await`, and removes the local control socket. The same
|
||||
cleanup path runs when the serving loop returns an error, preventing stale
|
||||
socket files and unclosed endpoint clones from becoming restart behavior.
|
||||
Foreground daemons handle Ctrl-C, and Unix daemons also handle the SIGTERM used
|
||||
by user service managers, through this same graceful shutdown path.
|
||||
|
||||
The local metadata store is SQLite product state. `geth-store` tracks a numeric
|
||||
`schema_version` in the `meta` table and applies ordered migrations up to the
|
||||
|
|
@ -44,6 +46,14 @@ daemon. The initial backends are systemd user units on Linux, launchd user agent
|
|||
on macOS, and per-user scheduled tasks on Windows. Geth does not install itself
|
||||
as a privileged system service.
|
||||
|
||||
The common service lifecycle is available directly as `geth daemon
|
||||
install|start|stop|status|uninstall`; the nested service commands remain the
|
||||
advanced and compatibility surface. `geth daemon run --ephemeral` creates a
|
||||
temporary home, reports how another CLI process can select it with `--home`, and
|
||||
removes it after a normal foreground shutdown. Ephemeral mode still starts the
|
||||
same daemon-owned Iroh endpoint and local control stack; it is not a second
|
||||
runtime or transport path.
|
||||
|
||||
## Iroh-Only Remote Communication
|
||||
|
||||
Remote geth node-to-node communication is Iroh-only. The daemon will own one
|
||||
|
|
|
|||
|
|
@ -18,12 +18,11 @@ export GETH_HOME="${GETH_HOME:-$HOME/.local/share/geth/geth}"
|
|||
geth init
|
||||
geth daemon run >"$GETH_HOME/daemon.log" 2>&1 &
|
||||
daemon_pid=$!
|
||||
trap 'kill "$daemon_pid" 2>/dev/null || true' EXIT
|
||||
|
||||
geth wait daemon --timeout-ms 30000
|
||||
geth doctor --json
|
||||
geth status --json
|
||||
|
||||
trap 'kill "$daemon_pid" 2>/dev/null || true' EXIT
|
||||
```
|
||||
|
||||
Create a backup into a separate directory and validate that it can restore to a
|
||||
|
|
@ -106,13 +105,17 @@ manager mutation:
|
|||
|
||||
```sh
|
||||
geth daemon service print
|
||||
geth daemon service install --start
|
||||
geth daemon install
|
||||
geth wait daemon --timeout-ms 30000
|
||||
geth daemon service status
|
||||
geth daemon service stop
|
||||
geth daemon service uninstall
|
||||
geth daemon status
|
||||
geth daemon stop
|
||||
geth daemon uninstall
|
||||
```
|
||||
|
||||
For a disposable interactive test, `geth daemon run --ephemeral` creates and
|
||||
prints a temporary home. Other commands can target it explicitly with `geth
|
||||
--home <printed-path> ...`; normal Ctrl-C shutdown removes the state.
|
||||
|
||||
Admin SSH keys remain outside geth state. Automation should pass public admin
|
||||
keys with `--admin-key` and use the matching private key only as an argument to
|
||||
the explicit signing command when an admin operation is intended:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ to stable commands after the first deployment tag.
|
|||
The following command families are intended to be stable automation surfaces:
|
||||
|
||||
- `geth init`
|
||||
- `geth daemon run`
|
||||
- `geth --home <dir> ...`
|
||||
- `geth daemon run [--ephemeral]`
|
||||
- `geth daemon install|start|stop|status|uninstall`
|
||||
- `geth daemon service install|uninstall|start|stop|status|print`
|
||||
- `geth status`
|
||||
- `geth doctor`
|
||||
|
|
|
|||
|
|
@ -49,8 +49,9 @@ On Machine A:
|
|||
|
||||
```sh
|
||||
geth init --admin-key ~/.ssh/id_ed25519.pub --signing-key ~/.ssh/id_ed25519 --node-name owner
|
||||
geth daemon service install --start
|
||||
geth daemon install
|
||||
geth wait daemon --timeout-ms 30000
|
||||
geth daemon status
|
||||
geth status --json
|
||||
geth doctor --json
|
||||
geth peer export --out /tmp/owner.peer.json
|
||||
|
|
@ -191,8 +192,8 @@ On Machine A:
|
|||
|
||||
```sh
|
||||
geth backup create --out /tmp/geth-backup
|
||||
geth daemon service stop
|
||||
geth daemon service start
|
||||
geth daemon stop
|
||||
geth daemon start
|
||||
geth wait daemon --timeout-ms 30000
|
||||
geth doctor --json
|
||||
```
|
||||
|
|
|
|||
|
|
@ -15,6 +15,53 @@ Status markers:
|
|||
For deployment-readiness work that cuts across feature areas, see
|
||||
[`docs/production-readiness-roadmap.md`](production-readiness-roadmap.md).
|
||||
|
||||
## Operator Usability
|
||||
|
||||
- `[x]` Make startup modes and the daemon lifecycle discoverable.
|
||||
Acceptance criteria:
|
||||
- `[x]` Base and nested CLI help explain every command family instead of
|
||||
showing unlabeled command names.
|
||||
- `[x]` `geth daemon install` initializes, installs, enables, and starts a
|
||||
background service for the current user.
|
||||
- `[x]` Common start, stop, status, and uninstall operations do not require
|
||||
the nested compatibility command path.
|
||||
- `[x]` Service status reports running, inactive/not-installed, and unknown
|
||||
manager states without treating every nonzero status probe as an action
|
||||
failure.
|
||||
- `[x]` `geth daemon run --ephemeral` creates disposable state and prints the
|
||||
exact `--home` selector needed by another terminal.
|
||||
- `[x]` Unix user-service shutdown handles SIGTERM through the daemon's
|
||||
graceful Iroh/task/socket cleanup path.
|
||||
- `[x]` Tests cover lifecycle parsing, help discoverability, and explicit
|
||||
home selection without mutating a real user service manager.
|
||||
|
||||
- `[x]` Document task-oriented user stories.
|
||||
Acceptance criteria:
|
||||
- `[x]` Workflows cover disposable evaluation, persistent background use,
|
||||
owner setup, enrollment, CAS transfer, synchronized application state,
|
||||
automation, diagnosis, backup, and recovery.
|
||||
- `[x]` Each workflow identifies its success signal and relevant trust or
|
||||
durability boundary.
|
||||
- `[x]` Documentation distinguishes automated coverage from real-machine,
|
||||
hardware-key, relay, and privileged-interface dogfooding.
|
||||
|
||||
- `[ ]` Add unified service-log inspection.
|
||||
Acceptance criteria:
|
||||
- `[ ]` One CLI command gives the platform-appropriate user-service log view
|
||||
or an exact recovery command on Linux, macOS, and Windows.
|
||||
- `[ ]` Log access remains user-scoped and does not require a system service.
|
||||
- `[ ]` Human and JSON output distinguish unavailable logs, an uninstalled
|
||||
service, and an installed service with no log entries.
|
||||
|
||||
- `[ ]` Publish copy-paste installation entrypoints for release artifacts.
|
||||
Acceptance criteria:
|
||||
- `[ ]` Linux, macOS, and Windows installation instructions verify artifact
|
||||
checksums and put the single `geth` executable on `PATH`.
|
||||
- `[ ]` Installation stays separate from explicit `geth daemon install` so
|
||||
downloading a binary never silently creates trust state or starts a service.
|
||||
- `[ ]` Upgrade and uninstall instructions preserve or explicitly remove the
|
||||
selected geth home.
|
||||
|
||||
## Long-Term Goal: Distributed Homelab Overlay
|
||||
|
||||
Goal: evolve geth into a distributed, fault-tolerant homelab overlay runtime in
|
||||
|
|
|
|||
233
docs/user-workflows.md
Normal file
233
docs/user-workflows.md
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
# User Workflows
|
||||
|
||||
This document describes geth from the operator's point of view. Each workflow
|
||||
states the intended outcome, the shortest supported command path, the success
|
||||
signal, and important trust or durability boundaries.
|
||||
|
||||
## Choose A Startup Mode
|
||||
|
||||
### Try geth without keeping state
|
||||
|
||||
User story: as a curious user or test author, I want an isolated daemon without
|
||||
choosing a directory or cleaning it up afterward.
|
||||
|
||||
```sh
|
||||
geth daemon run --ephemeral
|
||||
```
|
||||
|
||||
The command initializes a temporary home, prints its path and a ready-to-copy
|
||||
control command, and runs in the foreground. In another terminal:
|
||||
|
||||
```sh
|
||||
geth --home <printed-path> status
|
||||
geth --home <printed-path> node id
|
||||
```
|
||||
|
||||
Success means `geth status` reports `geth daemon: running`. Ctrl-C performs a
|
||||
graceful shutdown and removes the temporary home. An abrupt process kill may
|
||||
leave temporary files for the operating system's normal temp cleanup.
|
||||
|
||||
### Keep a persistent background node
|
||||
|
||||
User story: as a workstation user, I want geth to start now and at future
|
||||
logins without learning my platform's service-manager syntax.
|
||||
|
||||
```sh
|
||||
geth daemon install
|
||||
geth wait daemon
|
||||
geth status
|
||||
```
|
||||
|
||||
`daemon install` initializes the selected home if needed, installs and enables
|
||||
a service for the current user, and starts it. It never installs a system
|
||||
service. Common lifecycle operations are direct:
|
||||
|
||||
```sh
|
||||
geth daemon status
|
||||
geth daemon stop
|
||||
geth daemon start
|
||||
geth daemon uninstall
|
||||
```
|
||||
|
||||
The older `geth daemon service ...` family remains supported for scripts and
|
||||
advanced options. In particular, `geth daemon service install` installs without
|
||||
starting unless `--start` is supplied.
|
||||
|
||||
### Keep state but run in the foreground
|
||||
|
||||
User story: as a developer, I want persistent state and logs attached to my
|
||||
terminal.
|
||||
|
||||
```sh
|
||||
geth init
|
||||
geth daemon run
|
||||
```
|
||||
|
||||
Set `RUST_LOG=geth_node=debug` when more daemon diagnostics are useful. Use
|
||||
`geth --home <dir> ...` to operate an isolated home without exporting an
|
||||
environment variable.
|
||||
|
||||
## Establish An Owner Trust Root
|
||||
|
||||
User story: as the mesh owner, I want my first node rooted in an existing SSH
|
||||
or hardware-backed OpenSSH admin key without copying that private key into
|
||||
geth.
|
||||
|
||||
```sh
|
||||
geth init \
|
||||
--admin-key ~/.ssh/id_ed25519_sk.pub \
|
||||
--signing-key ~/.ssh/id_ed25519_sk \
|
||||
--owner eric \
|
||||
--node-name owner-laptop
|
||||
geth daemon install
|
||||
geth keychain status
|
||||
geth node list
|
||||
```
|
||||
|
||||
The public key becomes an admin trust anchor. The private key or FIDO/YubiKey
|
||||
stub is passed to `ssh-keygen -Y sign`; geth does not copy it into local state.
|
||||
Success means `keychain status` reports accepted signed operations and `node
|
||||
list` includes the named owner node.
|
||||
|
||||
## Enroll A Second Node
|
||||
|
||||
User story: as the owner, I want to approve a new device without treating
|
||||
discovery or a peer card as proof of trust.
|
||||
|
||||
1. Export and transfer the owner's signed peer card:
|
||||
|
||||
```sh
|
||||
geth peer export --out owner.peer.json
|
||||
```
|
||||
|
||||
2. On the new node, initialize, import the card, and submit a request:
|
||||
|
||||
```sh
|
||||
geth init
|
||||
geth daemon install
|
||||
geth peer import owner.peer.json
|
||||
geth node enroll request --node-name workstation --out workstation.enroll.json
|
||||
geth node enroll submit owner-laptop --path workstation.enroll.json
|
||||
```
|
||||
|
||||
3. On the owner node, review and approve with the admin key:
|
||||
|
||||
```sh
|
||||
geth node enroll list --status pending
|
||||
geth node enroll approve <request-id> --signing-key ~/.ssh/id_ed25519_sk
|
||||
```
|
||||
|
||||
4. On the new node, pull and inspect the approved state:
|
||||
|
||||
```sh
|
||||
geth sync now owner-laptop
|
||||
geth wait sync owner-laptop
|
||||
geth node list
|
||||
```
|
||||
|
||||
Peer-card import only supplies signed endpoint metadata. The owner-signed
|
||||
keychain and authorization operations are what create trust and capabilities.
|
||||
|
||||
## Move A Blob Between Nodes
|
||||
|
||||
User story: as a mesh user, I want to address content by hash and let an
|
||||
authorized node fetch it over Iroh.
|
||||
|
||||
On the provider:
|
||||
|
||||
```sh
|
||||
geth cas add ./archive.tar
|
||||
geth node grant workstation resource:cas:local cas.fetch \
|
||||
--signing-key ~/.ssh/id_ed25519_sk
|
||||
```
|
||||
|
||||
On the consumer, after peer cards and grants have synchronized:
|
||||
|
||||
```sh
|
||||
geth cas fetch owner-laptop <blob-hash>
|
||||
geth cas providers <blob-hash>
|
||||
geth cas get <blob-hash> --out ./archive.tar
|
||||
```
|
||||
|
||||
Success means the fetch reports the provider, `cas providers` records it, and
|
||||
the output hashes to the requested CAS hash. Remote fetch is Iroh-only and is
|
||||
checked against `cas.fetch` on `resource:cas:local`.
|
||||
|
||||
## Synchronize Application State
|
||||
|
||||
User story: as a script author, I want small durable state primitives without
|
||||
building transport, peer authentication, and retry handling myself.
|
||||
|
||||
Start locally with one of:
|
||||
|
||||
```sh
|
||||
geth kv create preferences
|
||||
geth kv set preferences theme dark
|
||||
|
||||
geth document create settings
|
||||
geth document set settings '{"theme":"dark"}'
|
||||
|
||||
geth db add inventory ./inventory.sqlite
|
||||
|
||||
geth cas root add notes ./notes
|
||||
geth cas root scan notes
|
||||
```
|
||||
|
||||
Grant the matching resource capability to a node, then use the module's `sync`
|
||||
command or `geth sync now <node>`. Check `geth sync status --json` for
|
||||
per-peer/per-stream cursors and retry state. File-root application is
|
||||
conservative: it does not overwrite local edits, and ambiguous changes become
|
||||
durable conflicts for `geth cas conflict list`.
|
||||
|
||||
## Automate Reliably
|
||||
|
||||
User story: as an automation author, I want explicit homes, readiness checks,
|
||||
machine-readable output, and stable errors.
|
||||
|
||||
```sh
|
||||
geth --home "$job_home" init
|
||||
geth --home "$job_home" daemon run >"$job_home/daemon.log" 2>&1 &
|
||||
daemon_pid=$!
|
||||
geth --home "$job_home" wait daemon --timeout-ms 30000 --json
|
||||
geth --home "$job_home" status --json
|
||||
```
|
||||
|
||||
Use `--json` for single responses and `--jsonl` for streaming responses. A
|
||||
missing daemon returns the stable error code `daemon_unavailable` plus a startup
|
||||
hint. See `automation-examples.md` and `command-stability.md` before depending
|
||||
on experimental command families.
|
||||
|
||||
## Diagnose, Back Up, And Recover
|
||||
|
||||
User story: as an operator, I want actionable local diagnostics and a backup
|
||||
that does not accidentally collect private trust anchors.
|
||||
|
||||
```sh
|
||||
geth doctor
|
||||
geth status
|
||||
geth sync status
|
||||
geth backup create --out ./geth-backup
|
||||
```
|
||||
|
||||
`doctor` works even when the daemon is unavailable. Backups exclude daemon
|
||||
runtime files, private geth identity keys, and external private SSH admin keys.
|
||||
Restore always targets a separate empty home:
|
||||
|
||||
```sh
|
||||
geth backup restore ./geth-backup --target-home ./restored-geth
|
||||
geth --home ./restored-geth daemon run
|
||||
```
|
||||
|
||||
## Workflow Verification Coverage
|
||||
|
||||
- CLI tests cover help discoverability, `--home` selection, initialization,
|
||||
daemon control, stable JSON errors, wait behavior, service-definition
|
||||
generation, and the owner/enrollment/sync path.
|
||||
- Two-daemon integration tests cover peer-card exchange, authorization denials,
|
||||
signed log import, CAS/KV/document/DB/file-root sync, pipe/pubsub, and restart
|
||||
behavior where practical.
|
||||
- Real user-service managers, hardware keys, real relays, TUN/Wintun privileges,
|
||||
and the full two-machine experience remain dogfood checks because automated
|
||||
tests must not mutate host services or require privileged hardware.
|
||||
|
||||
The real-machine acceptance checklist is in `dogfood-checklist.md`.
|
||||
Loading…
Reference in a new issue