#![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> = 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 }, #[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, } pub fn ensure_rustdesk( config_string: Option<&str>, password_override: Option<&str>, machine_id: Option<&str>, ) -> Result { 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}")), } } 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() { 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, 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 { 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_localservice_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 { env::var("APPDATA") .ok() .map(|value| Path::new(&value).join("RustDesk").join("config")) } fn user_appdata_config_path(filename: &str) -> Option { 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 { 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() -> Result<(), RustdeskError> { 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), })?; start_sequence() } 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 query_id_with_retries(exe_path: &Path, attempts: usize) -> Result { let mut last_error: Option = 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 { 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 { 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 { 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 { 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", "use-both-passwords") { log_event(&format!( "Falha ao ajustar verification-method em {}: {error}", local_path.display() )); } else { log_event(&format!( "verification-method atualizado para use-both-passwords em {}", local_path.display() )); } } if errors.is_empty() { Ok(()) } else { Err(errors.join(" | ")) } } fn propagate_password_profile() -> io::Result { 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 = 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 { 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 { 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 { 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) -> 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) { 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 { 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(()) }