Compare commits
2
Commits
5e7f98d2fe
...
b459019085
No files matched your search
@@ -45,6 +45,10 @@ pub struct TerminalConfig {
|
|||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
#[serde(default = "default_terminal_shell")]
|
#[serde(default = "default_terminal_shell")]
|
||||||
pub shell: PathBuf,
|
pub shell: PathBuf,
|
||||||
|
#[serde(default)]
|
||||||
|
pub args: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub current_dir: Option<PathBuf>,
|
||||||
#[serde(default = "default_terminal_max_sessions")]
|
#[serde(default = "default_terminal_max_sessions")]
|
||||||
pub max_sessions: usize,
|
pub max_sessions: usize,
|
||||||
}
|
}
|
||||||
@@ -54,6 +58,8 @@ impl Default for TerminalConfig {
|
|||||||
Self {
|
Self {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
shell: default_terminal_shell(),
|
shell: default_terminal_shell(),
|
||||||
|
args: Vec::new(),
|
||||||
|
current_dir: None,
|
||||||
max_sessions: default_terminal_max_sessions(),
|
max_sessions: default_terminal_max_sessions(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,6 +255,8 @@ mod tests {
|
|||||||
terminal: TerminalConfig {
|
terminal: TerminalConfig {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
shell: "/bin/sh".into(),
|
shell: "/bin/sh".into(),
|
||||||
|
args: vec!["-l".into()],
|
||||||
|
current_dir: Some("/tmp".into()),
|
||||||
max_sessions: 2,
|
max_sessions: 2,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -284,4 +292,24 @@ agent_token = "secret"
|
|||||||
);
|
);
|
||||||
assert_eq!(config.terminal, TerminalConfig::default());
|
assert_eq!(config.terminal, TerminalConfig::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn existing_terminal_config_defaults_optional_command_fields() {
|
||||||
|
let config: AgentConfig = toml::from_str(
|
||||||
|
r#"
|
||||||
|
server_url = "https://example.com"
|
||||||
|
agent_id = "agent-1"
|
||||||
|
agent_token = "secret"
|
||||||
|
|
||||||
|
[terminal]
|
||||||
|
enabled = true
|
||||||
|
shell = "/bin/sh"
|
||||||
|
max_sessions = 2
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect("existing terminal config should parse");
|
||||||
|
|
||||||
|
assert!(config.terminal.args.is_empty());
|
||||||
|
assert!(config.terminal.current_dir.is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -154,6 +154,8 @@ mod tests {
|
|||||||
terminal: crate::config::TerminalConfig {
|
terminal: crate::config::TerminalConfig {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
shell: "/bin/ash".into(),
|
shell: "/bin/ash".into(),
|
||||||
|
args: vec!["-l".into()],
|
||||||
|
current_dir: Some("/root".into()),
|
||||||
max_sessions: 2,
|
max_sessions: 2,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -215,6 +215,8 @@ async fn run_terminal(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let terminal = match wakey::wakey_linux::terminal::TerminalPty::spawn(
|
let terminal = match wakey::wakey_linux::terminal::TerminalPty::spawn(
|
||||||
Path::new(&config.terminal.shell),
|
Path::new(&config.terminal.shell),
|
||||||
|
&config.terminal.args,
|
||||||
|
config.terminal.current_dir.as_deref(),
|
||||||
rows,
|
rows,
|
||||||
cols,
|
cols,
|
||||||
) {
|
) {
|
||||||
@@ -225,7 +227,13 @@ async fn run_terminal(
|
|||||||
};
|
};
|
||||||
let (mut reader, mut writer, mut child) = terminal.into_parts();
|
let (mut reader, mut writer, mut child) = terminal.into_parts();
|
||||||
let process_group = child.id();
|
let process_group = child.id();
|
||||||
info!(terminal_id = %terminal_id, shell = %config.terminal.shell.display(), "terminal PTY ready");
|
info!(
|
||||||
|
terminal_id = %terminal_id,
|
||||||
|
program = %config.terminal.shell.display(),
|
||||||
|
args = ?config.terminal.args,
|
||||||
|
current_dir = ?config.terminal.current_dir,
|
||||||
|
"terminal PTY ready"
|
||||||
|
);
|
||||||
|
|
||||||
let (relay_input_tx, mut relay_input_rx) = mpsc::channel(RELAY_INPUT_QUEUE);
|
let (relay_input_tx, mut relay_input_rx) = mpsc::channel(RELAY_INPUT_QUEUE);
|
||||||
let mut relay_output: Option<mpsc::Sender<Message>> = None;
|
let mut relay_output: Option<mpsc::Sender<Message>> = None;
|
||||||
|
|||||||
@@ -68,14 +68,14 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
|||||||
.clone()
|
.clone()
|
||||||
.or(base.state_file)
|
.or(base.state_file)
|
||||||
.unwrap_or_else(|| defaults.state_file.clone());
|
.unwrap_or_else(|| defaults.state_file.clone());
|
||||||
let state_file = resolve_path(&data_dir, state_file_raw);
|
let state_file = path_for_rendered_config(&data_dir, state_file_raw);
|
||||||
|
|
||||||
let pid_file_raw = args
|
let pid_file_raw = args
|
||||||
.pid_file
|
.pid_file
|
||||||
.clone()
|
.clone()
|
||||||
.or(base.pid_file)
|
.or(base.pid_file)
|
||||||
.unwrap_or_else(|| defaults.pid_file.clone());
|
.unwrap_or_else(|| defaults.pid_file.clone());
|
||||||
let pid_file = resolve_path(&data_dir, pid_file_raw);
|
let pid_file = path_for_rendered_config(&data_dir, pid_file_raw);
|
||||||
|
|
||||||
let ui_dist_dir = args
|
let ui_dist_dir = args
|
||||||
.ui_dist_dir
|
.ui_dist_dir
|
||||||
@@ -147,6 +147,18 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keep relative paths relative when the rendered config's `data_dir` is also
|
||||||
|
/// relative. The daemon resolves relative paths under `data_dir`; embedding
|
||||||
|
/// that directory into another relative path here would make it get joined a
|
||||||
|
/// second time when the generated config is loaded.
|
||||||
|
fn path_for_rendered_config(data_dir: &std::path::Path, candidate: PathBuf) -> PathBuf {
|
||||||
|
if data_dir.is_relative() && candidate.is_relative() {
|
||||||
|
candidate
|
||||||
|
} else {
|
||||||
|
resolve_path(data_dir, candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn bootstrap_config_if_missing(args: &ServeArgs) -> Result<bool> {
|
pub fn bootstrap_config_if_missing(args: &ServeArgs) -> Result<bool> {
|
||||||
if args.config_file.exists() {
|
if args.config_file.exists() {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
@@ -303,4 +315,51 @@ json_logs = true
|
|||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn init_config_preserves_relative_paths_for_relative_data_dir() {
|
||||||
|
let dir = temp_test_dir("relative-data-dir");
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
std::fs::create_dir_all(&dir).expect("create temp dir");
|
||||||
|
let base = dir.join("base.toml");
|
||||||
|
let out = dir.join("out.toml");
|
||||||
|
std::fs::write(
|
||||||
|
&base,
|
||||||
|
r#"
|
||||||
|
data_dir = "test-ddir/wakey-control-plane"
|
||||||
|
state_file = "state.sqlite3"
|
||||||
|
pid_file = "wakey-control-plane.pid"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect("write base config");
|
||||||
|
|
||||||
|
write_init_config(&InitConfigArgs {
|
||||||
|
config_file: Some(out.clone()),
|
||||||
|
from_config: Some(base),
|
||||||
|
stdout: false,
|
||||||
|
data_dir: None,
|
||||||
|
bind: None,
|
||||||
|
public_url: None,
|
||||||
|
state_file: None,
|
||||||
|
pid_file: None,
|
||||||
|
ui_dist_dir: None,
|
||||||
|
command_timeout_ms: None,
|
||||||
|
enroll_token_ttl_seconds: None,
|
||||||
|
observation_retention_seconds: None,
|
||||||
|
bootstrap_enroll_tokens: Vec::new(),
|
||||||
|
telemetry_otlp_endpoint: None,
|
||||||
|
telemetry_service_name: None,
|
||||||
|
telemetry_json_logs: None,
|
||||||
|
force: false,
|
||||||
|
})
|
||||||
|
.expect("write derived config");
|
||||||
|
|
||||||
|
let parsed: toml::Value =
|
||||||
|
toml::from_str(&std::fs::read_to_string(&out).expect("read derived config"))
|
||||||
|
.expect("parse derived config");
|
||||||
|
assert_eq!(parsed["state_file"].as_str(), Some("state.sqlite3"));
|
||||||
|
assert_eq!(parsed["pid_file"].as_str(), Some("wakey-control-plane.pid"));
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -27,8 +27,18 @@ pub struct TerminalWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TerminalPty {
|
impl TerminalPty {
|
||||||
/// Starts `program` attached to a newly allocated PTY.
|
/// Starts a configured program attached to a newly allocated PTY.
|
||||||
pub fn spawn(program: &Path, rows: u16, cols: u16) -> Result<Self> {
|
///
|
||||||
|
/// Wakey retains control of the PTY streams, terminal environment, and
|
||||||
|
/// kill-on-drop behavior; callers may only select arguments and a working
|
||||||
|
/// directory.
|
||||||
|
pub fn spawn(
|
||||||
|
program: &Path,
|
||||||
|
args: &[String],
|
||||||
|
current_dir: Option<&Path>,
|
||||||
|
rows: u16,
|
||||||
|
cols: u16,
|
||||||
|
) -> Result<Self> {
|
||||||
let (pty, pts) = pty_process::open().context("failed to open PTY")?;
|
let (pty, pts) = pty_process::open().context("failed to open PTY")?;
|
||||||
pty.resize(Size::new(rows, cols))
|
pty.resize(Size::new(rows, cols))
|
||||||
.context("failed to set initial PTY size")?;
|
.context("failed to set initial PTY size")?;
|
||||||
@@ -39,10 +49,14 @@ impl TerminalPty {
|
|||||||
|
|
||||||
// The remote frontend is xterm.js regardless of the daemon's own
|
// The remote frontend is xterm.js regardless of the daemon's own
|
||||||
// environment, so advertise the terminal the child actually receives.
|
// environment, so advertise the terminal the child actually receives.
|
||||||
let command = Command::new(program)
|
let mut command = Command::new(program)
|
||||||
|
.args(args)
|
||||||
.env("TERM", "xterm-256color")
|
.env("TERM", "xterm-256color")
|
||||||
.env("COLORTERM", "truecolor")
|
.env("COLORTERM", "truecolor")
|
||||||
.kill_on_drop(true);
|
.kill_on_drop(true);
|
||||||
|
if let Some(current_dir) = current_dir {
|
||||||
|
command = command.current_dir(current_dir);
|
||||||
|
}
|
||||||
let child = command
|
let child = command
|
||||||
.spawn(pts)
|
.spawn(pts)
|
||||||
.with_context(|| format!("failed to spawn {} in PTY", program.display()))?;
|
.with_context(|| format!("failed to spawn {} in PTY", program.display()))?;
|
||||||
@@ -114,8 +128,10 @@ mod tests {
|
|||||||
///
|
///
|
||||||
/// `./scripts/test_remote.ps1 -Package wakey-linux -Filter terminal::tests::pty_round_trip_and_resize -Exact -ShowOutput`
|
/// `./scripts/test_remote.ps1 -Package wakey-linux -Filter terminal::tests::pty_round_trip_and_resize -Exact -ShowOutput`
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "live system check, requires PTY and shell"]
|
||||||
async fn pty_round_trip_and_resize() {
|
async fn pty_round_trip_and_resize() {
|
||||||
let (mut reader, mut writer, mut child) = TerminalPty::spawn(Path::new("/bin/sh"), 24, 80)
|
let (mut reader, mut writer, mut child) =
|
||||||
|
TerminalPty::spawn(Path::new("/bin/sh"), &[], None, 24, 80)
|
||||||
.expect("spawn PTY shell")
|
.expect("spawn PTY shell")
|
||||||
.into_parts();
|
.into_parts();
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user