use std::path::Path;
use std::sync::mpsc::{self, Receiver};
use std::thread::{self, JoinHandle};
use windows::Win32::Foundation::{HLOCAL, LocalFree};
use windows::Win32::Security::Authorization::{
ConvertSecurityDescriptorToStringSecurityDescriptorW,
ConvertStringSecurityDescriptorToSecurityDescriptorW, GetNamedSecurityInfoW, SDDL_REVISION_1,
SE_FILE_OBJECT,
};
use windows::Win32::Security::{DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR};
use windows::core::{PCWSTR, PWSTR};
use crate::AppCommand;
use crate::constants::{DEFAULT_PTY_COLS, DEFAULT_PTY_ROWS};
use crate::pal::ids::{AppId, JobId, PtyId};
use crate::pal::processes::{
AppSpawn, Breakaway, BuildTargetProcesses, Processes, ProcessesFacade,
};
use crate::pal::pseudoconsole::{Pseudoconsole, PseudoconsoleFacade, WindowSize};
use crate::pal::transport::current_user_sid_string;
#[must_use]
pub fn current_user_sid() -> String {
current_user_sid_string().expect("the current user has a SID")
}
#[must_use]
pub fn current_user_file_dacl_sddl() -> String {
let sid = current_user_sid();
let sddl = format!("D:P(A;;FA;;;{sid})");
let wide: Vec<u16> = sddl.encode_utf16().chain(std::iter::once(0)).collect();
let mut descriptor = PSECURITY_DESCRIPTOR::default();
unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
PCWSTR(wide.as_ptr()),
SDDL_REVISION_1,
&raw mut descriptor,
None,
)
}
.expect("the expected DACL is valid SDDL");
let text = descriptor_dacl_sddl(descriptor);
unsafe {
_ = LocalFree(Some(HLOCAL(descriptor.0.cast())));
}
text
}
#[must_use]
pub fn dacl_sddl(object: &str) -> String {
let wide: Vec<u16> = object.encode_utf16().chain(std::iter::once(0)).collect();
let mut descriptor = PSECURITY_DESCRIPTOR::default();
let queried = unsafe {
GetNamedSecurityInfoW(
PCWSTR(wide.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
None,
None,
&raw mut descriptor,
)
};
assert!(
queried.is_ok(),
"reading the security of {object:?}: {queried:?}"
);
let text = descriptor_dacl_sddl(descriptor);
unsafe {
_ = LocalFree(Some(HLOCAL(descriptor.0.cast())));
}
text
}
fn descriptor_dacl_sddl(descriptor: PSECURITY_DESCRIPTOR) -> String {
let mut sddl = PWSTR::null();
let converted = unsafe {
ConvertSecurityDescriptorToStringSecurityDescriptorW(
descriptor,
SDDL_REVISION_1,
DACL_SECURITY_INFORMATION,
&raw mut sddl,
None,
)
};
let text = converted
.is_ok()
.then(|| String::from_utf16_lossy(unsafe { sddl.as_wide() }));
unsafe {
if !sddl.is_null() {
_ = LocalFree(Some(HLOCAL(sddl.0.cast())));
}
}
text.expect("a security descriptor converts to SDDL")
}
#[derive(Debug)]
pub struct ConsoleProcess {
pty_host: PseudoconsoleFacade,
pty: PtyId,
job: JobId,
waiter: Option<JoinHandle<i32>>,
closed: bool,
}
impl ConsoleProcess {
#[must_use]
pub fn spawn(exe: &Path, args: &[String], cwd: &Path) -> Self {
Self::spawn_in_jobs(exe, args, cwd, &[Breakaway::Permitted])
}
#[must_use]
pub fn spawn_confined(exe: &Path, args: &[String], cwd: &Path) -> Self {
Self::spawn_in_jobs(exe, args, cwd, &[Breakaway::Forbidden])
}
#[must_use]
pub fn spawn_confined_by_ancestor(exe: &Path, args: &[String], cwd: &Path) -> Self {
Self::spawn_in_jobs(
exe,
args,
cwd,
&[Breakaway::Forbidden, Breakaway::Permitted],
)
}
fn spawn_in_jobs(exe: &Path, args: &[String], cwd: &Path, jobs: &[Breakaway]) -> Self {
let processes = ProcessesFacade::target();
let pty_host = PseudoconsoleFacade::target();
let job = BuildTargetProcesses::create_job_chain(jobs).expect("create test job");
let pty = pty_host
.create(WindowSize {
cols: DEFAULT_PTY_COLS,
rows: DEFAULT_PTY_ROWS,
})
.expect("create test pseudoconsole");
let mut argv = Vec::with_capacity(args.len().saturating_add(1));
argv.push(exe.to_string_lossy().into_owned());
argv.extend(args.iter().cloned());
let command = AppCommand::from_argv(argv).expect("test argv names an executable");
let app = processes
.spawn_app(&AppSpawn {
command,
launch_directory: cwd.to_path_buf(),
pty,
job,
})
.expect("spawn test client in pseudoconsole");
Self {
pty_host: pty_host.clone(),
pty,
job,
waiter: Some(spawn_waiter(&processes, &pty_host, app, pty)),
closed: false,
}
}
pub fn write_input(&self, data: &[u8]) {
self.pty_host
.write_input(self.pty, data)
.expect("write test console input");
}
pub fn resize(&self, cols: u16, rows: u16) {
let size =
WindowSize::new(cols, rows).expect("a test resizes to a size a console can have");
self.pty_host
.resize(self.pty, size)
.expect("resize test console");
}
#[must_use]
pub fn output_until_exit(&self) -> Receiver<Vec<u8>> {
let (sender, receiver) = mpsc::channel();
thread::spawn({
let pty_host = self.pty_host.clone();
let pty = self.pty;
move || {
loop {
match pty_host.read_output(pty) {
Ok(Some(bytes)) => {
if sender.send(bytes).is_err() {
break;
}
}
Ok(None) => break,
Err(error) => panic!("reading test console output: {error}"),
}
}
}
});
receiver
}
#[must_use]
pub fn wait(mut self) -> i32 {
let drain = self.output_until_exit();
let status = self
.waiter
.take()
.expect("a console process waits for its child exactly once")
.join()
.expect("wait test child");
for _chunk in drain {}
self.shutdown();
status
}
fn shutdown(&mut self) {
if self.closed {
return;
}
ProcessesFacade::target().close_job(self.job);
self.pty_host.close(self.pty);
self.closed = true;
}
}
fn spawn_waiter(
processes: &ProcessesFacade,
pty_host: &PseudoconsoleFacade,
app: AppId,
pty: PtyId,
) -> JoinHandle<i32> {
thread::spawn({
let processes = processes.clone();
let pty_host = pty_host.clone();
move || {
let status = processes.wait_app(app).expect("wait test child");
pty_host.finish(pty);
status
}
})
}
impl Drop for ConsoleProcess {
fn drop(&mut self) {
self.shutdown();
}
}