fix: macOS and Windows test/lint failures; repo.dixon.cx packaging for nexusterminal

CI hadn't run on macOS or Windows for a while (billing); once it did:
- Windows: the RDP shared-drive code and its symlink test are Unix-only
  but were compiled there (and the unused pieces failed clippy). The drive
  type, its impls, imports and test are now cfg(unix); refusing a drive
  request is a free function used everywhere. Other Windows-only warnings
  fixed (serial test import, unused mut, harden_process).
- macOS: the local shell's directory was sampled on the first output
  after Enter, which is the shell echoing the command before a cd runs,
  so restored tabs opened one directory behind. It's now sampled shortly
  after Enter and again once the command has had time to finish.
- packaging/repo.sh publishes the 'nexusterminal' .deb/.rpm/Arch packages
  to repo.dixon.cx from the release's own Linux binaries, verified
  against SHA256SUMS.
- The desktop entry mentions RDP.
This commit is contained in:
Andy Dixon
2026-10-13 11:36:45 +01:00
parent 9805681c2c
commit 73c1f77ca3
7 changed files with 291 additions and 40 deletions
+14 -3
View File
@@ -171,6 +171,9 @@ impl LocalShell {
}
}
/// macOS has no cheap cwd lookup (it's an `lsof` run), so the directory is sampled after Enter:
/// the first output after Enter is usually the shell echoing the command, before a `cd` has
/// run, so sample shortly afterwards and again once the command has had time to finish.
fn capture_cwd_in_background(self: &Arc<Self>) {
let Some(pid) = self.state.lock().0.as_ref().and_then(|r| r.pid) else { return };
if self.capturing.swap(true, Ordering::SeqCst) {
@@ -178,9 +181,12 @@ impl LocalShell {
}
let me = self.clone();
std::thread::spawn(move || {
// A failed lookup (the shell already exited) keeps the last known directory.
if let Some(dir) = process_cwd(pid).filter(|d| d.is_dir()) {
me.state.lock().1 = dir;
for delay in [150, 850] {
std::thread::sleep(std::time::Duration::from_millis(delay));
// A failed lookup (the shell already exited) keeps the last known directory.
if let Some(dir) = process_cwd(pid).filter(|d| d.is_dir()) {
me.state.lock().1 = dir;
}
}
me.capturing.store(false, Ordering::SeqCst);
});
@@ -223,6 +229,11 @@ mod tests {
std::thread::sleep(Duration::from_millis(20));
}
sh.resize(100, 30).unwrap();
// macOS samples the directory shortly after a command runs (Linux reads it live).
while sh.working_directory() != std::path::Path::new("/") {
assert!(t.elapsed() < Duration::from_secs(5), "cwd still {:?}", sh.working_directory());
std::thread::sleep(Duration::from_millis(20));
}
sh.write(b"exit\n").unwrap();
while *closed.lock() == 0 {
assert!(t.elapsed() < Duration::from_secs(5));
+1
View File
@@ -205,6 +205,7 @@ impl Client {
}
if self.cfg.shared_folder.is_some() || self.cfg.printer.is_some() {
let mut devices = super::rdp_devices::Devices::default();
#[cfg_attr(not(unix), allow(unused_mut))]
let mut drives = vec![];
#[cfg(unix)]
if let Some((folder, name)) = &self.cfg.shared_folder {
+34 -26
View File
@@ -14,9 +14,11 @@ use ironrdp::core::impl_as_any;
use ironrdp::pdu::PduResult;
use ironrdp::rdpdr::RdpdrBackend;
use ironrdp::rdpdr::pdu::RdpdrPdu;
#[cfg(unix)]
use ironrdp::rdpdr::pdu::efs::FileInformationClass;
use ironrdp::rdpdr::pdu::efs::{
ClientDriveQueryDirectoryResponse, ClientDriveSetInformationResponse, DeviceCloseResponse, DeviceControlRequest, DeviceCreateResponse, DeviceIoResponse,
DeviceWriteResponse, FileInformationClass, Information, NtStatus, PrinterIoRequest, ServerDeviceAnnounceResponse, ServerDriveIoRequest,
DeviceWriteResponse, Information, NtStatus, PrinterIoRequest, ServerDeviceAnnounceResponse, ServerDriveIoRequest,
};
use ironrdp::rdpdr::pdu::esc::{ScardCall, ScardIoCtlCode};
use ironrdp::svc::SvcMessage;
@@ -24,7 +26,9 @@ use ironrdp::svc::SvcMessage;
use ironrdp_rdpdr_native::backend::NixRdpdrBackend;
use std::collections::HashMap;
use std::io::Write;
use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::path::Path;
use std::path::PathBuf;
/// A print job may not grow past this (a hostile server could otherwise fill the disk).
const MAX_PRINT_JOB: u64 = 256 << 20;
@@ -32,6 +36,7 @@ const MAX_PRINT_JOB: u64 = 256 << 20;
const MAX_OPEN_PRINT_JOBS: usize = 4;
/// Largest drive read served in one request. The server picks the length (a u32), and the
/// native backend allocates it before reading; Windows clients never ask for more than ~1 MiB.
#[cfg(unix)]
const MAX_DRIVE_READ: u32 = 8 << 20;
/// Called with the file a finished print job was saved to.
@@ -151,7 +156,7 @@ impl RdpdrBackend for Devices {
if let Some(d) = &mut self.drive {
return d.handle(req);
}
ConfinedDrive::refuse(req)
refuse_drive_io(req)
}
fn handle_printer_io_request(&mut self, req: PrinterIoRequest) -> PduResult<Vec<SvcMessage>> {
@@ -164,14 +169,35 @@ impl RdpdrBackend for Devices {
}
}
/// Answers a drive request with a refusal (no shared folder, or a path outside it).
fn refuse_drive_io(req: ServerDriveIoRequest) -> PduResult<Vec<SvcMessage>> {
tracing::warn!("RDP server asked for a path outside the shared folder; refused");
let pdu = match req {
ServerDriveIoRequest::ServerCreateDriveRequest(r) => RdpdrPdu::DeviceCreateResponse(DeviceCreateResponse {
device_io_reply: DeviceIoResponse::new(r.device_io_request, NtStatus::ACCESS_DENIED),
file_id: 0,
information: Information::empty(),
}),
ServerDriveIoRequest::ServerDriveQueryDirectoryRequest(r) => RdpdrPdu::ClientDriveQueryDirectoryResponse(ClientDriveQueryDirectoryResponse {
device_io_reply: DeviceIoResponse::new(r.device_io_request, NtStatus::NO_MORE_FILES),
buffer: None,
}),
ServerDriveIoRequest::ServerDriveSetInformationRequest(r) => RdpdrPdu::ClientDriveSetInformationResponse(
ClientDriveSetInformationResponse::new(&r, NtStatus::ACCESS_DENIED).map_err(|e| ironrdp::pdu::encode_err!(e))?,
),
_ => return Ok(vec![]),
};
Ok(vec![SvcMessage::from(pdu)])
}
#[cfg(unix)]
#[derive(Debug)]
struct ConfinedDrive {
#[cfg(unix)]
inner: NixRdpdrBackend,
#[cfg_attr(not(unix), allow(dead_code))]
base: PathBuf,
}
#[cfg(unix)]
impl ConfinedDrive {
pub fn new(folder: &Path) -> std::io::Result<ConfinedDrive> {
let base = folder.canonicalize()?;
@@ -211,28 +237,9 @@ impl ConfinedDrive {
}
}
}
fn refuse(req: ServerDriveIoRequest) -> PduResult<Vec<SvcMessage>> {
tracing::warn!("RDP server asked for a path outside the shared folder; refused");
let pdu = match req {
ServerDriveIoRequest::ServerCreateDriveRequest(r) => RdpdrPdu::DeviceCreateResponse(DeviceCreateResponse {
device_io_reply: DeviceIoResponse::new(r.device_io_request, NtStatus::ACCESS_DENIED),
file_id: 0,
information: Information::empty(),
}),
ServerDriveIoRequest::ServerDriveQueryDirectoryRequest(r) => RdpdrPdu::ClientDriveQueryDirectoryResponse(ClientDriveQueryDirectoryResponse {
device_io_reply: DeviceIoResponse::new(r.device_io_request, NtStatus::NO_MORE_FILES),
buffer: None,
}),
ServerDriveIoRequest::ServerDriveSetInformationRequest(r) => RdpdrPdu::ClientDriveSetInformationResponse(
ClientDriveSetInformationResponse::new(&r, NtStatus::ACCESS_DENIED).map_err(|e| ironrdp::pdu::encode_err!(e))?,
),
_ => return Ok(vec![]),
};
Ok(vec![SvcMessage::from(pdu)])
}
}
#[cfg(unix)]
impl ConfinedDrive {
#[cfg(unix)]
fn handle(&mut self, mut req: ServerDriveIoRequest) -> PduResult<Vec<SvcMessage>> {
@@ -250,7 +257,7 @@ impl ConfinedDrive {
// Everything else names a file by an id that a checked create handed out.
_ => true,
};
if ok { self.inner.handle_drive_io_request(req) } else { Self::refuse(req) }
if ok { self.inner.handle_drive_io_request(req) } else { refuse_drive_io(req) }
}
}
@@ -314,6 +321,7 @@ mod tests {
std::fs::remove_dir_all(dir).unwrap();
}
#[cfg(unix)]
#[test]
fn server_paths_stay_inside_the_share() {
let root = std::env::temp_dir().join(format!("nexus-drive-{}", std::process::id()));
+1
View File
@@ -206,6 +206,7 @@ fn open_error(path: &str, e: &serialport::Error) -> crate::Error {
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
use std::sync::mpsc;
#[test]