diff --git a/proto/bottles.proto b/proto/bottles.proto index f7dbcfa..8f63f86 100644 --- a/proto/bottles.proto +++ b/proto/bottles.proto @@ -2,21 +2,185 @@ syntax = "proto3"; package bottles; -service Bottles { +// --- Services --- + +service Management { + // Lifecycle + rpc CreateBottle (CreateBottleRequest) returns (Bottle); + rpc DeleteBottle (DeleteBottleRequest) returns (ResultResponse); + rpc ListBottles (ListBottlesRequest) returns (ListBottlesResponse); + rpc GetBottle (GetBottleRequest) returns (Bottle); + + // Power Management (Agent Lifecycle) + rpc StartBottle (BottleRequest) returns (ResultResponse); + rpc StopBottle (BottleRequest) returns (ResultResponse); + rpc RestartBottle (BottleRequest) returns (ResultResponse); +} + +service Configuration { + rpc GetConfig (BottleRequest) returns (BottleConfig); + rpc UpdateConfig (UpdateConfigRequest) returns (BottleConfig); + rpc GetEnvironmentVariables (BottleRequest) returns (EnvironmentVariables); + rpc SetEnvironmentVariables (SetEnvironmentVariablesRequest) returns (ResultResponse); +} + +service Installer { + rpc InstallComponent (InstallComponentRequest) returns (stream InstallProgress); + rpc ListComponents (ListComponentsRequest) returns (ListComponentsResponse); + rpc UninstallComponent (ComponentRequest) returns (ResultResponse); +} + +service Runtime { + rpc LaunchProgram (LaunchProgramRequest) returns (LaunchProgramResponse); + rpc TerminateProgram (TerminateProgramRequest) returns (ResultResponse); + rpc ListRunningProcesses (BottleRequest) returns (ProcessList); +} + +service System { rpc Health (HealthRequest) returns (HealthResponse); rpc Notify (NotifyRequest) returns (NotifyResponse); } -message HealthRequest {} +// --- Messages --- + +message ResultResponse { + bool success = 1; + string error_message = 2; // Optional, set if success is false +} + +// Requests +message CreateBottleRequest { + string name = 1; + string type = 2; // e.g., "Gaming", "Software", "Custom" + string runner = 3; // Optional implementation/runner override +} + +message DeleteBottleRequest { + string name = 1; +} + +message ListBottlesRequest {} + +message ListBottlesResponse { + repeated Bottle bottles = 1; +} + +message GetBottleRequest { + string name = 1; +} + +message BottleRequest { + string name = 1; +} + +// Entities +message Bottle { + string name = 1; + string path = 2; + string type = 3; + bool active = 4; // True if the Agent is running for this bottle + BottleConfig config = 5; +} + +message BottleConfig { + string runner = 1; + string dxvk_version = 2; + string vkd3d_version = 3; + string latencyflex_version = 4; // Optional + bool dxvk_nvapi = 5; + bool esync = 6; + bool fsync = 7; + // Add other relevant settings +} + +message EnvironmentVariables { + map variables = 1; +} + +message SetEnvironmentVariablesRequest { + string bottle_name = 1; + map variables = 2; +} + +message UpdateConfigRequest { + string bottle_name = 1; + BottleConfig config = 2; +} + +// Components +message InstallComponentRequest { + string bottle_name = 1; + string component_id = 2; // e.g., "dxvk", "dotnet48" + string version = 3; // Optional, uses 'latest' if empty +} + +message InstallProgress { + int32 percentage = 1; + string status_message = 2; + bool data_complete = 3; +} + +message ListComponentsRequest { + string filter_type = 1; // Optional: "runner", "dependency", "layer" +} + +message ListComponentsResponse { + repeated Component components = 1; +} + +message ComponentRequest { + string bottle_name = 1; + string component_id = 2; +} +message Component { + string id = 1; + string name = 2; + string version = 3; + string type = 4; +} + +// Runtime +message LaunchProgramRequest { + string bottle_name = 1; + string program_path = 2; // Path inside the bottle or mapped absolute path + repeated string arguments = 3; + string work_dir = 4; + map env_overrides = 5; + bool run_in_terminal = 6; +} + +message LaunchProgramResponse { + uint32 pid = 1; + bool success = 2; +} + +message TerminateProgramRequest { + string bottle_name = 1; + uint32 pid = 2; +} + +message ProcessList { + repeated ProcessInfo processes = 1; +} + +message ProcessInfo { + uint32 pid = 1; + string name = 2; + uint32 threads = 3; + // Memory, CPU could be added here +} + +// System +message HealthRequest {} message HealthResponse { bool ok = 1; + string version = 2; } message NotifyRequest { string message = 1; } - message NotifyResponse { bool success = 1; } diff --git a/proto/winebridge.proto b/proto/winebridge.proto index 6f6dc23..7ebfe91 100644 --- a/proto/winebridge.proto +++ b/proto/winebridge.proto @@ -3,19 +3,103 @@ syntax = "proto3"; package winebridge; service WineBridge { + // Basic Communication rpc Message (MessageRequest) returns (MessageResponse); + + // Process Management rpc RunningProcesses (RunningProcessesRequest) returns (RunningProcessesResponse); rpc CreateProcess (CreateProcessRequest) returns (CreateProcessResponse); rpc KillProcess (KillProcessRequest) returns (KillProcessResponse); + // Registry Management rpc CreateRegistryKey (CreateRegistryKeyRequest) returns (MessageResponse); rpc DeleteRegistryKey (DeleteRegistryKeyRequest) returns (MessageResponse); rpc GetRegistryKey (GetRegistryKeyRequest) returns (RegistryKey); rpc GetRegistryKeyValue (RegistryKeyRequest) returns (RegistryValue); rpc SetRegistryKeyValue (SetRegistryKeyValueRequest) returns (MessageResponse); rpc DeleteRegistryKeyValue (RegistryKeyRequest) returns (MessageResponse); + + // File System Access (In-Prefix) + rpc CreateDirectory (FileOperationRequest) returns (FileOperationResponse); + rpc DeleteFile (FileOperationRequest) returns (FileOperationResponse); + rpc CopyFile (CopyMoveRequest) returns (FileOperationResponse); + rpc MoveFile (CopyMoveRequest) returns (FileOperationResponse); + rpc Exists (FileOperationRequest) returns (ExistsResponse); + rpc ListDirectory (FileOperationRequest) returns (ListDirectoryResponse); + + // System / Lifecycle + rpc Shutdown (ShutdownRequest) returns (MessageResponse); // Terminates the Agent + rpc Wineboot (WinebootRequest) returns (MessageResponse); // Simulates wineboot + rpc GetDriveInfo (DriveInfoRequest) returns (DriveInfoResponse); +} + +// --- Messages --- + +// General +message MessageRequest { + string message = 1; +} + +message MessageResponse { + bool success = 1; + string error = 2; // Optional error detail +} + +// System +message ShutdownRequest {} + +message WinebootRequest { + bool shutdown = 1; // If true, simpler shutdown + bool kill = 2; // If true, force kill +} + +message DriveInfoRequest {} + +message DriveInfoResponse { + repeated Drive drives = 1; +} + +message Drive { + string letter = 1; // "C", "Z" + string label = 2; + uint64 total_space = 3; + uint64 free_space = 4; } +// Process +message Process { + uint32 pid = 1; + string name = 2; + uint32 threads = 3; +} + +message RunningProcessesRequest {} + +message RunningProcessesResponse { + repeated Process processes = 1; +} + +message CreateProcessRequest { + string command = 1; + repeated string args = 2; + string work_dir = 3; // Optional working directory + map env = 4; // Environment variables + bool run_elevated = 5; // Simulates "Run as Admin" behavior if possible +} + +message CreateProcessResponse { + uint32 pid = 1; +} + +message KillProcessRequest { + uint32 pid = 1; +} + +message KillProcessResponse { + bool success = 1; +} + +// Registry enum RegistryValueType { REG_NONE = 0; REG_BINARY = 1; @@ -68,39 +152,32 @@ message DeleteRegistryKeyRequest { string subkey = 2; } -message MessageRequest { - string message = 1; -} - -message MessageResponse { - bool success = 1; -} - -message Process { - uint32 pid = 1; - string name = 2; - uint32 threads = 3; +// File System +message FileOperationRequest { + string path = 1; } -message RunningProcessesRequest { +message CopyMoveRequest { + string source = 1; + string destination = 2; } -message RunningProcessesResponse { - repeated Process processes = 1; -} - -message CreateProcessRequest { - string command = 1; - repeated string args = 2; +message FileOperationResponse { + bool success = 1; + string error = 2; } -message CreateProcessResponse { - uint32 pid = 1; +message ExistsResponse { + bool exists = 1; + bool is_dir = 2; } -message KillProcessRequest { - uint32 pid = 1; +message ListDirectoryResponse { + repeated FileInfo files = 1; } -message KillProcessResponse { +message FileInfo { + string name = 1; + bool is_dir = 2; + uint64 size = 3; } diff --git a/src/bottle.rs b/src/bottle.rs new file mode 100644 index 0000000..62fb13d --- /dev/null +++ b/src/bottle.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum BottleType { + Gaming, + Software, + Custom, +} + +impl Default for BottleType { + fn default() -> Self { + Self::Custom + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct BottleConfig { + pub runner: Option, + pub dxvk_version: Option, + pub vkd3d_version: Option, + pub environment: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Bottle { + pub name: String, + pub path: PathBuf, + pub kind: BottleType, + pub config: BottleConfig, + #[serde(skip)] + pub active: bool, // Runtime state, not persisted +} + +impl Bottle { + pub fn new(name: String, path: impl Into, kind: BottleType) -> Self { + Self { + name, + path: path.into(), + kind, + config: BottleConfig::default(), + active: false, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 9af6fcd..1513ef4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,14 @@ mod error; pub mod runner; +pub mod bottle; +pub mod persistence; pub use error::Error; pub mod proto { - tonic::include_proto!("winebridge"); - tonic::include_proto!("bottles"); + pub mod bottles { + tonic::include_proto!("bottles"); + } + pub mod winebridge { + tonic::include_proto!("winebridge"); + } } diff --git a/src/persistence.rs b/src/persistence.rs new file mode 100644 index 0000000..10ea70b --- /dev/null +++ b/src/persistence.rs @@ -0,0 +1,41 @@ +use crate::bottle::Bottle; +use crate::Error; +use std::fs; +use std::path::PathBuf; + +pub struct Persistence { + base_path: PathBuf, +} + +impl Persistence { + pub fn new(base_path: impl Into) -> Self { + Self { + base_path: base_path.into(), + } + } + + fn index_file(&self) -> PathBuf { + self.base_path.join("bottles.json") + } + + pub fn load_bottles(&self) -> Result, Error> { + let path = self.index_file(); + if !path.exists() { + return Ok(Vec::new()); + } + + let content = fs::read_to_string(&path).map_err(Error::Io)?; + let bottles: Vec = serde_json::from_str(&content).map_err(|e| Error::Io(e.into()))?; + Ok(bottles) + } + + pub fn save_bottles(&self, bottles: &[Bottle]) -> Result<(), Error> { + if !self.base_path.exists() { + fs::create_dir_all(&self.base_path).map_err(Error::Io)?; + } + + let content = serde_json::to_string_pretty(bottles).map_err(|e| Error::Io(e.into()))?; + fs::write(self.index_file(), content).map_err(Error::Io)?; + Ok(()) + } +} diff --git a/src/runner/gptk.rs b/src/runner/gptk.rs index 8d690b6..6969821 100644 --- a/src/runner/gptk.rs +++ b/src/runner/gptk.rs @@ -88,7 +88,17 @@ impl Runner for GPTK { arch_output == "i386" || arch_output == "arm64" } - fn initialize(&self, _prefix: impl AsRef) -> Result<(), crate::Error> { + fn initialize(&self, _prefix: &Path) -> Result<(), crate::Error> { todo!("Initialize GPTK") } + + fn launch( + &self, + _executable: &Path, + _args: &[String], + _prefix: &Path, + _env: &std::collections::HashMap, + ) -> Result { + todo!("Launch GPTK") + } } diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 2e9b30c..82dbf38 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -210,5 +210,25 @@ pub trait Runner { /// /// * `prefix` - Path where the new prefix should be created. The directory will be /// created if it doesn't exist. - fn initialize(&self, prefix: impl AsRef) -> Result<(), Error>; + fn initialize(&self, prefix: &Path) -> Result<(), Error>; + + /// Launch a command inside the runner environment. + /// + /// # Arguments + /// + /// * `executable` - Path to the executable to run (inside the bottle). + /// * `args` - Arguments to pass to the executable. + /// * `prefix` - The Wine prefix path. + /// * `env` - Additional environment variables. + /// + /// # Returns + /// + /// A `std::process::Child` handle to the running process. + fn launch( + &self, + executable: &Path, + args: &[String], + prefix: &Path, + env: &std::collections::HashMap, + ) -> Result; } diff --git a/src/runner/proton.rs b/src/runner/proton.rs index ae6aebc..12df279 100644 --- a/src/runner/proton.rs +++ b/src/runner/proton.rs @@ -45,16 +45,26 @@ impl Runner for Proton { &mut self.info } - fn initialize(&self, prefix: impl AsRef) -> Result<(), crate::Error> { + fn initialize(&self, prefix: &Path) -> Result<(), crate::Error> { // FIXME: Launch winebridge to initialize the prefix Command::new(self.info().executable_path()) .arg("run") .arg("wineboot") - .env("WINEPREFIX", prefix.as_ref()) - .env("STEAM_COMPAT_DATA_PATH", prefix.as_ref()) + .env("WINEPREFIX", prefix) + .env("STEAM_COMPAT_DATA_PATH", prefix) .env("STEAM_COMPAT_CLIENT_INSTALL_PATH", "") .output()?; Ok(()) } + + fn launch( + &self, + executable: &Path, + args: &[String], + prefix: &Path, + env: &std::collections::HashMap, + ) -> Result { + todo!("Launch Proton") + } } diff --git a/src/runner/umu.rs b/src/runner/umu.rs index f1d4118..e0e3d4d 100644 --- a/src/runner/umu.rs +++ b/src/runner/umu.rs @@ -52,14 +52,24 @@ impl Runner for UMU { &mut self.info } - fn initialize(&self, prefix: impl AsRef) -> Result<(), crate::Error> { + fn initialize(&self, prefix: &Path) -> Result<(), crate::Error> { // FIXME: Launch winebridge to initialize the prefix let proton_path = self.proton.as_ref().unwrap().info().directory(); Command::new(self.info().executable_path()) .arg("wineboot") // This is wrong but it'll anyways initialize the prefix - .env("WINEPREFIX", prefix.as_ref()) + .env("WINEPREFIX", prefix) .env("PROTONPATH", proton_path) .output()?; Ok(()) } + + fn launch( + &self, + executable: &Path, + args: &[String], + prefix: &Path, + env: &std::collections::HashMap, + ) -> Result { + todo!("Launch UMU") + } } diff --git a/src/runner/wine.rs b/src/runner/wine.rs index 3d2ce0f..bbac3b0 100644 --- a/src/runner/wine.rs +++ b/src/runner/wine.rs @@ -60,14 +60,24 @@ impl Runner for Wine { &mut self.info } - fn initialize(&self, prefix: impl AsRef) -> Result<(), crate::Error> { + fn initialize(&self, prefix: &Path) -> Result<(), crate::Error> { // FIXME: Launch winebridge to initialize the prefix Command::new(self.info().executable_path()) .arg("wineboot") .arg("--init") - .env("WINEPREFIX", prefix.as_ref()) + .env("WINEPREFIX", prefix) .output()?; Ok(()) } + + fn launch( + &self, + executable: &Path, + args: &[String], + prefix: &Path, + env: &std::collections::HashMap, + ) -> Result { + todo!("Launch WINE") + } }