diff --git a/plugin/src/lib.rs b/plugin/src/lib.rs index 1128e84..e8f4da3 100644 --- a/plugin/src/lib.rs +++ b/plugin/src/lib.rs @@ -6,7 +6,7 @@ use log_instrument::instrument; use plugin::{Result, WSLPluginV1}; use std::{env, io::Read}; use windows::{ - core::{Error as WinError, Result as WinResult, GUID}, + core::{Error as WinError, Result as WinResult}, Win32::Foundation::E_FAIL, }; use wslplugins_rs::wsl_user_configuration::bitflags::WSLUserConfigurationFlags; @@ -58,12 +58,12 @@ impl WSLPluginV1 for Plugin { ) -> Result<()> { let flags: WSLUserConfigurationFlags = user_settings.custom_configuration_flags().into(); info!("User configuration {:?}", flags); - - let ver_args = ["/bin/cat", "/proc/version"]; match self .context .api - .execute_binary(session, ver_args[0], &ver_args) + .new_command(session, "/bin/cat") + .arg("/proc/version") + .execute() { Ok(mut stream) => { let mut buf = String::new(); @@ -81,7 +81,7 @@ impl WSLPluginV1 for Plugin { ) } }; - self.log_os_release(session, None); + self.log_os_release(session, DistributionID::System); Ok(()) } @@ -101,7 +101,7 @@ impl WSLPluginV1 for Plugin { // Use unknow if init_pid not available distribution.init_pid().map(|res| res.to_string()).unwrap_or("Unknow".to_string()) ); - self.log_os_release(session, Some(distribution.id())); + self.log_os_release(session, DistributionID::User(distribution.id())); Ok(()) } @@ -132,21 +132,15 @@ impl WSLPluginV1 for Plugin { } impl Plugin { - fn log_os_release(&self, session: &WSLSessionInformation, distro_id: Option) { - let args: [&str; 2] = ["/bin/cat", "/etc/os-release"]; - let tcp_stream: std::result::Result = match distro_id { - Some(dist_id) => self - .context - .api - .execute_binary_in_distribution(session, dist_id, args[0], &args), - None => self - .context - .api - .execute_binary(session, args[0], &args) - .map_err(Into::into), - }; - let result = tcp_stream; - match result { + fn log_os_release(&self, session: &WSLSessionInformation, distro_id: DistributionID) { + match self + .context + .api + .new_command(session, "/bin/cat") + .arg("/etc/os-release") + .distribution_id(distro_id) + .execute() + { Ok(stream) => match OsRelease::from_reader(stream) { Ok(release) => { if let Some(version) = release.version() { diff --git a/wslplugins-rs/src/api/api_v1.rs b/wslplugins-rs/src/api/api_v1.rs index 2be24bd..712e03c 100644 --- a/wslplugins-rs/src/api/api_v1.rs +++ b/wslplugins-rs/src/api/api_v1.rs @@ -1,7 +1,7 @@ extern crate wslplugins_sys; #[cfg(doc)] use super::Error; -use super::Result; +use super::{Result, WSLCommand}; use crate::api::errors::require_update_error::Result as UpReqResult; use crate::utils::{cstring_from_str, encode_wide_null_terminated}; use crate::wsl_session_information::WSLSessionInformation; @@ -264,6 +264,30 @@ impl ApiV1 { }; Ok(stream) } + /// Creates a new `WSLCommand` instance tied to the current WSL API. + /// + /// This method initializes a `WSLCommand` with the provided session and + /// program details. The program is specified as a path that can be converted + /// to a `Utf8UnixPath`. + /// + /// # Parameters + /// - `session`: The session information associated with the WSL instance. + /// - `program`: A reference to the path of the program to be executed, + /// represented as an object implementing `AsRef`. + /// + /// # Returns + /// A new instance of `WSLCommand` configured to execute the specified program + /// within the provided WSL session. + /// + /// # Type Parameters + /// - `T`: A type that implements `AsRef`. + pub fn new_command<'a, T: AsRef + ?Sized>( + &'a self, + session: &'a WSLSessionInformation<'a>, + program: &'a T, + ) -> WSLCommand<'a> { + WSLCommand::new(self, session, program) + } fn check_required_version(&self, version: &WSLVersion) -> UpReqResult<()> { check_required_version_result(self.version(), version) diff --git a/wslplugins-rs/src/api/mod.rs b/wslplugins-rs/src/api/mod.rs index 01cfcc7..b01f0b1 100644 --- a/wslplugins-rs/src/api/mod.rs +++ b/wslplugins-rs/src/api/mod.rs @@ -26,3 +26,5 @@ pub use errors::Result; /// /// These utilities simplify common tasks, such as version checking or string manipulation. pub mod utils; +mod wsl_command; +pub use wsl_command::WSLCommand; diff --git a/wslplugins-rs/src/api/wsl_command.rs b/wslplugins-rs/src/api/wsl_command.rs new file mode 100644 index 0000000..b4ea823 --- /dev/null +++ b/wslplugins-rs/src/api/wsl_command.rs @@ -0,0 +1,221 @@ +use typed_path::Utf8UnixPath; + +#[cfg(doc)] +use super::super::api::Error as ApiError; +use super::super::api::{ApiV1, Result as ApiResult}; +use crate::{DistributionID, WSLSessionInformation}; +use std::net::TcpStream; +#[cfg(doc)] +use windows::core::GUID; + +/// Represents a command to be executed in WSL. +/// +/// The `WSLCommand` struct encapsulates details such as the program path, arguments, +/// and the associated distribution ID for execution. +#[derive(Clone)] +pub struct WSLCommand<'a> { + /// The WSL context associated with the command. + api: &'a ApiV1<'a>, + /// Arguments for the command. + args: Vec<&'a str>, + /// Path to the program being executed. + path: &'a Utf8UnixPath, + /// The distribution ID under which the command is executed. + distribution_id: DistributionID, + /// Session information for the current WSL session. + session: &'a WSLSessionInformation<'a>, +} + +impl<'a> WSLCommand<'a> { + /// Creates a new `WSLCommand` instance. + /// + /// This function initializes a `WSLCommand` with the necessary details to + /// execute a program in a WSL instance. + /// + /// # Parameters + /// - `api`: A reference to the WSL API version 1. + /// - `session`: The session information associated with the WSL instance. + /// - `program`: A reference to the path of the program to be executed, + /// represented as an object implementing `AsRef`. + /// + /// # Returns + /// A new `WSLCommand` instance. + /// + /// # Type Parameters + /// - `T`: A type that implements `AsRef`. + pub(crate) fn new + ?Sized>( + api: &'a ApiV1<'a>, + session: &'a WSLSessionInformation<'a>, + program: &'a T, + ) -> Self { + let my_program = program.as_ref(); + let program_str = my_program.as_str(); + Self { + api, + args: vec![program_str], + path: my_program, + distribution_id: DistributionID::System, + session, + } + } + + /// Returns the path of the command. + pub fn get_path(&self) -> &'a Utf8UnixPath { + self.path + } + + /// Sets the first argument (arg0) of the command. + /// + /// # Parameters + /// - `arg0`: The new value for the first argument. + pub fn arg0 + ?Sized>(&mut self, arg0: &'a T) -> &mut Self { + self.args[0] = arg0.as_ref(); + self + } + + /// Gets the first argument (arg0) of the command. + pub fn get_arg0(&self) -> &str { + self.args[0] + } + + /// Checks if the first argument is the standard argument (the path). + pub fn is_standard_arg_0(&self) -> bool { + self.path == self.get_arg0() + } + + /// Resets the first argument to the path of the command. + pub fn reset_arg0(&mut self) -> &mut Self { + self.args[0] = self.path.as_str(); + self + } + + /// Adds an argument to the command. + /// + /// # Parameters + /// - `arg`: The argument to add. + pub fn arg + ?Sized>(&mut self, arg: &'a T) -> &mut Self { + self.args.push(arg.as_ref()); + self + } + + /// Adds multiple arguments to the command. + /// + /// # Parameters + /// - `args`: An iterator of arguments to add. + pub fn args(&mut self, args: I) -> &mut Self + where + I: IntoIterator, + T: 'a + AsRef + ?Sized, + { + self.args.extend(args.into_iter().map(AsRef::as_ref)); + self + } + + /// Returns an iterator over the arguments of the command, excluding arg0. + pub fn get_args(&self) -> impl ExactSizeIterator { + self.args[1..].iter().copied() + } + + /// Clears all arguments except arg0. + /// + /// This method removes all additional arguments from the command, + /// effectively resetting the arguments to only include the program path. + /// + /// # Returns + /// A mutable reference to the current `WSLCommand` instance. + /// + /// # Example + /// ```rust ignore + /// command.arg("Hello").arg("World"); + /// command.clear_args(); // Only arg0 ("/bin/echo") remains. + /// assert_eq!(command.get_args().count(), 0) + /// ``` + pub fn crear_args(&mut self) -> &mut Self { + self.truncate_args(0); + self + } + + /// Truncates the arguments of the command after a specified index. + /// + /// This method keeps `arg0` and the first `i` additional arguments, discarding the rest. + /// + /// # Parameters + /// - `i`: The index after which arguments will be removed. Note that `i = 0` keeps only `arg0`. + /// + /// # Returns + /// A mutable reference to the current `WSLCommand` instance. + /// + /// # Example + /// ```rust ignore + /// let mut command = WSLCommand::new(context, session, "/bin/echo"); + /// command.arg("Hello").arg("World"); + /// command.truncate_args(1); // Keeps only "/bin/echo" and "Hello". + /// ``` + pub fn truncate_args(&mut self, i: usize) -> &mut Self { + self.args.truncate(i + 1); + self + } + + /// Sets the distribution ID for the command. + /// + /// # Parameters + /// - `distribution_id`: The new distribution ID to set. + pub fn distribution_id(&mut self, distribution_id: DistributionID) -> &mut Self { + self.distribution_id = distribution_id; + self + } + + /// Resets the distribution ID to the system default. + pub fn reset_distribution_id(&mut self) -> &mut Self { + self.distribution_id = DistributionID::System; + self + } + + /// Gets the current distribution ID for the command. + pub fn get_distribution_id(&self) -> DistributionID { + self.distribution_id + } + + /// Executes the command and returns a [`TcpStream`]. + /// + /// This method determines the API call to be used based on the [`DistributionID`]: + /// - If [`DistributionID::System`], the method invokes [`execute_binary`](super::ApiV1::execute_binary). + /// - If [`DistributionID::User`], it invokes [`execute_binary_in_distribution`](super::ApiV1::execute_binary_in_distribution) + /// with the associated [GUID]. + /// + /// # Returns + /// - On success, it returns a [`TcpStream`] connected to the executed process, enabling interaction + /// with the process's stdin and stdout. + /// - On failure, an error indicating the cause of the failure. + /// + /// # Errors + /// This function will return an error if: + /// - [`ApiError::RequiresUpdate`]: The WSL runtime version does not support targeting a user distribution. + /// - [`ApiError::WinError`]: The API call fails to execute the binary. + /// + /// # Example + /// ```rust ignore + /// let mut command = ; + /// match WSLCommand::new(context, session, "/bin/echo").arg("Hello, World!").execute() { + /// Ok(stream) => { + /// // Interact with the process via the TcpStream. + /// }, + /// Err(e) => eprintln!("Error: {}", e), + /// } + /// ``` + pub fn execute(&mut self) -> ApiResult { + let stream = match self.distribution_id { + DistributionID::System => { + self.api + .execute_binary(self.session, self.path, self.args.as_slice())? + } + DistributionID::User(id) => self.api.execute_binary_in_distribution( + self.session, + id, + self.path, + self.args.as_slice(), + )?, + }; + Ok(stream) + } +} diff --git a/wslplugins-rs/src/distribution_id.rs b/wslplugins-rs/src/distribution_id.rs new file mode 100644 index 0000000..527de18 --- /dev/null +++ b/wslplugins-rs/src/distribution_id.rs @@ -0,0 +1,119 @@ +//! # Module DistributionID +//! +//! This module defines an abstraction to represent WSL distributions through a +//! `DistributionID`. It supports two types of identifiers: system-level distributions +//! and user-specific installed distributions identified by a GUID. +//! +//! ## Key Features +//! +//! - Bi-directional conversion between [DistributionID] and [GUID]. +//! - Robust error handling for conversions via [ConversionError]. +//! - Display implementation ([Display]) and support for other idiomatic conversions. +//! +//! ## Usage Context +//! +//! This abstraction is particularly useful in environments where WSL requires +//! distribution identification via GUIDs or when a distinction between a system-level +//! distribution and a user-specific distribution is necessary. The associated functions +//! and conversions simplify integration with APIs like those defined in `WslPluginApi`. + +use crate::CoreDistributionInformation; +use std::{convert::TryFrom, fmt::Display}; +use thiserror::Error; +use windows::core::GUID; + +/// Represents a distribution identifier in the Windows Subsystem for Linux (WSL). +/// +/// A distribution can either be the system-level distribution or a user-specific distribution +/// identified by a [GUID]. +/// +/// ## Variants +/// +/// - `System`: Represents the system distribution, a central distribution used by WSL +/// for managing low-level functionalities such as audio and graphical interaction. +/// Refer to the [WSLg Architecture blogpost](https://devblogs.microsoft.com/commandline/wslg-architecture/#system-distro). +/// +/// - `User(GUID)`: Represents an individual distribution installed by a user. Each distribution +/// is uniquely identified by a [GUID], which is consistent across reboots. This GUID +/// corresponds to the identifier used by WSL for managing the distribution. +/// +/// ## Note +/// +/// - The system distribution serves as a foundational component in WSL, often interacting with +/// user distributions for operations like Linux GUI apps. +/// - User distributions provide isolated environments for specific Linux distributions, allowing +/// users to install and run various Linux distributions on their Windows machines. +#[derive(Debug, Clone, Copy)] +pub enum DistributionID { + /// Represents the system-level distribution. + /// For more info about the system distribution please check the [WSLg architecture blogpost](https://devblogs.microsoft.com/commandline/wslg-architecture/#system-distro) + System, + /// Represents an installed user-specific distribution identified by a [GUID]. + User(GUID), +} + +/// Error type for conversion failures between `DistributionID` and GUID. +#[derive(Debug, Error)] +#[error("Cannot convert System distribution to GUID.")] +pub struct ConversionError; + +impl TryFrom for GUID { + type Error = ConversionError; + + /// Attempts to convert a `DistributionID` into a GUID. + /// + /// # Errors + /// Returns `ConversionError` if the `DistributionID` is `System`. + fn try_from(value: DistributionID) -> Result { + match value { + DistributionID::User(id) => Ok(id), + DistributionID::System => Err(ConversionError), + } + } +} + +impl From for DistributionID { + /// Converts a GUID into a `DistributionID`. + fn from(value: GUID) -> Self { + Self::User(value) + } +} + +impl From for DistributionID { + /// Converts a type implementing `CoreDistributionInformation` into a `DistributionID`. + fn from(value: T) -> Self { + value.id().into() + } +} + +impl From> for DistributionID { + /// Converts an `Option` into a `DistributionID`, defaulting to `System` if `None`. + fn from(value: Option) -> Self { + match value { + Some(id) => Self::User(id), + None => Self::System, + } + } +} + +impl From for Option { + /// Converts a `DistributionID` into an `Option`. + fn from(value: DistributionID) -> Self { + match value { + DistributionID::System => None, + DistributionID::User(id) => Some(id), + } + } +} + +impl Display for DistributionID { + /// Formats the `DistributionID` for display. + /// + /// Displays "System" for the system-level distribution, or the GUID for a user-specific distribution. + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DistributionID::System => f.write_str("System"), + DistributionID::User(id) => std::fmt::Debug::fmt(id, f), + } + } +} diff --git a/wslplugins-rs/src/lib.rs b/wslplugins-rs/src/lib.rs index 6dc3da0..a7b0a4d 100644 --- a/wslplugins-rs/src/lib.rs +++ b/wslplugins-rs/src/lib.rs @@ -41,6 +41,7 @@ pub mod api; // Internal modules for managing specific WSL features. mod core_distribution_information; +pub mod distribution_id; mod distribution_information; mod offline_distribution_information; mod utils; @@ -56,6 +57,7 @@ pub mod plugin; // Re-exports for core structures to simplify usage. pub use core_distribution_information::CoreDistributionInformation; +pub use distribution_id::DistributionID; pub use distribution_information::DistributionInformation; pub use offline_distribution_information::OfflineDistributionInformation; pub use wsl_context::WSLContext;