rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood00e85d6303bc4ed7132b00acdc88f41e2ceaf695
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "5109cda98885b00c1d4f7165013e4d66f9b7512f",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"title": "node: Use winpipe for control socket on Windows",
"state": {
"status": "merged",
"conflicts": []
},
"before": "a670b6e668761ea5893b7d343b4515b888712f14",
"after": "00e85d6303bc4ed7132b00acdc88f41e2ceaf695",
"commits": [
"00e85d6303bc4ed7132b00acdc88f41e2ceaf695"
],
"target": "3419821106e274c96f56948a05d2f51f28f5a6c7",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "5109cda98885b00c1d4f7165013e4d66f9b7512f",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "As `std::os::unix` is obviously not available on Windows, resort to the\n`winpipe` crate, which implements a very similar API for named pipes.\n\nApart from simple changes to imports, we need to jump through a few\nhoops because the `std::io::{Read, Write}` impls are on `&mut` for\n`winpipe`, thus yielding different mutability requirements on Unix vs.\nWindows.\n\nWe resort to cloning, but as this fallible, swallow the added complexity\nof a platform-dependent implementation to not risk panics on Unix.",
"base": "a670b6e668761ea5893b7d343b4515b888712f14",
"oid": "55c073044d0515f4782f7823bac55ea68a8c3db6",
"timestamp": 1755855522
},
{
"id": "f2060aa6253e8d2ce9dd0a411f9d1be77880a516",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "Review",
"base": "a670b6e668761ea5893b7d343b4515b888712f14",
"oid": "00e85d6303bc4ed7132b00acdc88f41e2ceaf695",
"timestamp": 1755864816
},
{
"id": "6e77ad97ce195b1b8c69e5ba8101178f7109cfea",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "Rebased",
"base": "ce11f03fe63b458c38c331e1c0441b3967b44fc5",
"oid": "4e5e9d31019ce14d73d606d760f612aeabc9de3f",
"timestamp": 1755873662
},
{
"id": "27cef672743d76b560f17afa00ad3bd3d887d30a",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "Changes:\n- Missed `cargo fmt`",
"base": "ce11f03fe63b458c38c331e1c0441b3967b44fc5",
"oid": "19a262d3d67ea92a37ceaf18e79a783d8e65ccdd",
"timestamp": 1755873968
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "ecb87542-9e4c-454a-83b8-44858a27b62f"
},
"info_url": "https://cci.rad.levitte.org//ecb87542-9e4c-454a-83b8-44858a27b62f.html"
}
Started at: 2026-09-08 12:00:49.690591+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/ecb87542-9e4c-454a-83b8-44858a27b62f/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 189 issues · 38 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 5109cda98885b00c1d4f7165013e4d66f9b7512f
✓ Switched to branch patch/5109cda at revision f2060aa
✓ Branch patch/5109cda setup to track rad/patches/5109cda98885b00c1d4f7165013e4d66f9b7512f
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 00e85d6303bc4ed7132b00acdc88f41e2ceaf695
HEAD is now at 00e85d63 node: Use winpipe for control socket on Windows
Exit code: 0
$ rad patch show 5109cda98885b00c1d4f7165013e4d66f9b7512f -p
╭───────────────────────────────────────────────────────────────────────────────╮
│ Title node: Use winpipe for control socket on Windows │
│ Patch 5109cda98885b00c1d4f7165013e4d66f9b7512f │
│ Author lorenz z6MkkPv…WX5sTEz │
│ Head 00e85d6303bc4ed7132b00acdc88f41e2ceaf695 │
│ Base a670b6e668761ea5893b7d343b4515b888712f14 │
│ Target master │
│ Branches patch/5109cda │
│ Commits ahead 1, behind 896 │
│ Status merged │
│ │
│ As `std::os::unix` is obviously not available on Windows, resort to the │
│ `winpipe` crate, which implements a very similar API for named pipes. │
│ │
│ Apart from simple changes to imports, we need to jump through a few │
│ hoops because the `std::io::{Read, Write}` impls are on `&mut` for │
│ `winpipe`, thus yielding different mutability requirements on Unix vs. │
│ Windows. │
│ │
│ We resort to cloning, but as this fallible, swallow the added complexity │
│ of a platform-dependent implementation to not risk panics on Unix. │
├───────────────────────────────────────────────────────────────────────────────┤
│ 00e85d6 node: Use winpipe for control socket on Windows │
├───────────────────────────────────────────────────────────────────────────────┤
│ ● Revision 5109cda @ a670b6e..55c0730 by lorenz z6MkkPv…WX5sTEz 1 year ago │
│ ↑ Revision f2060aa @ a670b6e..00e85d6 by lorenz z6MkkPv…WX5sTEz 1 year ago │
│ ↑ Revision 6e77ad9 @ ce11f03..4e5e9d3 by fintohaps z6Mkire…SQZ3voM 1 year ago │
│ ↑ Revision 27cef67 @ ce11f03..19a262d by fintohaps z6Mkire…SQZ3voM 1 year ago │
│ └─ ✓ merged by fintohaps z6Mkire…SQZ3voM │
╰───────────────────────────────────────────────────────────────────────────────╯
commit 00e85d6303bc4ed7132b00acdc88f41e2ceaf695
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Thu Aug 21 18:16:03 2025 +0200
node: Use winpipe for control socket on Windows
As `std::os::unix` is obviously not available on Windows, resort to the
`winpipe` crate, which implements a very similar API for named pipes.
Apart from simple changes to imports, we need to jump through a few
hoops because the `std::io::{Read, Write}` impls are on `&mut` for
`winpipe`, thus yielding different mutability requirements on Unix vs.
Windows.
We resort to cloning, but as this fallible, swallow the added complexity
of a platform-dependent implementation to not risk panics on Unix.
diff --git a/Cargo.lock b/Cargo.lock
index 0cbb7436a..c30595326 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2870,6 +2870,7 @@ dependencies = [
"tempfile",
"test-log",
"thiserror 1.0.69",
+ "winpipe",
]
[[package]]
diff --git a/crates/radicle-node/Cargo.toml b/crates/radicle-node/Cargo.toml
index e5fea0619..667b084c5 100644
--- a/crates/radicle-node/Cargo.toml
+++ b/crates/radicle-node/Cargo.toml
@@ -51,6 +51,9 @@ thiserror = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
radicle-systemd = { workspace = true, optional = true }
+[target.'cfg(windows)'.dependencies]
+winpipe = { workspace = true }
+
[dev-dependencies]
qcheck = { workspace = true }
qcheck-macros = { workspace = true }
diff --git a/crates/radicle-node/src/control.rs b/crates/radicle-node/src/control.rs
index 0f40b74da..3ec2a7390 100644
--- a/crates/radicle-node/src/control.rs
+++ b/crates/radicle-node/src/control.rs
@@ -2,11 +2,14 @@
use std::io::prelude::*;
use std::io::BufReader;
use std::io::LineWriter;
-use std::os::unix::net::UnixListener;
-use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::{io, net, time};
+#[cfg(unix)]
+use std::os::unix::net::{UnixListener as Listener, UnixStream as Stream};
+#[cfg(windows)]
+use winpipe::{WinListener as Listener, WinStream as Stream};
+
use radicle::node::Handle;
use serde_json as json;
@@ -30,7 +33,7 @@ pub enum Error {
}
/// Listen for commands on the control socket, and process them.
-pub fn listen<E, H>(listener: UnixListener, handle: H) -> Result<(), Error>
+pub fn listen<E, H>(listener: Listener, handle: H) -> Result<(), Error>
where
H: Handle<Error = runtime::HandleError> + 'static,
H::Sessions: serde::Serialize,
@@ -42,11 +45,11 @@ where
for incoming in listener.incoming() {
match incoming {
- Ok(mut stream) => {
+ Ok(stream) => {
let handle = handle.clone();
thread::spawn(&nid, "control", move || {
- if let Err(e) = command(&stream, handle) {
+ if let Err((e, mut stream)) = command(stream, handle) {
log::error!(target: "control", "Command returned error: {e}");
CommandResult::error(e).to_writer(&mut stream).ok();
@@ -74,15 +77,56 @@ enum CommandError {
Io(#[from] io::Error),
}
-fn command<E, H>(stream: &UnixStream, mut handle: H) -> Result<(), CommandError>
+#[cfg(unix)]
+fn command<E, H>(stream: Stream, handle: H) -> Result<(), (CommandError, Stream)>
+where
+ H: Handle<Error = runtime::HandleError> + 'static,
+ H::Sessions: serde::Serialize,
+ CommandResult<E>: From<H::Event>,
+ E: serde::Serialize,
+{
+ let reader = BufReader::new(&stream);
+ let writer = LineWriter::new(&stream);
+
+ command_internal(reader, writer, handle).map_err(|e| (e, stream))
+}
+
+
+/// Due to different mutability requirements between Unix and Windows,
+/// we are forced to clone the stream on Windows.
+///
+/// # Errors
+///
+/// As of winpipe 0.1.1, [`WinStream::try_clone`] is actually infallible.
+#[cfg(windows)]
+fn command<E, H>(mut stream: Stream, handle: H) -> Result<(), (CommandError, Stream)>
+where
+ H: Handle<Error = runtime::HandleError> + 'static,
+ H::Sessions: serde::Serialize,
+ CommandResult<E>: From<H::Event>,
+ E: serde::Serialize,
+{
+ let mut clone = stream.try_clone().map_err(|e| (e.into(), stream))?;
+ let reader = BufReader::new(&mut clone);
+ let writer = LineWriter::new(&mut stream);
+
+ command_internal(reader, writer, handle).map_err(|e| (e, stream))
+}
+
+#[inline(always)]
+fn command_internal<E, H, R, W>(
+ mut reader: BufReader<R>,
+ mut writer: LineWriter<W>,
+ mut handle: H,
+) -> Result<(), CommandError>
where
H: Handle<Error = runtime::HandleError> + 'static,
H::Sessions: serde::Serialize,
CommandResult<E>: From<H::Event>,
E: serde::Serialize,
+ R: io::Read,
+ W: io::Write,
{
- let mut reader = BufReader::new(stream);
- let mut writer = LineWriter::new(stream);
let mut line = String::new();
reader.read_line(&mut line)?;
@@ -241,7 +285,6 @@ fn fetch<W: Write, H: Handle<Error = runtime::HandleError>>(
#[cfg(test)]
mod tests {
use std::io::prelude::*;
- use std::os::unix::net::UnixStream;
use std::thread;
use super::*;
@@ -257,7 +300,7 @@ mod tests {
let handle = test::handle::Handle::default();
let socket = tmp.path().join("alice.sock");
let rids = test::arbitrary::set::<RepoId>(1..3);
- let listener = UnixListener::bind(&socket).unwrap();
+ let listener = Listener::bind(&socket).unwrap();
thread::spawn({
let handle = handle.clone();
@@ -267,7 +310,7 @@ mod tests {
for rid in &rids {
let stream = loop {
- if let Ok(stream) = UnixStream::connect(&socket) {
+ if let Ok(stream) = Stream::connect(&socket) {
break stream;
}
};
@@ -305,7 +348,7 @@ mod tests {
let socket = tmp.path().join("node.sock");
let proj = test::arbitrary::gen::<RepoId>(1);
let peer = test::arbitrary::gen::<NodeId>(1);
- let listener = UnixListener::bind(&socket).unwrap();
+ let listener = Listener::bind(&socket).unwrap();
let mut handle = Node::new(&socket);
thread::spawn({
diff --git a/crates/radicle-node/src/runtime.rs b/crates/radicle-node/src/runtime.rs
index b0de29056..8639e93b9 100644
--- a/crates/radicle-node/src/runtime.rs
+++ b/crates/radicle-node/src/runtime.rs
@@ -1,10 +1,14 @@
pub mod handle;
pub mod thread;
-use std::os::unix::net::UnixListener;
use std::path::PathBuf;
use std::{fs, io, net};
+#[cfg(unix)]
+use std::os::unix::net::UnixListener as Listener;
+#[cfg(windows)]
+use winpipe::WinListener as Listener;
+
use crossbeam_channel as chan;
use cyphernet::Ecdh;
use netservices::resource::NetAccept;
@@ -100,9 +104,9 @@ impl From<service::Error> for Error {
/// Wraps a [`UnixListener`] but tracks its origin.
pub enum ControlSocket {
/// The listener was created by binding to it.
- Bound(UnixListener, PathBuf),
+ Bound(Listener, PathBuf),
/// The listener was received via socket activation.
- Received(UnixListener),
+ Received(Listener),
}
/// Holds join handles to the client threads, as well as a client handle.
@@ -313,7 +317,7 @@ impl Runtime {
}
#[cfg(all(feature = "systemd", target_os = "linux"))]
- fn receive_listener() -> Option<UnixListener> {
+ fn receive_listener() -> Option<Listener> {
use std::os::fd::FromRawFd;
match radicle_systemd::listen::fd("control") {
Ok(Some(fd)) => {
@@ -327,7 +331,7 @@ impl Runtime {
Some(unsafe {
// SAFETY: We take ownership of this FD from systemd,
// which guarantees that it is open.
- UnixListener::from_raw_fd(fd)
+ Listener::from_raw_fd(fd)
})
}
Ok(None) => None,
@@ -348,7 +352,7 @@ impl Runtime {
}
log::info!(target: "node", "Binding control socket {}..", &path.display());
- match UnixListener::bind(&path) {
+ match Listener::bind(&path) {
Ok(sock) => Ok(ControlSocket::Bound(sock, path)),
Err(err) if err.kind() == io::ErrorKind::AddrInUse => Err(Error::AlreadyRunning(path)),
Err(err) => Err(err.into()),
diff --git a/crates/radicle-node/src/runtime/handle.rs b/crates/radicle-node/src/runtime/handle.rs
index f93f61cb1..d071308fb 100644
--- a/crates/radicle-node/src/runtime/handle.rs
+++ b/crates/radicle-node/src/runtime/handle.rs
@@ -1,9 +1,13 @@
use std::net;
-use std::os::unix::net::UnixStream;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::{fmt, io, time};
+#[cfg(unix)]
+use std::os::unix::net::UnixStream as Stream;
+#[cfg(windows)]
+use winpipe::WinStream as Stream;
+
use crossbeam_channel as chan;
use radicle::node::events::{Event, Events};
use radicle::node::policy;
@@ -320,7 +324,7 @@ impl radicle::node::Handle for Handle {
// Send a shutdown request to our own control socket. This is the only way to kill the
// control thread gracefully. Since the control thread may have called this function,
// the control socket may already be disconnected. Ignore errors.
- UnixStream::connect(self.home.socket())
+ Stream::connect(self.home.socket())
.and_then(|sock| Command::Shutdown.to_writer(sock))
.ok();
Exit code: 0
shell: 'cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name ecb87542-9e4c-454a-83b8-44858a27b62f -v /opt/radcis/ci.rad.levitte.org/cci/state/ecb87542-9e4c-454a-83b8-44858a27b62f/s:/ecb87542-9e4c-454a-83b8-44858a27b62f/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/ecb87542-9e4c-454a-83b8-44858a27b62f/w:/ecb87542-9e4c-454a-83b8-44858a27b62f/w -w /ecb87542-9e4c-454a-83b8-44858a27b62f/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /ecb87542-9e4c-454a-83b8-44858a27b62f/s/script.sh
+ cargo --version
info: syncing channel updates for '1.88-x86_64-unknown-linux-gnu'
info: latest update on 2025-06-26, rust version 1.88.0 (6b00bc388 2025-06-23)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.88.0 (873a06493 2025-05-10)
+ rustc --version
rustc 1.88.0 (6b00bc388 2025-06-23)
+ cargo fmt --check
Diff in /ecb87542-9e4c-454a-83b8-44858a27b62f/w/crates/radicle-node/src/control.rs:91:
command_internal(reader, writer, handle).map_err(|e| (e, stream))
}
-
/// Due to different mutability requirements between Unix and Windows,
/// we are forced to clone the stream on Windows.
///
Exit code: 1
{
"response": "finished",
"result": "failure"
}