sistema-de-chamados/apps/desktop/src-tauri/src/rustdesk.rs

980 lines
31 KiB
Rust

#![cfg(target_os = "windows")]
use crate::RustdeskProvisioningResult;
use chrono::{Local, Utc};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use reqwest::blocking::Client;
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::env;
use std::ffi::OsStr;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;
use thiserror::Error;
use std::os::windows::process::CommandExt;
const RELEASES_API: &str = "https://api.github.com/repos/rustdesk/rustdesk/releases/latest";
const USER_AGENT: &str = "RavenDesktop/1.0";
const SERVER_HOST: &str = "rust.rever.com.br";
const SERVER_KEY: &str = "0mxocQKmK6GvTZQYKgjrG9tlNkKOqf81gKgqwAmnZuI=";
const DEFAULT_PASSWORD: &str = "FMQ9MA>e73r.FI<b*34Vmx_8P";
const SERVICE_NAME: &str = "RustDesk";
const CACHE_DIR_NAME: &str = "Rever\\RustDeskCache";
const LOCAL_SERVICE_CONFIG: &str = r"C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\RustDesk\\config";
const LOCAL_SYSTEM_CONFIG: &str = r"C:\\Windows\\System32\\config\\systemprofile\\AppData\\Roaming\\RustDesk\\config";
const SECURITY_VERIFICATION_VALUE: &str = "use-permanent-password";
const SECURITY_APPROVE_MODE_VALUE: &str = "password";
const CREATE_NO_WINDOW: u32 = 0x08000000;
static PROVISION_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
#[derive(Debug, Error)]
pub enum RustdeskError {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("Release asset não encontrado para Windows x86_64")]
AssetMissing,
#[error("Falha ao executar comando {command}: status {status:?}")]
CommandFailed { command: String, status: Option<i32> },
#[error("Falha ao detectar ID do RustDesk")]
MissingId,
}
#[derive(Debug, Deserialize)]
struct ReleaseAsset {
name: String,
browser_download_url: String,
}
#[derive(Debug, Deserialize)]
struct ReleaseResponse {
tag_name: String,
assets: Vec<ReleaseAsset>,
}
pub fn ensure_rustdesk(
config_string: Option<&str>,
password_override: Option<&str>,
machine_id: Option<&str>,
) -> Result<RustdeskProvisioningResult, RustdeskError> {
let _guard = PROVISION_MUTEX.lock();
log_event("Iniciando preparo do RustDesk");
if let Err(error) = ensure_service_profiles_writable_preflight() {
log_event(&format!(
"Aviso: não foi possível preparar ACL dos perfis do serviço ({error}). Continuando mesmo assim; o serviço pode não aplicar a senha."
));
}
let exe_path = detect_executable_path();
let (installed_version, freshly_installed) = ensure_installed(&exe_path)?;
log_event(if freshly_installed {
"RustDesk instalado a partir do instalador mais recente"
} else {
"RustDesk já instalado, usando binário existente"
});
if let Some(value) = config_string.and_then(|raw| {
let trimmed = raw.trim();
if trimmed.is_empty() { None } else { Some(trimmed) }
}) {
if let Err(error) = run_with_args(&exe_path, &["--config", value]) {
log_event(&format!("Falha ao aplicar configuração inline: {error}"));
} else {
log_event("Configuração aplicada via --config");
}
} else {
let config_path = write_config_files()?;
log_event(&format!(
"Arquivo de configuração atualizado em {}",
config_path.display()
));
if let Err(error) = apply_config(&exe_path, &config_path) {
log_event(&format!("Falha ao aplicar configuração via CLI: {error}"));
} else {
log_event("Configuração aplicada via CLI");
}
}
let password = password_override
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| DEFAULT_PASSWORD.to_string());
if let Err(error) = set_password(&exe_path, &password) {
log_event(&format!("Falha ao definir senha padrão: {error}"));
} else {
log_event("Senha padrão definida com sucesso");
if let Err(error) = set_verification_method(&exe_path) {
log_event(&format!("Falha ao ajustar verification-method via CLI: {error}"));
}
if let Err(error) = set_approve_mode(&exe_path) {
log_event(&format!("Falha ao ajustar approve-mode via CLI: {error}"));
}
match propagate_password_profile() {
Ok(true) => log_event("Perfil de senha propagado para ProgramData/LocalService"),
Ok(false) => match ensure_password_files(&password) {
Ok(_) => log_event("Senha persistida via fallback nos perfis do RustDesk"),
Err(error) => log_event(&format!("Falha ao persistir senha nos perfis: {error}")),
},
Err(error) => {
log_event(&format!("Falha ao copiar perfil de senha: {error}"));
match ensure_password_files(&password) {
Ok(_) => log_event("Senha persistida via fallback nos perfis do RustDesk"),
Err(inner) => log_event(&format!("Falha ao persistir senha nos perfis: {inner}")),
}
}
}
match replicate_password_artifacts() {
Ok(_) => log_event("Artefatos de senha replicados para o serviço do RustDesk"),
Err(error) => log_event(&format!("Falha ao replicar artefatos de senha: {error}")),
}
if let Err(error) = enforce_security_flags() {
log_event(&format!("Falha ao reforçar configuração de senha permanente: {error}"));
}
}
let custom_id = if let Some(value) = machine_id.and_then(|raw| {
let trimmed = raw.trim();
if trimmed.is_empty() { None } else { Some(trimmed) }
}) {
match set_custom_id(&exe_path, value) {
Ok(custom) => {
log_event(&format!("ID determinístico definido: {custom}"));
Some(custom)
}
Err(error) => {
log_event(&format!("Falha ao definir ID determinístico: {error}"));
None
}
}
} else {
None
};
if let Err(error) = ensure_service_running(&exe_path) {
log_event(&format!("Falha ao reiniciar serviço do RustDesk: {error}"));
} else {
log_event("Serviço RustDesk reiniciado/run ativo");
}
let reported_id = match query_id_with_retries(&exe_path, 5) {
Ok(value) => value,
Err(error) => {
log_event(&format!("Falha ao obter ID após múltiplas tentativas: {error}"));
match read_remote_id_from_profiles().or_else(|| custom_id.clone()) {
Some(value) => {
log_event(&format!("ID obtido via arquivos de perfil: {value}"));
value
}
None => return Err(error),
}
}
};
if let Some(expected) = custom_id.as_ref() {
if expected != &reported_id {
log_event(&format!(
"ID retornado difere do determinístico ({expected}) -> aplicando {reported_id}"
));
}
}
ensure_remote_id_files(&reported_id);
let version = query_version(&exe_path).ok().or(installed_version);
let result = RustdeskProvisioningResult {
id: reported_id.clone(),
password: password.clone(),
installed_version: version.clone(),
updated: freshly_installed,
last_provisioned_at: Utc::now().timestamp_millis(),
};
log_event(&format!("Provisionamento concluído. ID final: {reported_id}. Versão: {:?}", version));
Ok(result)
}
fn detect_executable_path() -> PathBuf {
let program_files = env::var("PROGRAMFILES").unwrap_or_else(|_| "C:/Program Files".to_string());
Path::new(&program_files).join("RustDesk").join("rustdesk.exe")
}
fn ensure_installed(exe_path: &Path) -> Result<(Option<String>, bool), RustdeskError> {
if exe_path.exists() {
return Ok((None, false));
}
let cache_root = PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string()))
.join(CACHE_DIR_NAME);
fs::create_dir_all(&cache_root)?;
let (installer_path, version_tag) = download_latest_installer(&cache_root)?;
run_installer(&installer_path)?;
thread::sleep(Duration::from_secs(20));
Ok((Some(version_tag), true))
}
fn download_latest_installer(cache_root: &Path) -> Result<(PathBuf, String), RustdeskError> {
let client = Client::builder()
.user_agent(USER_AGENT)
.timeout(Duration::from_secs(60))
.build()?;
let release: ReleaseResponse = client.get(RELEASES_API).send()?.error_for_status()?.json()?;
let asset = release
.assets
.iter()
.find(|a| a.name.ends_with("x86_64.exe"))
.ok_or(RustdeskError::AssetMissing)?;
let target_path = cache_root.join(&asset.name);
if target_path.exists() {
return Ok((target_path, release.tag_name));
}
let mut response = client.get(&asset.browser_download_url).send()?.error_for_status()?;
let mut output = File::create(&target_path)?;
response.copy_to(&mut output)?;
Ok((target_path, release.tag_name))
}
fn run_installer(installer_path: &Path) -> Result<(), RustdeskError> {
let status = hidden_command(installer_path)
.arg("--silent-install")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
if !status.success() {
return Err(RustdeskError::CommandFailed {
command: format!("{} --silent-install", installer_path.display()),
status: status.code(),
});
}
Ok(())
}
fn write_config_files() -> Result<PathBuf, RustdeskError> {
let config_contents = build_config_contents();
let main_path = program_data_config_dir().join("RustDesk2.toml");
write_file(&main_path, &config_contents)?;
log_event(&format!(
"Config principal gravada em {}",
main_path.display()
));
let service_profile = PathBuf::from(LOCAL_SERVICE_CONFIG).join("RustDesk2.toml");
let _ = ensure_service_profiles_writable_preflight();
if let Err(error) = write_file(&service_profile, &config_contents) {
log_event(&format!(
"Falha ao gravar config no perfil do serviço: {error}"
));
}
if let Some(appdata_path) = user_appdata_config_path("RustDesk2.toml") {
if let Err(error) = write_file(&appdata_path, &config_contents) {
log_event(&format!(
"Falha ao atualizar config no AppData do usuário: {error}"
));
}
}
Ok(main_path)
}
fn write_file(path: &Path, contents: &str) -> Result<(), io::Error> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
file.write_all(contents.as_bytes())
}
fn program_data_config_dir() -> PathBuf {
PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string()))
.join("RustDesk")
.join("config")
}
fn user_appdata_config_dir() -> Option<PathBuf> {
env::var("APPDATA")
.ok()
.map(|value| Path::new(&value).join("RustDesk").join("config"))
}
fn user_appdata_config_path(filename: &str) -> Option<PathBuf> {
user_appdata_config_dir().map(|dir| dir.join(filename))
}
fn build_config_contents() -> String {
format!(
r#"[options]
key = "{key}"
relay-server = "{host}"
custom-rendezvous-server = "{host}"
api-server = "https://{host}"
"#,
host = SERVER_HOST,
key = SERVER_KEY
)
}
fn apply_config(exe_path: &Path, config_path: &Path) -> Result<(), RustdeskError> {
let status = hidden_command(exe_path)
.arg("--import-config")
.arg(config_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
if !status.success() {
return Err(RustdeskError::CommandFailed {
command: format!("{} --import-config {}", exe_path.display(), config_path.display()),
status: status.code(),
});
}
Ok(())
}
fn set_password(exe_path: &Path, secret: &str) -> Result<(), RustdeskError> {
run_with_args(exe_path, &["--password", secret])
}
fn set_verification_method(exe_path: &Path) -> Result<(), RustdeskError> {
run_with_args(exe_path, &["--set-verification-method", "use-permanent-password"])
}
fn set_approve_mode(exe_path: &Path) -> Result<(), RustdeskError> {
run_with_args(exe_path, &["--set-approve-mode", "password"])
}
fn set_custom_id(exe_path: &Path, machine_id: &str) -> Result<String, RustdeskError> {
let custom_id = derive_numeric_id(machine_id);
run_with_args(exe_path, &["--set-id", &custom_id])?;
Ok(custom_id)
}
fn derive_numeric_id(machine_id: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(machine_id.as_bytes());
let hash = hasher.finalize();
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&hash[..8]);
let value = u64::from_le_bytes(bytes);
let num = (value % 900_000_000) + 100_000_000;
format!("{:09}", num)
}
fn ensure_service_running(exe_path: &Path) -> Result<(), RustdeskError> {
ensure_service_installed(exe_path)?;
fn start_sequence() -> Result<(), RustdeskError> {
let _ = run_sc(&["stop", SERVICE_NAME]);
thread::sleep(Duration::from_secs(2));
let _ = run_sc(&["config", SERVICE_NAME, &format!("start= {}", "auto")]);
run_sc(&["start", SERVICE_NAME])
}
match start_sequence() {
Ok(_) => Ok(()),
Err(RustdeskError::CommandFailed { command: _, status: Some(5), .. }) => {
log_event("SC retornou acesso negado; tentando ajustar ACL dos perfis do serviço...");
ensure_service_profiles_writable_preflight().map_err(|error| RustdeskError::CommandFailed {
command: format!("fix_acl ({error})"),
status: Some(5),
})?;
let _ = run_sc(&["stop", SERVICE_NAME]);
start_sequence().or_else(|_| Ok(()))
}
Err(error) => Err(error),
}
}
fn run_sc(args: &[&str]) -> Result<(), RustdeskError> {
let status = hidden_command("sc")
.args(args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
if !status.success() {
return Err(RustdeskError::CommandFailed {
command: format!("sc {}", args.join(" ")),
status: status.code(),
});
}
Ok(())
}
fn ensure_service_installed(exe_path: &Path) -> Result<(), RustdeskError> {
if run_sc(&["query", SERVICE_NAME]).is_ok() {
return Ok(());
}
log_event("Serviço RustDesk não encontrado; instalando via CLI");
run_with_args(exe_path, &["--install-service"])?;
Ok(())
}
fn query_id_with_retries(exe_path: &Path, attempts: usize) -> Result<String, RustdeskError> {
let mut last_error: Option<RustdeskError> = None;
for attempt in 0..attempts {
match query_id(exe_path) {
Ok(value) if !value.trim().is_empty() => return Ok(value),
Ok(_) => {
last_error = Some(RustdeskError::MissingId);
}
Err(error) => {
last_error = Some(error);
}
}
if attempt + 1 < attempts {
thread::sleep(Duration::from_millis(800));
}
}
Err(last_error.unwrap_or(RustdeskError::MissingId))
}
fn query_id(exe_path: &Path) -> Result<String, RustdeskError> {
let output = hidden_command(exe_path)
.arg("--get-id")
.output()?;
if !output.status.success() {
return Err(RustdeskError::CommandFailed {
command: format!("{} --get-id", exe_path.display()),
status: output.status.code(),
});
}
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
if stdout.is_empty() {
return Err(RustdeskError::MissingId);
}
Ok(stdout)
}
fn query_version(exe_path: &Path) -> Result<String, RustdeskError> {
let output = hidden_command(exe_path)
.arg("--version")
.output()?;
if !output.status.success() {
return Err(RustdeskError::CommandFailed {
command: format!("{} --version", exe_path.display()),
status: output.status.code(),
});
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn ensure_remote_id_files(id: &str) {
for dir in remote_id_directories() {
let path = dir.join("RustDesk_local.toml");
match write_remote_id_value(&path, id) {
Ok(_) => log_event(&format!(
"remote_id atualizado para {} em {}",
id,
path.display()
)),
Err(error) => log_event(&format!(
"Falha ao atualizar remote_id em {}: {error}",
path.display()
)),
}
}
}
fn remote_id_directories() -> Vec<PathBuf> {
let mut dirs = Vec::new();
dirs.push(program_data_config_dir());
for profile in service_profile_dirs() {
dirs.push(profile);
}
if let Some(appdir) = user_appdata_config_dir() {
dirs.push(appdir);
}
dirs
}
fn service_profile_dirs() -> Vec<PathBuf> {
vec![
PathBuf::from(LOCAL_SERVICE_CONFIG),
PathBuf::from(LOCAL_SYSTEM_CONFIG),
]
}
fn ensure_password_files(secret: &str) -> Result<(), String> {
let mut errors = Vec::new();
for dir in remote_id_directories() {
let password_path = dir.join("RustDesk.toml");
if let Err(error) = write_toml_kv(&password_path, "password", secret) {
errors.push(format!("{} -> {}", password_path.display(), error));
} else {
log_event(&format!(
"Senha escrita via fallback em {}",
password_path.display()
));
}
let local_path = dir.join("RustDesk_local.toml");
if let Err(error) = write_toml_kv(&local_path, "verification-method", SECURITY_VERIFICATION_VALUE) {
log_event(&format!(
"Falha ao ajustar verification-method em {}: {error}",
local_path.display()
));
} else {
log_event(&format!(
"verification-method atualizado para {} em {}",
SECURITY_VERIFICATION_VALUE,
local_path.display()
));
}
if let Err(error) = write_toml_kv(&local_path, "approve-mode", SECURITY_APPROVE_MODE_VALUE) {
log_event(&format!(
"Falha ao ajustar approve-mode em {}: {error}",
local_path.display()
));
} else {
log_event(&format!(
"approve-mode atualizado para {} em {}",
SECURITY_APPROVE_MODE_VALUE,
local_path.display()
));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors.join(" | "))
}
}
fn enforce_security_flags() -> Result<(), String> {
let mut errors = Vec::new();
for dir in remote_id_directories() {
let local_path = dir.join("RustDesk_local.toml");
if let Err(error) = write_toml_kv(&local_path, "verification-method", SECURITY_VERIFICATION_VALUE) {
errors.push(format!("{} -> {}", local_path.display(), error));
} else {
log_event(&format!(
"verification-method atualizado para {} em {}",
SECURITY_VERIFICATION_VALUE,
local_path.display()
));
}
if let Err(error) = write_toml_kv(&local_path, "approve-mode", SECURITY_APPROVE_MODE_VALUE) {
errors.push(format!("{} -> {}", local_path.display(), error));
} else {
log_event(&format!(
"approve-mode atualizado para {} em {}",
SECURITY_APPROVE_MODE_VALUE,
local_path.display()
));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors.join(" | "))
}
}
fn propagate_password_profile() -> io::Result<bool> {
let Some(src_dir) = user_appdata_config_dir() else {
log_event("AppData do usuário não disponível para copiar RustDesk.toml (propagação ignorada)");
return Ok(false);
};
let src_path = src_dir.join("RustDesk.toml");
if !src_path.exists() {
log_event(&format!(
"Arquivo {} não encontrado; usando fallback de escrita",
src_path.display()
));
return Ok(false);
}
log_event(&format!(
"Copiando {} para ProgramData/LocalService",
src_path.display()
));
let mut propagated = false;
for dest_root in [program_data_config_dir(), PathBuf::from(LOCAL_SERVICE_CONFIG)] {
let target_path = dest_root.join("RustDesk.toml");
copy_overwrite(&src_path, &target_path)?;
log_event(&format!(
"RustDesk.toml propagado para {}",
target_path.display()
));
propagated = true;
}
Ok(propagated)
}
fn replicate_password_artifacts() -> io::Result<()> {
let Some(src) = user_appdata_config_dir() else {
return Ok(());
};
let destinations = [program_data_config_dir(), PathBuf::from(LOCAL_SERVICE_CONFIG)];
let candidates = ["password", "passwd", "passwd.txt"];
for dest in destinations {
fs::create_dir_all(&dest)?;
for name in candidates {
let source_path = src.join(name);
if !source_path.exists() {
continue;
}
let metadata = match fs::metadata(&source_path) {
Ok(data) => data,
Err(_) => continue,
};
if !metadata.is_file() || metadata.len() == 0 {
continue;
}
let target_path = dest.join(name);
copy_overwrite(&source_path, &target_path)?;
log_event(&format!(
"Artefato de senha {name} replicado para {}",
target_path.display()
));
}
}
Ok(())
}
fn run_powershell_elevated(script: &str) -> Result<(), String> {
let temp_dir = env::temp_dir();
let payload = temp_dir.join("raven_payload.ps1");
fs::write(&payload, script).map_err(|error| format!("write payload: {error}"))?;
let launcher = temp_dir.join("raven_launcher.ps1");
let launcher_body = format!(
r#"
$ErrorActionPreference='Stop'
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = 'powershell.exe'
$psi.Arguments = '-NoProfile -ExecutionPolicy Bypass -File "{payload}"'
$psi.Verb = 'runas'
$psi.WindowStyle = 'Hidden'
$process = [System.Diagnostics.Process]::Start($psi)
$process.WaitForExit()
exit $process.ExitCode
"#,
payload = payload.display()
);
fs::write(&launcher, launcher_body).map_err(|error| format!("write launcher: {error}"))?;
let status = Command::new("powershell")
.creation_flags(CREATE_NO_WINDOW)
.args([
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
&launcher.to_string_lossy(),
])
.status()
.map_err(|error| format!("spawn ps: {error}"))?;
let _ = fs::remove_file(&launcher);
let _ = fs::remove_file(&payload);
if let Some(code) = status.code() {
if code == 0 || code == 1 {
return Ok(());
}
} else if status.success() {
return Ok(());
}
Err(format!("elevated ps exit {:?}", status.code()))
}
fn fix_profile_acl(target: &Path) -> Result<(), String> {
let target_str = target.display().to_string();
let transcript = env::temp_dir().join("raven_acl_ps.log");
let log_str = transcript.display().to_string();
let script = format!(
r#"
$ErrorActionPreference='Stop'
Start-Transcript -Path '{log}' -Force
try {{
if (-not (Test-Path '{target}')) {{ New-Item -ItemType Directory -Force -Path '{target}' | Out-Null }}
& takeown /F '{target}' /R /D Y
$takeCode = $LASTEXITCODE
& icacls '{target}' /grant '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-19:(OI)(CI)F' '*S-1-5-32-545:(OI)(CI)M' /T /C /Q
$icaCode = $LASTEXITCODE
if (($takeCode -eq 0) -and ($icaCode -in 0,1)) {{ exit 0 }}
if ($icaCode -ne 0) {{ exit $icaCode }}
exit $takeCode
}} catch {{
Write-Host ("exception: " + ($_.Exception.Message))
exit 1
}} finally {{
try {{ Stop-Transcript | Out-Null }} catch {{ }}
}}
"#,
target = target_str,
log = log_str
);
let result = run_powershell_elevated(&script);
if result.is_err() {
if let Ok(content) = fs::read_to_string(&transcript) {
log_event(&format!(
"ACL transcript para {}:\n{}",
target.display(), content
));
}
}
let _ = fs::remove_file(&transcript);
result
}
fn ensure_service_profiles_writable_preflight() -> Result<(), String> {
let mut success = false;
let mut last_error: Option<String> = None;
for dir in service_profile_dirs() {
if can_write_dir(&dir) {
success = true;
continue;
}
log_event(&format!(
"Tentando corrigir ACL via UAC (preflight) em {}...",
dir.display()
));
if let Err(error) = fix_profile_acl(&dir) {
last_error = Some(error);
continue;
}
if can_write_dir(&dir) {
log_event(&format!(
"ACL ajustada com sucesso em {}",
dir.display()
));
success = true;
} else {
last_error = Some(format!(
"continua sem permissão para {} mesmo após preflight",
dir.display()
));
}
}
if success {
Ok(())
} else {
Err(last_error.unwrap_or_else(|| "nenhum perfil de serviço acessível".into()))
}
}
fn can_write_dir(dir: &Path) -> bool {
if fs::create_dir_all(dir).is_err() {
return false;
}
let probe = dir.join(".raven_acl_probe");
match OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&probe)
{
Ok(mut file) => {
if file.write_all(b"ok").is_err() {
let _ = fs::remove_file(&probe);
return false;
}
let _ = fs::remove_file(&probe);
true
}
Err(_) => false,
}
}
fn write_remote_id_value(path: &Path, id: &str) -> io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let replacement = format!("remote_id = '{}'\n", id);
if let Ok(existing) = fs::read_to_string(path) {
let mut replaced = false;
let mut buffer = String::with_capacity(existing.len() + replacement.len());
for line in existing.lines() {
if line.trim_start().starts_with("remote_id") {
buffer.push_str(&replacement);
replaced = true;
} else {
buffer.push_str(line);
buffer.push('\n');
}
}
if !replaced {
buffer.push_str(&replacement);
}
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
file.write_all(buffer.as_bytes())
} else {
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
file.write_all(replacement.as_bytes())
}
}
fn write_toml_kv(path: &Path, key: &str, value: &str) -> io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
if path.is_dir() {
fs::remove_dir_all(path)?;
}
let sanitized = value.replace('\\', "\\\\").replace('"', "\\\"");
let replacement = format!("{key} = \"{sanitized}\"\n");
let existing = fs::read_to_string(path).unwrap_or_default();
let mut replaced = false;
let mut buffer = String::with_capacity(existing.len() + replacement.len());
for line in existing.lines() {
let trimmed = line.trim_start();
if trimmed.starts_with(&format!("{key} ")) || trimmed.starts_with(&format!("{key}=")) {
buffer.push_str(&replacement);
replaced = true;
} else {
buffer.push_str(line);
buffer.push('\n');
}
}
if !replaced {
buffer.push_str(&replacement);
}
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
file.write_all(buffer.as_bytes())
}
fn read_remote_id_from_profiles() -> Option<String> {
for dir in remote_id_directories() {
for candidate in [dir.join("RustDesk_local.toml"), dir.join("RustDesk.toml")] {
if let Some(id) = read_remote_id_file(&candidate) {
if !id.is_empty() {
return Some(id);
}
}
}
}
None
}
fn read_remote_id_file(path: &Path) -> Option<String> {
let content = fs::read_to_string(path).ok()?;
for line in content.lines() {
if let Some(value) = parse_assignment(line, "remote_id") {
return Some(value);
}
}
None
}
fn parse_assignment(line: &str, key: &str) -> Option<String> {
let trimmed = line.trim();
if !trimmed.starts_with(key) {
return None;
}
let (_, rhs) = trimmed.split_once('=')?;
let value = rhs.trim().trim_matches(|c| c == '\'' || c == '"');
if value.is_empty() {
None
} else {
Some(value.to_string())
}
}
fn run_with_args(exe_path: &Path, args: &[&str]) -> Result<(), RustdeskError> {
let status = hidden_command(exe_path)
.args(args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
if !status.success() {
return Err(RustdeskError::CommandFailed {
command: format!("{} {}", exe_path.display(), args.join(" ")),
status: status.code(),
});
}
Ok(())
}
fn hidden_command(program: impl AsRef<OsStr>) -> Command {
let mut cmd = Command::new(program);
cmd.creation_flags(CREATE_NO_WINDOW);
cmd
}
fn copy_overwrite(src: &Path, dst: &Path) -> io::Result<()> {
if let Some(parent) = dst.parent() {
fs::create_dir_all(parent)?;
}
if dst.is_dir() {
fs::remove_dir_all(dst)?;
} else if dst.exists() {
fs::remove_file(dst)?;
}
fs::copy(src, dst)?;
Ok(())
}
fn log_event(message: impl AsRef<str>) {
if let Some(dir) = logs_directory() {
if let Err(error) = append_log(dir, message.as_ref()) {
eprintln!("[rustdesk][log] Falha ao registrar log: {error}");
}
}
}
fn logs_directory() -> Option<PathBuf> {
let base = env::var("LOCALAPPDATA").ok()?;
Some(
Path::new(&base)
.join("br.com.esdrasrenan.sistemadechamados")
.join("logs"),
)
}
fn append_log(dir: PathBuf, message: &str) -> io::Result<()> {
fs::create_dir_all(&dir)?;
let log_path = dir.join("rustdesk.log");
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(log_path)?;
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
writeln!(file, "[{timestamp}] {message}")?;
Ok(())
}