Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 78 additions & 3 deletions rust/host/src/kwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ use std::sync::Arc;

use serde::{Deserialize, Deserializer, de};

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum KwtTmuxAttachMode {
Direct,
Protected,
}

#[derive(Clone, Eq, PartialEq)]
pub struct KwtBundle {
revision: String,
Expand Down Expand Up @@ -206,6 +213,7 @@ pub struct KwtWorktree {
repository: String,
session_name: String,
tmux_socket_name: Option<String>,
tmux_attach_mode: KwtTmuxAttachMode,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
Expand Down Expand Up @@ -278,6 +286,7 @@ pub struct KwtImportedWorkspace {
session_name: String,
#[serde(default)]
tmux_socket_name: Option<String>,
tmux_attach_mode: KwtTmuxAttachMode,
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -363,6 +372,7 @@ pub struct KwtWorktreeOpen {
registration_fingerprint: String,
generation: String,
session_name: String,
tmux_socket_name: Option<String>,
}

impl KwtWorktreeOpen {
Expand All @@ -373,13 +383,15 @@ impl KwtWorktreeOpen {
registration_fingerprint: impl Into<String>,
generation: impl Into<String>,
session_name: impl Into<String>,
tmux_socket_name: Option<String>,
) -> Self {
Self {
path: path.into(),
repository: repository.into(),
registration_fingerprint: registration_fingerprint.into(),
generation: generation.into(),
session_name: session_name.into(),
tmux_socket_name,
}
}

Expand All @@ -403,6 +415,11 @@ impl KwtWorktreeOpen {
pub fn session_name(&self) -> &str {
&self.session_name
}

#[must_use]
pub fn tmux_socket_name(&self) -> Option<&str> {
self.tmux_socket_name.as_deref()
}
}

impl KwtWorktreeCreate {
Expand Down Expand Up @@ -556,6 +573,9 @@ pub(crate) fn parse_pull_request_import(output: &[u8]) -> Result<KwtPullRequestI
{
return Err("KWT pull-request import omitted its protected tmux socket".to_owned());
}
if response.workspace.tmux_attach_mode != KwtTmuxAttachMode::Protected {
return Err("KWT pull-request import did not select protected attachment".to_owned());
}
Ok(response)
}

Expand Down Expand Up @@ -619,6 +639,10 @@ impl KwtImportedWorkspace {
pub fn tmux_socket_name(&self) -> Option<&str> {
self.tmux_socket_name.as_deref()
}
#[must_use]
pub const fn tmux_attach_mode(&self) -> KwtTmuxAttachMode {
self.tmux_attach_mode
}
}

impl KwtPullRequestImport {
Expand Down Expand Up @@ -673,6 +697,47 @@ impl KwtWorktree {
pub fn tmux_socket_name(&self) -> Option<&str> {
self.tmux_socket_name.as_deref()
}
#[must_use]
pub const fn tmux_attach_mode(&self) -> KwtTmuxAttachMode {
self.tmux_attach_mode
}
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct KwtDirectoryWorkspaceOpen {
path: String,
session_name: String,
tmux_socket_name: Option<String>,
}

impl KwtDirectoryWorkspaceOpen {
#[must_use]
pub fn new(
path: impl Into<String>,
session_name: impl Into<String>,
tmux_socket_name: Option<String>,
) -> Self {
Self {
path: path.into(),
session_name: session_name.into(),
tmux_socket_name,
}
}

#[must_use]
pub fn path(&self) -> &str {
&self.path
}

#[must_use]
pub fn session_name(&self) -> &str {
&self.session_name
}

#[must_use]
pub fn tmux_socket_name(&self) -> Option<&str> {
self.tmux_socket_name.as_deref()
}
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
Expand All @@ -682,6 +747,8 @@ pub struct KwtDirectoryWorkspace {
path: String,
session_name: String,
session_live: bool,
tmux_socket_name: Option<String>,
tmux_attach_mode: KwtTmuxAttachMode,
}

impl KwtDirectoryWorkspace {
Expand All @@ -701,6 +768,14 @@ impl KwtDirectoryWorkspace {
pub const fn session_live(&self) -> bool {
self.session_live
}
#[must_use]
pub fn tmux_socket_name(&self) -> Option<&str> {
self.tmux_socket_name.as_deref()
}
#[must_use]
pub const fn tmux_attach_mode(&self) -> KwtTmuxAttachMode {
self.tmux_attach_mode
}
}

#[derive(Clone, Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -793,8 +868,8 @@ mod tests {
fn inventory_joins_global_worktrees_without_reordering_projects() {
let inventory = KwtInventory::parse(
br#"[{"repository":"two","name":"Second","path":"/r/two","last_touched":null,"registration_fingerprint":"two-fingerprint"},{"repository":"one","name":"First","path":"/r/one","last_touched":"now","registration_fingerprint":"one-fingerprint"}]"#,
br#"[{"path":"/w/one","branch":"main","commit_hash":"abc","is_main":true,"created_at":null,"generation":"g1","repository":"one","session_name":"one-main","tmux_socket_name":null},{"path":"/w/two","branch":"topic","commit_hash":"def","is_main":false,"created_at":"then","generation":null,"repository":"two","session_name":"two-topic","tmux_socket_name":"alt"}]"#,
br#"[{"name":"scratch","path":"/w/scratch","session_name":"scratch","session_live":false}]"#,
br#"[{"path":"/w/one","branch":"main","commit_hash":"abc","is_main":true,"created_at":null,"generation":"g1","repository":"one","session_name":"one-main","tmux_socket_name":null,"tmux_attach_mode":"direct"},{"path":"/w/two","branch":"topic","commit_hash":"def","is_main":false,"created_at":"then","generation":null,"repository":"two","session_name":"two-topic","tmux_socket_name":"alt","tmux_attach_mode":"direct"}]"#,
br#"[{"name":"scratch","path":"/w/scratch","session_name":"scratch","session_live":false,"tmux_socket_name":"kwt","tmux_attach_mode":"direct"}]"#,
).expect("valid inventory");

assert_eq!(inventory.projects()[0].project().repository(), "two");
Expand Down Expand Up @@ -860,7 +935,7 @@ mod tests {
assert_eq!(pull_requests[0].number(), 17);
assert_eq!(pull_requests[0].source_branch(), "feature/rendering");

let imported = parse_pull_request_import(br#"{"status":"created","pull_request":{"id":"github:github.com/acme/widget#17","provider":"github","repository":{"provider":"github","identity":"github.com/acme/widget","host":"github.com","owner":"acme","name":"widget"},"number":17,"url":"https://github.com/acme/widget/pull/17","title":"Improve rendering","author":"octocat","source":{"branch":"feature/rendering","repository":{"provider":"github","identity":"github.com/octocat/widget","host":"github.com","owner":"octocat","name":"widget"},"is_fork":true},"target":{"branch":"main","repository":{"provider":"github","identity":"github.com/acme/widget","host":"github.com","owner":"acme","name":"widget"},"is_fork":false},"draft":false,"state":"open","head_sha":"0123456789abcdef0123456789abcdef01234567","imported":true,"workspace":{"id":"workspace","repository":"github.com/acme/widget","branch":"pr-17-feature-rendering","path":"/worktrees/pr-17","generation":"11111111111111111111111111111111","state":"ready","session_name":"widget-pr-17","tmux_socket_name":"kwt-pr-a1b2"}},"project":{"identity":"github.com/acme/widget","name":"widget","path":"/code/widget"},"workspace":{"id":"workspace","repository":"github.com/acme/widget","branch":"pr-17-feature-rendering","path":"/worktrees/pr-17","generation":"11111111111111111111111111111111","state":"ready","session_name":"widget-pr-17","tmux_socket_name":"kwt-pr-a1b2"}}"#).expect("valid import");
let imported = parse_pull_request_import(br#"{"status":"created","pull_request":{"id":"github:github.com/acme/widget#17","provider":"github","repository":{"provider":"github","identity":"github.com/acme/widget","host":"github.com","owner":"acme","name":"widget"},"number":17,"url":"https://github.com/acme/widget/pull/17","title":"Improve rendering","author":"octocat","source":{"branch":"feature/rendering","repository":{"provider":"github","identity":"github.com/octocat/widget","host":"github.com","owner":"octocat","name":"widget"},"is_fork":true},"target":{"branch":"main","repository":{"provider":"github","identity":"github.com/acme/widget","host":"github.com","owner":"acme","name":"widget"},"is_fork":false},"draft":false,"state":"open","head_sha":"0123456789abcdef0123456789abcdef01234567","imported":true,"workspace":{"id":"workspace","repository":"github.com/acme/widget","branch":"pr-17-feature-rendering","path":"/worktrees/pr-17","generation":"11111111111111111111111111111111","state":"ready","session_name":"widget-pr-17","tmux_socket_name":"kwt-pr-a1b2","tmux_attach_mode":"protected"}},"project":{"identity":"github.com/acme/widget","name":"widget","path":"/code/widget"},"workspace":{"id":"workspace","repository":"github.com/acme/widget","branch":"pr-17-feature-rendering","path":"/worktrees/pr-17","generation":"11111111111111111111111111111111","state":"ready","session_name":"widget-pr-17","tmux_socket_name":"kwt-pr-a1b2","tmux_attach_mode":"protected"}}"#).expect("valid import");
assert_eq!(imported.workspace().tmux_socket_name(), Some("kwt-pr-a1b2"));
}

Expand Down
8 changes: 4 additions & 4 deletions rust/host/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ mod wsl;
mod zellij;

pub use kwt::{
KwtBranchCandidate, KwtBundle, KwtDirectoryWorkspace, KwtInventory, KwtProject,
KwtProjectInventory, KwtProtectedWorktreeOpen, KwtPullRequest, KwtPullRequestImport,
KwtPullRequestImportRequest, KwtWorktree, KwtWorktreeCreate, KwtWorktreeOpen,
kwt_command_failure_message,
KwtBranchCandidate, KwtBundle, KwtDirectoryWorkspace, KwtDirectoryWorkspaceOpen, KwtInventory,
KwtProject, KwtProjectInventory, KwtProtectedWorktreeOpen, KwtPullRequest,
KwtPullRequestImport, KwtPullRequestImportRequest, KwtTmuxAttachMode, KwtWorktree,
KwtWorktreeCreate, KwtWorktreeOpen, kwt_command_failure_message,
};
pub use remote::{
RemoteSessionInventory, RemoteTmuxConfig, RemoteTmuxError, RemoteTmuxHost, RemoteTmuxSnapshot,
Expand Down
131 changes: 116 additions & 15 deletions rust/host/src/wsl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ use crate::kwt::{
use crate::zellij;
use crate::{
CancellationToken, CommandOutput, CommandPrefix, CommandRunner, KwtBranchCandidate, KwtBundle,
KwtInventory, KwtProject, KwtProtectedWorktreeOpen, KwtPullRequest, KwtPullRequestImport,
KwtPullRequestImportRequest, KwtWorktreeCreate, KwtWorktreeOpen, RemoteTmuxConfig,
RemoteTmuxHost,
KwtDirectoryWorkspaceOpen, KwtInventory, KwtProject, KwtProtectedWorktreeOpen, KwtPullRequest,
KwtPullRequestImport, KwtPullRequestImportRequest, KwtWorktreeCreate, KwtWorktreeOpen,
RemoteTmuxConfig, RemoteTmuxHost,
};

const DEFAULT_TMUX: &str = "/usr/bin/tmux";
Expand Down Expand Up @@ -1296,6 +1296,71 @@ impl<R: CommandRunner> WslHost<R> {
))
}

/// Resolve the revision-pinned helper and build a re-runnable ordinary
/// client for one exact registered directory workspace.
///
/// # Errors
///
/// Returns an error when the helper or captured WSL runtime cannot be
/// verified, or when KWT supplied an unsafe named socket.
pub fn kwt_directory_open_plan(
&self,
endpoint: &WslEndpoint,
runtime: &WslRuntimeIdentity,
request: &KwtDirectoryWorkspaceOpen,
term: AttachTerm,
cancellation: &CancellationToken,
) -> Result<RepairOrOpenPlan, HostError> {
if let Some(socket_name) = request.tmux_socket_name() {
require_kwt_socket_name(socket_name)?;
}
self.require_runtime(endpoint, runtime, cancellation)?;
let bundle = self.config.kwt_bundle().ok_or_else(|| {
HostError::new(
DiagnosticKind::ExecutableNotFound,
"the revision-pinned KWT helper is not bundled",
)
})?;
let helper = self.ensure_kwt_helper(endpoint, runtime, bundle, cancellation)?;
self.require_runtime(endpoint, runtime, cancellation)?;
let readiness_path = kwt_client_readiness_path()?;
let readiness_staging_path = format!("{readiness_path}.tmp");
let mut args = pinned_prefix(endpoint);
let kwt_home = self
.config
.kwt_home
.as_deref()
.map(|path| format!("KWT_HOME={path}"));
let extra_environment = kwt_home.as_deref().into_iter().collect::<Vec<_>>();
append_tmux_environment(
&mut args,
Some(term.environment()),
self.config.tmux_tmpdir.as_deref(),
&extra_environment,
);
args.extend(
[
"/bin/sh",
"-c",
"umask 077; /usr/bin/printf '%s\\n' \"$$\" > \"$1\" && /usr/bin/mv -T -- \"$1\" \"$2\" && shift 2 && exec \"$@\"",
"ghosthub-directory-workspace-client",
readiness_staging_path.as_str(),
readiness_path.as_str(),
helper.as_str(),
"open",
request.path(),
]
.into_iter()
.map(OsString::from),
);
Ok(RepairOrOpenPlan::worktree(
self.wsl_executable.as_os_str(),
args,
request.session_name(),
&readiness_path,
))
}

/// Build a re-runnable protected attach for one imported PR workspace.
///
/// # Errors
Expand Down Expand Up @@ -1391,8 +1456,12 @@ impl<R: CommandRunner> WslHost<R> {
endpoint: &WslEndpoint,
runtime: &WslRuntimeIdentity,
readiness_path: &str,
tmux_socket_name: Option<&str>,
cancellation: &CancellationToken,
) -> Result<Option<SessionIdentity>, HostError> {
if let Some(socket_name) = tmux_socket_name {
require_kwt_socket_name(socket_name)?;
}
require_kwt_client_readiness_path(readiness_path)?;
self.require_runtime(endpoint, runtime, cancellation)?;
let receipt = self.run_scrubbed(
Expand All @@ -1417,17 +1486,18 @@ impl<R: CommandRunner> WslHost<R> {
return Ok(None);
}
let client_pid = parse_kwt_client_pid(&receipt.stdout)?;
let output = self.run_tmux_command(
endpoint,
cancellation,
&[
"-f",
"/dev/null",
"list-clients",
"-F",
CLIENT_READINESS_FORMAT,
],
)?;
let mut command = Vec::new();
if let Some(socket_name) = tmux_socket_name {
command.extend(["-L", socket_name]);
}
command.extend([
"-f",
"/dev/null",
"list-clients",
"-F",
CLIENT_READINESS_FORMAT,
]);
let output = self.run_tmux_command(endpoint, cancellation, &command)?;
if output.status != 0 {
let stderr = String::from_utf8_lossy(&output.stderr);
if is_no_server(&stderr) {
Expand Down Expand Up @@ -5249,7 +5319,7 @@ mod tests {
} else if args.windows(2).any(|pair| pair == ["pr", "list"]) {
br#"{"pull_requests":[{"id":"github:github.com/acme/widget#17","provider":"github","repository":{"provider":"github","identity":"github.com/acme/widget","host":"github.com","owner":"acme","name":"widget"},"number":17,"url":"https://github.com/acme/widget/pull/17","title":"Improve rendering","author":"octocat","source":{"branch":"feature/rendering","repository":{"provider":"github","identity":"github.com/octocat/widget","host":"github.com","owner":"octocat","name":"widget"},"is_fork":true},"target":{"branch":"main","repository":{"provider":"github","identity":"github.com/acme/widget","host":"github.com","owner":"acme","name":"widget"},"is_fork":false},"draft":false,"state":"open","head_sha":"0123456789abcdef0123456789abcdef01234567","imported":false}]}"#.to_vec()
} else if args.windows(2).any(|pair| pair == ["pr", "import"]) {
br#"{"status":"created","pull_request":{"id":"github:github.com/acme/widget#17","provider":"github","repository":{"provider":"github","identity":"github.com/acme/widget","host":"github.com","owner":"acme","name":"widget"},"number":17,"url":"https://github.com/acme/widget/pull/17","title":"Improve rendering","author":"octocat","source":{"branch":"feature/rendering","repository":{"provider":"github","identity":"github.com/octocat/widget","host":"github.com","owner":"octocat","name":"widget"},"is_fork":true},"target":{"branch":"main","repository":{"provider":"github","identity":"github.com/acme/widget","host":"github.com","owner":"acme","name":"widget"},"is_fork":false},"draft":false,"state":"open","head_sha":"0123456789abcdef0123456789abcdef01234567","imported":true},"project":{"identity":"github.com/acme/widget","name":"widget","path":"/code/widget"},"workspace":{"id":"workspace","repository":"github.com/acme/widget","branch":"pr-17-feature-rendering","path":"/worktrees/pr-17","generation":"11111111111111111111111111111111","state":"ready","session_name":"widget-pr-17","tmux_socket_name":"kwt-pr-a1b2"}}"#.to_vec()
br#"{"status":"created","pull_request":{"id":"github:github.com/acme/widget#17","provider":"github","repository":{"provider":"github","identity":"github.com/acme/widget","host":"github.com","owner":"acme","name":"widget"},"number":17,"url":"https://github.com/acme/widget/pull/17","title":"Improve rendering","author":"octocat","source":{"branch":"feature/rendering","repository":{"provider":"github","identity":"github.com/octocat/widget","host":"github.com","owner":"octocat","name":"widget"},"is_fork":true},"target":{"branch":"main","repository":{"provider":"github","identity":"github.com/acme/widget","host":"github.com","owner":"acme","name":"widget"},"is_fork":false},"draft":false,"state":"open","head_sha":"0123456789abcdef0123456789abcdef01234567","imported":true},"project":{"identity":"github.com/acme/widget","name":"widget","path":"/code/widget"},"workspace":{"id":"workspace","repository":"github.com/acme/widget","branch":"pr-17-feature-rendering","path":"/worktrees/pr-17","generation":"11111111111111111111111111111111","state":"ready","session_name":"widget-pr-17","tmux_socket_name":"kwt-pr-a1b2","tmux_attach_mode":"protected"}}"#.to_vec()
} else if (args.iter().any(|argument| argument == "add")
&& args.iter().any(|argument| argument == "--no-launch"))
|| (args.iter().any(|argument| argument == "remove")
Expand Down Expand Up @@ -5754,6 +5824,7 @@ mod tests {
"registration-fingerprint",
"0123456789abcdef0123456789abcdef",
"widget-topic",
None,
),
AttachTerm::Xterm256Color,
&CancellationToken::new(),
Expand Down Expand Up @@ -5801,6 +5872,36 @@ mod tests {
assert_eq!(plan.clone(), plan);
}

#[test]
fn kwt_directory_open_plan_uses_the_exact_registered_path() {
let (host, _runner, endpoint, runtime) = kwt_mutation_host();
let plan = host
.kwt_directory_open_plan(
&endpoint,
&runtime,
&KwtDirectoryWorkspaceOpen::new(
"/work/scratch",
"kwt-workspace-dir-scratch-abc",
Some("kwt".to_owned()),
),
AttachTerm::Xterm256Color,
&CancellationToken::new(),
)
.expect("build directory workspace open plan");
let args = plan
.args()
.iter()
.map(|argument| argument.to_string_lossy())
.collect::<Vec<_>>();

assert!(
args.windows(3)
.any(|args| { args == [&test_kwt_helper_path(), "open", "/work/scratch",] })
);
assert!(!args.iter().any(|argument| argument == "--expected-session"));
assert_eq!(plan.target_name(), "kwt-workspace-dir-scratch-abc");
}

#[test]
fn protected_kwt_attach_plan_uses_pr_attach_and_exact_socket_authority() {
let (host, _runner, endpoint, runtime) = kwt_mutation_host();
Expand Down
Loading
Loading