From 0ed28d7c2e2750952f6b57be6a4466055d301746 Mon Sep 17 00:00:00 2001 From: Mirko Brombin Date: Tue, 30 Dec 2025 20:03:13 +0100 Subject: [PATCH 1/5] feat: gRPC protocol --- proto/bottles.proto | 170 ++++++++++++++++++++++++++++++++++++++++- proto/winebridge.proto | 127 ++++++++++++++++++++++++------ src/bottle.rs | 46 +++++++++++ src/lib.rs | 10 ++- src/persistence.rs | 41 ++++++++++ 5 files changed, 364 insertions(+), 30 deletions(-) create mode 100644 src/bottle.rs create mode 100644 src/persistence.rs 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..c7aaafd --- /dev/null +++ b/src/persistence.rs @@ -0,0 +1,41 @@ +use crate::bottle::Bottle; +use crate::Error; +use std::fs; +use std::path::{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(()) + } +} From d7da46dd3f6caa306c0c249311ed3e626ea35b2c Mon Sep 17 00:00:00 2001 From: Mirko Brombin Date: Tue, 30 Dec 2025 20:24:17 +0100 Subject: [PATCH 2/5] cleanup --- src/persistence.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/persistence.rs b/src/persistence.rs index c7aaafd..10ea70b 100644 --- a/src/persistence.rs +++ b/src/persistence.rs @@ -1,7 +1,7 @@ use crate::bottle::Bottle; use crate::Error; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; pub struct Persistence { base_path: PathBuf, From f8b4874b1f1ae66bb5b490a717b3fb7ecb12c86d Mon Sep 17 00:00:00 2001 From: Mirko Brombin Date: Tue, 6 Jan 2026 22:13:33 +0100 Subject: [PATCH 3/5] feat: implement initialzie/launch methods placeholders to satisfy the gRPC logics --- src/runner/gptk.rs | 12 +++++++++++- src/runner/mod.rs | 22 +++++++++++++++++++++- src/runner/proton.rs | 16 +++++++++++++--- src/runner/umu.rs | 14 ++++++++++++-- src/runner/wine.rs | 14 ++++++++++++-- 5 files changed, 69 insertions(+), 9 deletions(-) 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") + } } From a0c387537404aedbaff61fb7c9858e1c9c172e42 Mon Sep 17 00:00:00 2001 From: Mirko Brombin Date: Wed, 8 Apr 2026 12:09:45 +0200 Subject: [PATCH 4/5] fix: proto files --- proto/bottles.proto | 20 +++++++++++--------- proto/winebridge.proto | 11 ++++++++--- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/proto/bottles.proto b/proto/bottles.proto index 8f63f86..1474c21 100644 --- a/proto/bottles.proto +++ b/proto/bottles.proto @@ -41,6 +41,14 @@ service System { rpc Notify (NotifyRequest) returns (NotifyResponse); } +// --- Enums --- + +enum BottleType { + Custom = 0; + Gaming = 1; + Software = 2; +} + // --- Messages --- message ResultResponse { @@ -51,7 +59,7 @@ message ResultResponse { // Requests message CreateBottleRequest { string name = 1; - string type = 2; // e.g., "Gaming", "Software", "Custom" + BottleType type = 2; string runner = 3; // Optional implementation/runner override } @@ -77,20 +85,14 @@ message BottleRequest { message Bottle { string name = 1; string path = 2; - string type = 3; + BottleType 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 + map settings = 2; } message EnvironmentVariables { diff --git a/proto/winebridge.proto b/proto/winebridge.proto index 7ebfe91..1b8a3fb 100644 --- a/proto/winebridge.proto +++ b/proto/winebridge.proto @@ -42,15 +42,20 @@ message MessageRequest { message MessageResponse { bool success = 1; - string error = 2; // Optional error detail + optional string error = 2; } // System message ShutdownRequest {} +enum WinebootMode { + NORMAL = 0; + SHUTDOWN = 1; + KILL = 2; +} + message WinebootRequest { - bool shutdown = 1; // If true, simpler shutdown - bool kill = 2; // If true, force kill + WinebootMode mode = 1; } message DriveInfoRequest {} From 9c50218fd76cdd445b05bd7d7614044817fb0c4d Mon Sep 17 00:00:00 2001 From: Mirko Brombin Date: Wed, 8 Apr 2026 14:25:00 +0200 Subject: [PATCH 5/5] feat: add registry and filesystem diffing tools - Add regdiff-rs dependency (git) for Wine .reg file diffing - Add walkdir dependency for filesystem traversal - Implement RegistryDiff::diff(before, after, hive) wrapping regdiff-rs - Implement FilesystemDiff::snapshot(root) and FilesystemDiff::diff(before, after) - Export diff module from bottles-core - Update winebridge.proto: WinebootMode enum, optional error, service management RPCs, DLL override RPCs --- Cargo.toml | 4 ++ proto/winebridge.proto | 99 ++++++++++++++++++++++++++++++++++++++- src/diff/filesystem.rs | 104 +++++++++++++++++++++++++++++++++++++++++ src/diff/mod.rs | 2 + src/diff/registry.rs | 19 ++++++++ src/lib.rs | 1 + 6 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 src/diff/filesystem.rs create mode 100644 src/diff/mod.rs create mode 100644 src/diff/registry.rs diff --git a/Cargo.toml b/Cargo.toml index 93a2afd..6868fd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,10 @@ tonic.workspace = true tokio.workspace = true prost.workspace = true tonic-prost = "*" +walkdir = "2" + +[dependencies.regdiff-rs] +git = "https://github.com/bottlesdevs/regdiff-rs" [build-dependencies] tonic-prost-build = "0.14" diff --git a/proto/winebridge.proto b/proto/winebridge.proto index 1b8a3fb..9838d93 100644 --- a/proto/winebridge.proto +++ b/proto/winebridge.proto @@ -27,9 +27,23 @@ service WineBridge { rpc Exists (FileOperationRequest) returns (ExistsResponse); rpc ListDirectory (FileOperationRequest) returns (ListDirectoryResponse); + // Service Management + rpc ListServices (ListServicesRequest) returns (ListServicesResponse); + rpc GetServiceStatus (ServiceRequest) returns (ServiceStatusResponse); + rpc StartService (ServiceRequest) returns (MessageResponse); + rpc StopService (ServiceRequest) returns (MessageResponse); + rpc CreateService (CreateServiceRequest) returns (MessageResponse); + rpc DeleteService (ServiceRequest) returns (MessageResponse); + + // DLL Overrides + rpc ListDllOverrides (ListDllOverridesRequest) returns (ListDllOverridesResponse); + rpc GetDllOverride (DllOverrideRequest) returns (DllOverrideResponse); + rpc SetDllOverride (SetDllOverrideRequest) returns (MessageResponse); + rpc DeleteDllOverride (DllOverrideRequest) returns (MessageResponse); + // System / Lifecycle rpc Shutdown (ShutdownRequest) returns (MessageResponse); // Terminates the Agent - rpc Wineboot (WinebootRequest) returns (MessageResponse); // Simulates wineboot + rpc Wineboot (WinebootRequest) returns (MessageResponse); rpc GetDriveInfo (DriveInfoRequest) returns (DriveInfoResponse); } @@ -186,3 +200,86 @@ message FileInfo { bool is_dir = 2; uint64 size = 3; } + +// Service Management +enum ServiceState { + SERVICE_STOPPED = 0; + SERVICE_START_PENDING = 1; + SERVICE_STOP_PENDING = 2; + SERVICE_RUNNING = 3; + SERVICE_CONTINUE_PENDING = 4; + SERVICE_PAUSE_PENDING = 5; + SERVICE_PAUSED = 6; +} + +enum ServiceStartType { + SERVICE_BOOT_START = 0; + SERVICE_SYSTEM_START = 1; + SERVICE_AUTO_START = 2; + SERVICE_DEMAND_START = 3; + SERVICE_DISABLED = 4; +} + +message ServiceInfo { + string name = 1; + string display_name = 2; + ServiceState state = 3; + ServiceStartType start_type = 4; +} + +message ListServicesRequest {} + +message ListServicesResponse { + repeated ServiceInfo services = 1; +} + +message ServiceRequest { + string name = 1; +} + +message ServiceStatusResponse { + string name = 1; + ServiceState state = 2; +} + +message CreateServiceRequest { + string name = 1; + string display_name = 2; + string binary_path = 3; + ServiceStartType start_type = 4; +} + +// DLL Overrides +enum DllOverrideMode { + NATIVE_BUILTIN = 0; + BUILTIN_NATIVE = 1; + NATIVE = 2; + BUILTIN = 3; + DISABLED = 4; +} + +message DllOverride { + string dll = 1; + DllOverrideMode mode = 2; +} + +message ListDllOverridesRequest {} + +message ListDllOverridesResponse { + repeated DllOverride overrides = 1; +} + +message DllOverrideRequest { + string dll = 1; +} + +message DllOverrideResponse { + string dll = 1; + DllOverrideMode mode = 2; +} + +message SetDllOverrideRequest { + string dll = 1; + DllOverrideMode mode = 2; +} + diff --git a/src/diff/filesystem.rs b/src/diff/filesystem.rs new file mode 100644 index 0000000..5036348 --- /dev/null +++ b/src/diff/filesystem.rs @@ -0,0 +1,104 @@ +use std::collections::HashMap; +use std::path::Path; +use std::time::UNIX_EPOCH; +use walkdir::WalkDir; + +#[derive(Debug, Clone)] +pub struct FileEntry { + pub path: String, + pub is_dir: bool, + pub size: u64, + pub modified_secs: u64, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum DiffOp { + Added, + Removed, + Modified, +} + +#[derive(Debug, Clone)] +pub struct FileDiff { + pub path: String, + pub operation: DiffOp, +} + +pub struct FilesystemDiff; + +impl FilesystemDiff { + /// Walks a directory tree and returns metadata for every entry. + pub fn snapshot(root: &Path) -> Vec { + WalkDir::new(root) + .into_iter() + .filter_map(|e| e.ok()) + .filter_map(|entry| { + let meta = entry.metadata().ok()?; + let relative = entry + .path() + .strip_prefix(root) + .unwrap_or(entry.path()) + .to_string_lossy() + .replace('\\', "/"); + + if relative.is_empty() { + return None; + } + + let modified_secs = meta + .modified() + .ok() + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) + .map(|d| d.as_secs()) + .unwrap_or(0); + + Some(FileEntry { + path: relative, + is_dir: meta.is_dir(), + size: meta.len(), + modified_secs, + }) + }) + .collect() + } + + /// Computes the diff between two filesystem snapshots. + pub fn diff(before: &[FileEntry], after: &[FileEntry]) -> Vec { + let before_map: HashMap<&str, &FileEntry> = + before.iter().map(|e| (e.path.as_str(), e)).collect(); + let after_map: HashMap<&str, &FileEntry> = + after.iter().map(|e| (e.path.as_str(), e)).collect(); + + let mut changes = Vec::new(); + + for (path, b) in &before_map { + match after_map.get(path) { + None => changes.push(FileDiff { + path: path.to_string(), + operation: DiffOp::Removed, + }), + Some(a) + if !b.is_dir + && (a.size != b.size || a.modified_secs != b.modified_secs) => + { + changes.push(FileDiff { + path: path.to_string(), + operation: DiffOp::Modified, + }) + } + _ => {} + } + } + + for path in after_map.keys() { + if !before_map.contains_key(path) { + changes.push(FileDiff { + path: path.to_string(), + operation: DiffOp::Added, + }); + } + } + + changes + } +} diff --git a/src/diff/mod.rs b/src/diff/mod.rs new file mode 100644 index 0000000..3cae3e2 --- /dev/null +++ b/src/diff/mod.rs @@ -0,0 +1,2 @@ +pub mod registry; +pub mod filesystem; diff --git a/src/diff/registry.rs b/src/diff/registry.rs new file mode 100644 index 0000000..9c0c403 --- /dev/null +++ b/src/diff/registry.rs @@ -0,0 +1,19 @@ +use regdiff_rs::prelude::{Diff, Hive, Registry}; +use std::path::Path; + +pub use regdiff_rs::prelude::Hive as RegistryHive; + +pub struct RegistryDiff; + +impl RegistryDiff { + /// Computes a .reg patch between two Wine registry snapshots. + pub fn diff(before: &Path, after: &Path, hive: Hive) -> Result { + let before_reg = Registry::try_from(before, hive) + .map_err(|e| format!("Failed to load before snapshot: {}", e))?; + let after_reg = Registry::try_from(after, hive) + .map_err(|e| format!("Failed to load after snapshot: {}", e))?; + + let patch = Registry::diff(&before_reg, &after_reg); + Ok(patch.serialize()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 1513ef4..006b5cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ mod error; pub mod runner; pub mod bottle; pub mod persistence; +pub mod diff; pub use error::Error; pub mod proto {