185 lines
5.0 KiB
Rust
185 lines
5.0 KiB
Rust
use crate::models::ComponentId;
|
||
use serde::{Deserialize, Serialize};
|
||
use serde_json::{json, Value};
|
||
use std::path::{Path, PathBuf};
|
||
|
||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
pub enum HelperAction {
|
||
#[serde(rename = "install-control-app")]
|
||
InstallControlApp,
|
||
#[serde(rename = "install-proxyfier")]
|
||
InstallProxyfier,
|
||
#[serde(rename = "install-singbox")]
|
||
InstallSingbox,
|
||
#[serde(rename = "proxyfier.apply")]
|
||
ProxyfierApply,
|
||
#[serde(rename = "service.status")]
|
||
ServiceStatus,
|
||
#[serde(rename = "service.start")]
|
||
ServiceStart,
|
||
#[serde(rename = "service.stop")]
|
||
ServiceStop,
|
||
#[serde(rename = "service.restart")]
|
||
ServiceRestart,
|
||
}
|
||
|
||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct HelperRequest {
|
||
pub action: HelperAction,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub component: Option<ComponentId>,
|
||
#[serde(default)]
|
||
pub payload: Value,
|
||
}
|
||
|
||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct HelperResponse {
|
||
pub success: bool,
|
||
pub action: HelperAction,
|
||
pub changed: bool,
|
||
pub message: String,
|
||
#[serde(default)]
|
||
pub details: Value,
|
||
}
|
||
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct HelperCommandSpec {
|
||
pub program: PathBuf,
|
||
pub args: Vec<String>,
|
||
pub stdin: String,
|
||
pub requires_elevation: bool,
|
||
}
|
||
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct HelperCommandOutput {
|
||
pub status_code: i32,
|
||
pub stdout: String,
|
||
pub stderr: String,
|
||
}
|
||
|
||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct HelperError {
|
||
pub code: String,
|
||
pub message: String,
|
||
}
|
||
|
||
impl HelperError {
|
||
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
|
||
Self {
|
||
code: code.into(),
|
||
message: message.into(),
|
||
}
|
||
}
|
||
}
|
||
|
||
pub trait HelperCommandRunner {
|
||
fn run(&self, spec: &HelperCommandSpec) -> Result<HelperCommandOutput, HelperError>;
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct StructuredHelper<R> {
|
||
helper_program: PathBuf,
|
||
runner: R,
|
||
}
|
||
|
||
impl<R> StructuredHelper<R>
|
||
where
|
||
R: HelperCommandRunner,
|
||
{
|
||
pub fn new(helper_program: impl Into<PathBuf>, runner: R) -> Self {
|
||
Self {
|
||
helper_program: helper_program.into(),
|
||
runner,
|
||
}
|
||
}
|
||
|
||
pub fn runner(&self) -> &R {
|
||
&self.runner
|
||
}
|
||
|
||
pub fn execute(&self, request: &HelperRequest) -> Result<HelperResponse, HelperError> {
|
||
let stdin = serde_json::to_string(request)
|
||
.map_err(|error| HelperError::new("helper_request_encode", error.to_string()))?;
|
||
let spec = HelperCommandSpec {
|
||
program: self.helper_program.clone(),
|
||
args: vec!["--json".to_string()],
|
||
stdin,
|
||
requires_elevation: helper_action_requires_elevation(&request.action),
|
||
};
|
||
let output = self.runner.run(&spec)?;
|
||
|
||
if output.status_code != 0 {
|
||
return Err(HelperError::new(
|
||
"helper_exit",
|
||
format!(
|
||
"Помощник завершился с кодом {}: {}",
|
||
output.status_code, output.stderr
|
||
),
|
||
));
|
||
}
|
||
|
||
parse_helper_response(&output.stdout)
|
||
}
|
||
}
|
||
|
||
pub fn parse_helper_response(stdout: &str) -> Result<HelperResponse, HelperError> {
|
||
serde_json::from_str(stdout).map_err(|error| {
|
||
HelperError::new(
|
||
"helper_response_decode",
|
||
format!("Помощник вернул не JSON или некорректный JSON: {error}"),
|
||
)
|
||
})
|
||
}
|
||
|
||
pub fn install_request(component: ComponentId) -> HelperRequest {
|
||
let action = match component {
|
||
ComponentId::ControlApp => HelperAction::InstallControlApp,
|
||
ComponentId::Proxyfier => HelperAction::InstallProxyfier,
|
||
ComponentId::Singbox => HelperAction::InstallSingbox,
|
||
};
|
||
|
||
HelperRequest {
|
||
action,
|
||
component: Some(component),
|
||
payload: json!({}),
|
||
}
|
||
}
|
||
|
||
pub fn service_request(component: ComponentId, action: HelperAction) -> HelperRequest {
|
||
HelperRequest {
|
||
action,
|
||
component: Some(component),
|
||
payload: json!({}),
|
||
}
|
||
}
|
||
|
||
pub fn proxifyre_apply_request(
|
||
config_path: impl AsRef<Path>,
|
||
service_name: impl Into<String>,
|
||
) -> HelperRequest {
|
||
HelperRequest {
|
||
action: HelperAction::ProxyfierApply,
|
||
component: Some(ComponentId::Proxyfier),
|
||
payload: json!({
|
||
"configPath": config_path.as_ref().display().to_string(),
|
||
"serviceName": service_name.into(),
|
||
}),
|
||
}
|
||
}
|
||
|
||
pub fn helper_action_requires_elevation(action: &HelperAction) -> bool {
|
||
matches!(
|
||
action,
|
||
HelperAction::InstallControlApp
|
||
| HelperAction::InstallProxyfier
|
||
| HelperAction::InstallSingbox
|
||
| HelperAction::ProxyfierApply
|
||
| HelperAction::ServiceStart
|
||
| HelperAction::ServiceStop
|
||
| HelperAction::ServiceRestart
|
||
)
|
||
}
|