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-node",
|
||||||
"geth-pipe",
|
"geth-pipe",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
61
README.md
61
README.md
|
|
@ -8,28 +8,25 @@ still named `geth`.
|
||||||
|
|
||||||
## First 10 Minutes
|
## First 10 Minutes
|
||||||
|
|
||||||
Build the single binary, initialize an isolated home, start the daemon, and
|
Build the single binary and start a disposable daemon:
|
||||||
verify the local control path:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo build -p geth
|
cargo build -p geth
|
||||||
export GETH_HOME="$(mktemp -d)"
|
./target/debug/geth daemon run --ephemeral
|
||||||
printf 'GETH_HOME=%s\n' "$GETH_HOME"
|
|
||||||
./target/debug/geth init
|
|
||||||
./target/debug/geth daemon run
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```sh
|
||||||
export GETH_HOME="<same directory>"
|
./target/debug/geth --home <printed-path> wait daemon
|
||||||
./target/debug/geth wait daemon --timeout-ms 30000
|
./target/debug/geth --home <printed-path> status --json
|
||||||
./target/debug/geth status --json
|
./target/debug/geth --home <printed-path> doctor --json
|
||||||
./target/debug/geth doctor --json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates a local-only identity for evaluation. Before enrolling other
|
Ctrl-C stops the daemon and removes its temporary state. For a persistent
|
||||||
machines, use the owner setup in `geth guide owner-setup`; it records an
|
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
|
OpenSSH admin public key as the trust anchor and signs the initial keychain
|
||||||
statements without copying the private key into geth state.
|
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.
|
and signed-operation compatibility rules.
|
||||||
- [`docs/automation-examples.md`](docs/automation-examples.md): shell, Python,
|
- [`docs/automation-examples.md`](docs/automation-examples.md): shell, Python,
|
||||||
and user-service examples.
|
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):
|
- [`docs/production-readiness-roadmap.md`](docs/production-readiness-roadmap.md):
|
||||||
the pre-deployment gate and its current status.
|
the pre-deployment gate and its current status.
|
||||||
- [`docs/dogfood-checklist.md`](docs/dogfood-checklist.md): required
|
- [`docs/dogfood-checklist.md`](docs/dogfood-checklist.md): required
|
||||||
|
|
@ -57,7 +56,7 @@ It has daemon mode and control mode:
|
||||||
```sh
|
```sh
|
||||||
geth init
|
geth init
|
||||||
geth daemon run
|
geth daemon run
|
||||||
geth daemon service install
|
geth daemon install
|
||||||
geth status
|
geth status
|
||||||
geth node id
|
geth node id
|
||||||
geth resource list
|
geth resource list
|
||||||
|
|
@ -91,14 +90,15 @@ signed records.
|
||||||
The daemon can also install itself as a user service:
|
The daemon can also install itself as a user service:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
geth daemon service install
|
geth daemon install
|
||||||
geth daemon service status
|
geth daemon status
|
||||||
geth daemon service uninstall
|
geth daemon uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
The bootstrap service managers are systemd user units on Linux, launchd user
|
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
|
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
|
## Transport And SSH
|
||||||
|
|
||||||
|
|
@ -131,14 +131,15 @@ metadata from an authorized peer over Iroh.
|
||||||
|
|
||||||
The bootstrap implementation provides:
|
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
|
embedded workflow help, including `--admin-key` / `--signing-key` setup
|
||||||
examples
|
examples
|
||||||
- `geth completions <bash|zsh|fish|powershell|elvish>` for shell completion
|
- `geth completions <bash|zsh|fish|powershell|elvish>` for shell completion
|
||||||
scripts generated from the live CLI command tree
|
scripts generated from the live CLI command tree
|
||||||
- `geth init`
|
- `geth init`
|
||||||
- `geth init --admin-key <public-key> --signing-key <private-key> --node-name <name>`
|
- `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 daemon service install|uninstall|start|stop|status|print`
|
||||||
- `geth status`
|
- `geth status`
|
||||||
- `geth wait daemon|peer|sync --timeout-ms <ms>`
|
- `geth wait daemon|peer|sync --timeout-ms <ms>`
|
||||||
|
|
@ -444,25 +445,25 @@ $GETH_HOME/
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
In one shell:
|
For a disposable evaluation, run this in one shell:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
export GETH_HOME="$(mktemp -d)"
|
cargo run -p geth -- daemon run --ephemeral
|
||||||
cargo run -p geth -- init
|
|
||||||
cargo run -p geth -- daemon run
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In another shell:
|
In another shell, use the home it prints:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
export GETH_HOME="<same dir>"
|
cargo run -p geth -- --home <printed-path> status
|
||||||
cargo run -p geth -- status
|
cargo run -p geth -- --home <printed-path> node id
|
||||||
cargo run -p geth -- node id
|
|
||||||
echo "hello geth" > /tmp/hello-geth.txt
|
echo "hello geth" > /tmp/hello-geth.txt
|
||||||
cargo run -p geth -- cas add /tmp/hello-geth.txt
|
cargo run -p geth -- --home <printed-path> cas add /tmp/hello-geth.txt
|
||||||
cargo run -p geth -- cas list
|
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
|
## Backup And Restore
|
||||||
|
|
||||||
`geth backup create --out <dir>` creates an offline directory backup with a
|
`geth backup create --out <dir>` creates an offline directory backup with a
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ base64.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
clap_complete.workspace = true
|
clap_complete.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
tempfile.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
geth-config = { path = "../geth-config" }
|
geth-config = { path = "../geth-config" }
|
||||||
geth-control = { path = "../geth-control" }
|
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?;
|
signal?;
|
||||||
tracing::info!("shutdown signal received");
|
tracing::info!("shutdown signal received");
|
||||||
return Ok(());
|
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(
|
pub(crate) fn spawn_iroh_control_accept_loop(
|
||||||
node: LocalNode,
|
node: LocalNode,
|
||||||
endpoint: GethIrohEndpoint,
|
endpoint: GethIrohEndpoint,
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ pub struct ServiceReport {
|
||||||
pub manager: ServiceManager,
|
pub manager: ServiceManager,
|
||||||
pub action: ServiceAction,
|
pub action: ServiceAction,
|
||||||
pub service_name: String,
|
pub service_name: String,
|
||||||
|
pub state: Option<String>,
|
||||||
pub definition_path: Option<PathBuf>,
|
pub definition_path: Option<PathBuf>,
|
||||||
pub definition: Option<String>,
|
pub definition: Option<String>,
|
||||||
pub commands: Vec<Vec<String>>,
|
pub commands: Vec<Vec<String>>,
|
||||||
|
|
@ -223,39 +224,37 @@ pub fn status_user_service(manager: ServiceManager) -> Result<ServiceReport, Ser
|
||||||
let manager = manager.resolve()?;
|
let manager = manager.resolve()?;
|
||||||
match manager {
|
match manager {
|
||||||
ServiceManager::SystemdUser => {
|
ServiceManager::SystemdUser => {
|
||||||
let command = run_command(
|
let (command, code, detail) = inspect_command(
|
||||||
"systemctl",
|
"systemctl",
|
||||||
&["--user", "status", SYSTEMD_UNIT, "--no-pager"],
|
&["--user", "status", SYSTEMD_UNIT, "--no-pager"],
|
||||||
)?;
|
)?;
|
||||||
Ok(report(
|
Ok(status_report(
|
||||||
manager,
|
manager,
|
||||||
ServiceAction::Status,
|
|
||||||
Some(systemd_unit_path()?),
|
Some(systemd_unit_path()?),
|
||||||
None,
|
command,
|
||||||
vec![command],
|
systemd_state(code),
|
||||||
"queried systemd user service",
|
&detail,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
ServiceManager::LaunchdUser => {
|
ServiceManager::LaunchdUser => {
|
||||||
let command = run_command("launchctl", &["list", LAUNCHD_LABEL])?;
|
let (command, code, detail) = inspect_command("launchctl", &["list", LAUNCHD_LABEL])?;
|
||||||
Ok(report(
|
Ok(status_report(
|
||||||
manager,
|
manager,
|
||||||
ServiceAction::Status,
|
|
||||||
Some(launchd_plist_path()?),
|
Some(launchd_plist_path()?),
|
||||||
None,
|
command,
|
||||||
vec![command],
|
launchd_state(code, &detail),
|
||||||
"queried launchd user agent",
|
&detail,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
ServiceManager::WindowsTask => {
|
ServiceManager::WindowsTask => {
|
||||||
let command = run_command("schtasks", &["/Query", "/TN", WINDOWS_TASK_NAME])?;
|
let (command, code, detail) =
|
||||||
Ok(report(
|
inspect_command("schtasks", &["/Query", "/TN", WINDOWS_TASK_NAME])?;
|
||||||
|
Ok(status_report(
|
||||||
manager,
|
manager,
|
||||||
ServiceAction::Status,
|
|
||||||
None,
|
None,
|
||||||
None,
|
command,
|
||||||
vec![command],
|
windows_task_state(code, &detail),
|
||||||
"queried Windows per-user scheduled task",
|
&detail,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
ServiceManager::Auto => unreachable!("auto is resolved above"),
|
ServiceManager::Auto => unreachable!("auto is resolved above"),
|
||||||
|
|
@ -358,12 +357,12 @@ fn install_launchd_user(
|
||||||
}
|
}
|
||||||
let definition = launchd_plist(paths, executable);
|
let definition = launchd_plist(paths, executable);
|
||||||
std::fs::write(&plist_path, &definition)?;
|
std::fs::write(&plist_path, &definition)?;
|
||||||
let mut commands = vec![run_command(
|
let mut commands = Vec::new();
|
||||||
|
if start {
|
||||||
|
commands.push(run_command(
|
||||||
"launchctl",
|
"launchctl",
|
||||||
&["load", "-w", &plist_path.display().to_string()],
|
&["load", "-w", &plist_path.display().to_string()],
|
||||||
)?];
|
)?);
|
||||||
if start {
|
|
||||||
commands.push(run_command("launchctl", &["start", LAUNCHD_LABEL])?);
|
|
||||||
}
|
}
|
||||||
Ok(report(
|
Ok(report(
|
||||||
ServiceManager::LaunchdUser,
|
ServiceManager::LaunchdUser,
|
||||||
|
|
@ -379,10 +378,9 @@ fn uninstall_launchd_user() -> Result<ServiceReport, ServiceError> {
|
||||||
let plist_path = launchd_plist_path()?;
|
let plist_path = launchd_plist_path()?;
|
||||||
let mut commands = Vec::new();
|
let mut commands = Vec::new();
|
||||||
if plist_path.exists() {
|
if plist_path.exists() {
|
||||||
commands.push(run_command(
|
let (command, _, _) =
|
||||||
"launchctl",
|
inspect_command("launchctl", &["unload", &plist_path.display().to_string()])?;
|
||||||
&["unload", &plist_path.display().to_string()],
|
commands.push(command);
|
||||||
)?);
|
|
||||||
std::fs::remove_file(&plist_path)?;
|
std::fs::remove_file(&plist_path)?;
|
||||||
}
|
}
|
||||||
Ok(report(
|
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(
|
fn report(
|
||||||
manager: ServiceManager,
|
manager: ServiceManager,
|
||||||
action: ServiceAction,
|
action: ServiceAction,
|
||||||
|
|
@ -563,6 +638,7 @@ fn report(
|
||||||
},
|
},
|
||||||
manager,
|
manager,
|
||||||
action,
|
action,
|
||||||
|
state: None,
|
||||||
definition_path,
|
definition_path,
|
||||||
definition,
|
definition,
|
||||||
commands,
|
commands,
|
||||||
|
|
@ -619,4 +695,35 @@ mod tests {
|
||||||
assert!(command.contains("GETH_HOME="));
|
assert!(command.contains("GETH_HOME="));
|
||||||
assert!(command.contains("daemon run"));
|
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"));
|
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]
|
#[test]
|
||||||
fn guide_command_explains_key_roles() {
|
fn guide_command_explains_key_roles() {
|
||||||
let home = tempfile::tempdir().expect("tempdir");
|
let home = tempfile::tempdir().expect("tempdir");
|
||||||
|
|
@ -535,6 +570,12 @@ fn json_errors_include_stable_code_for_common_failures() {
|
||||||
.expect("error message")
|
.expect("error message")
|
||||||
.contains("connect to daemon")
|
.contains("connect to daemon")
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
error["hint"]
|
||||||
|
.as_str()
|
||||||
|
.expect("daemon recovery hint")
|
||||||
|
.contains("geth daemon install")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,15 @@ Accepted.
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
Geth provides `geth daemon service ...` commands to install, uninstall, start,
|
Geth provides direct `geth daemon install|start|stop|status|uninstall` commands
|
||||||
stop, inspect, and print daemon service definitions.
|
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:
|
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
|
`Endpoint::close().await`, and removes the local control socket. The same
|
||||||
cleanup path runs when the serving loop returns an error, preventing stale
|
cleanup path runs when the serving loop returns an error, preventing stale
|
||||||
socket files and unclosed endpoint clones from becoming restart behavior.
|
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
|
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
|
`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
|
on macOS, and per-user scheduled tasks on Windows. Geth does not install itself
|
||||||
as a privileged system service.
|
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
|
## Iroh-Only Remote Communication
|
||||||
|
|
||||||
Remote geth node-to-node communication is Iroh-only. The daemon will own one
|
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 init
|
||||||
geth daemon run >"$GETH_HOME/daemon.log" 2>&1 &
|
geth daemon run >"$GETH_HOME/daemon.log" 2>&1 &
|
||||||
daemon_pid=$!
|
daemon_pid=$!
|
||||||
|
trap 'kill "$daemon_pid" 2>/dev/null || true' EXIT
|
||||||
|
|
||||||
geth wait daemon --timeout-ms 30000
|
geth wait daemon --timeout-ms 30000
|
||||||
geth doctor --json
|
geth doctor --json
|
||||||
geth status --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
|
Create a backup into a separate directory and validate that it can restore to a
|
||||||
|
|
@ -106,13 +105,17 @@ manager mutation:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
geth daemon service print
|
geth daemon service print
|
||||||
geth daemon service install --start
|
geth daemon install
|
||||||
geth wait daemon --timeout-ms 30000
|
geth wait daemon --timeout-ms 30000
|
||||||
geth daemon service status
|
geth daemon status
|
||||||
geth daemon service stop
|
geth daemon stop
|
||||||
geth daemon service uninstall
|
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
|
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
|
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:
|
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:
|
The following command families are intended to be stable automation surfaces:
|
||||||
|
|
||||||
- `geth init`
|
- `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 daemon service install|uninstall|start|stop|status|print`
|
||||||
- `geth status`
|
- `geth status`
|
||||||
- `geth doctor`
|
- `geth doctor`
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,9 @@ On Machine A:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
geth init --admin-key ~/.ssh/id_ed25519.pub --signing-key ~/.ssh/id_ed25519 --node-name owner
|
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 wait daemon --timeout-ms 30000
|
||||||
|
geth daemon status
|
||||||
geth status --json
|
geth status --json
|
||||||
geth doctor --json
|
geth doctor --json
|
||||||
geth peer export --out /tmp/owner.peer.json
|
geth peer export --out /tmp/owner.peer.json
|
||||||
|
|
@ -191,8 +192,8 @@ On Machine A:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
geth backup create --out /tmp/geth-backup
|
geth backup create --out /tmp/geth-backup
|
||||||
geth daemon service stop
|
geth daemon stop
|
||||||
geth daemon service start
|
geth daemon start
|
||||||
geth wait daemon --timeout-ms 30000
|
geth wait daemon --timeout-ms 30000
|
||||||
geth doctor --json
|
geth doctor --json
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,53 @@ Status markers:
|
||||||
For deployment-readiness work that cuts across feature areas, see
|
For deployment-readiness work that cuts across feature areas, see
|
||||||
[`docs/production-readiness-roadmap.md`](production-readiness-roadmap.md).
|
[`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
|
## Long-Term Goal: Distributed Homelab Overlay
|
||||||
|
|
||||||
Goal: evolve geth into a distributed, fault-tolerant homelab overlay runtime in
|
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