zerolaunch-platform-windows 0.2.2

Windows platform implementations of zerolaunch-plugin-api traits.
use crate::utils::get_u16_vec;
use async_trait::async_trait;
use std::os::windows::process::CommandExt;
use std::path::Path;
use tracing::{debug, warn};
use windows::core::PCWSTR;
use windows::Win32::Foundation::{GetLastError, ERROR_CANCELLED};
use windows::Win32::UI::Shell::{ShellExecuteExW, ShellExecuteW, SHELLEXECUTEINFOW};
use windows::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
use zerolaunch_plugin_api::host::{HostApiError, OpenTarget};
use zerolaunch_plugin_api::services::shell::{CommandRunMode, ShellExecutor};

/// 使用 ShellExecuteW 以系统默认方式打开指定路径
fn shell_execute_open(path: &str) -> Result<(), HostApiError> {
    let wide_path = get_u16_vec(path);

    unsafe {
        let result = ShellExecuteW(
            None,
            PCWSTR::from_raw(std::ptr::null()),
            PCWSTR::from_raw(wide_path.as_ptr()),
            PCWSTR::from_raw(std::ptr::null()),
            PCWSTR::from_raw(std::ptr::null()),
            SW_SHOWNORMAL,
        );

        if result.0 as isize <= 32 {
            let error = GetLastError();
            Err(HostApiError::ShellOperationFailed {
                target: path.to_string(),
                reason: format!("ShellExecuteW 失败,错误码: {}", error.0),
            })
        } else {
            Ok(())
        }
    }
}

/// 后台静默执行命令:`cmd /D /S /C` + `CREATE_NO_WINDOW | DETACHED_PROCESS`,不弹出控制台窗口。
/// 调用方保证命令非空。
fn run_command_background(command: &str) -> Result<(), HostApiError> {
    const CREATE_NO_WINDOW: u32 = 0x08000000;
    const DETACHED_PROCESS: u32 = 0x00000008;

    let result = std::process::Command::new("cmd")
        .args(["/D", "/S", "/C"])
        .raw_arg(command)
        .creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS)
        .spawn();

    match result {
        Ok(_) => {
            debug!("命令后台启动成功: {}", command);
            Ok(())
        }
        Err(e) => {
            let msg = format!("命令启动失败: {:?}", e);
            warn!("{}", msg);
            Err(HostApiError::ShellOperationFailed {
                target: command.to_string(),
                reason: msg,
            })
        }
    }
}

/// 在可见的控制台窗口中执行命令:经 ShellExecuteW 打开 cmd.exe(与「运行」对话框同一路径)。
/// 新控制台自行持有标准句柄,不继承宿主进程的标准句柄 —— 宿主若从管道/重定向环境启动,
/// `cmd /K` 会因 stdin 立即 EOF 而在命令结束后立刻退出。命令原样拼进 cmd 参数串,不做转义。
/// keep_open = true 用 `/K`(命令结束后保留窗口),false 用 `/C`(结束后自动关闭)。调用方保证命令非空。
fn run_command_in_console(command: &str, keep_open: bool) -> Result<(), HostApiError> {
    let parameters = format!("/D /S {} {}", if keep_open { "/K" } else { "/C" }, command);
    let verb = get_u16_vec("open");
    let file = get_u16_vec("cmd.exe");
    let params = get_u16_vec(&parameters);

    unsafe {
        let result = ShellExecuteW(
            None,
            PCWSTR::from_raw(verb.as_ptr()),
            PCWSTR::from_raw(file.as_ptr()),
            PCWSTR::from_raw(params.as_ptr()),
            PCWSTR::from_raw(std::ptr::null()),
            SW_SHOWNORMAL,
        );

        if result.0 as isize <= 32 {
            let error = GetLastError();
            let msg = format!("ShellExecuteW 失败,错误码: {}", error.0);
            warn!("{}", msg);
            Err(HostApiError::ShellOperationFailed {
                target: command.to_string(),
                reason: msg,
            })
        } else {
            debug!("命令已在控制台窗口启动: {}", command);
            Ok(())
        }
    }
}

/// Windows 平台 Shell 执行器实现
pub struct WindowsShellExecutor;

impl Default for WindowsShellExecutor {
    fn default() -> Self {
        Self::new()
    }
}

impl WindowsShellExecutor {
    pub fn new() -> Self {
        Self
    }

    /// 通过 explorer.exe 代理启动文件,模拟用户在资源管理器中双击的效果。
    /// 优势:进程分离,被启动程序不会成为当前进程的子进程;
    ///       同时 explorer 会正确处理各种文件类型(.exe, .lnk, .url, steam:// 协议等)。
    /// 失败时回退到 ShellExecuteW。
    /// 参数:path - 要打开的文件路径。
    /// 返回:成功返回 Ok(()),失败返回 HostApiError。
    async fn shell_open_file(&self, path: &str) -> Result<(), HostApiError> {
        const CREATE_NO_WINDOW: u32 = 0x08000000;
        const DETACHED_PROCESS: u32 = 0x00000008;

        let result = std::process::Command::new("explorer")
            .arg(path)
            .creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS)
            .spawn();

        match result {
            Ok(_) => {
                debug!("已请求 Explorer 启动: {}", path);
                Ok(())
            }
            Err(e) => {
                warn!("Explorer 启动失败: {:?}, 回退到 ShellExecuteW...", e);
                shell_execute_open(path)
            }
        }
    }
}

#[async_trait]
impl ShellExecutor for WindowsShellExecutor {
    /// 使用系统默认方式打开目标。
    /// 对 File 类型:优先通过 explorer.exe 代理启动(模拟双击,进程分离),失败时回退到 ShellExecuteW。
    /// 对 Url/Folder 类型:直接使用 ShellExecuteW。
    async fn shell_open(&self, target: &OpenTarget) -> Result<(), HostApiError> {
        match target {
            OpenTarget::File(path) => self.shell_open_file(path).await,
            OpenTarget::Url(url) => shell_execute_open(url),
            OpenTarget::Folder(path) => shell_execute_open(path),
        }
    }

    /// 打开目标文件所在的文件夹。
    /// 如果目标是目录则直接打开,否则打开其父目录。
    async fn shell_open_folder(&self, path: &str) -> Result<(), HostApiError> {
        let target_path = Path::new(path);

        let folder_to_open = if target_path.is_dir() {
            target_path
        } else {
            target_path.parent().unwrap_or_else(|| {
                warn!(
                    "Target path has no parent, fallback to original path: {}",
                    target_path.display()
                );
                target_path
            })
        };

        if !folder_to_open.exists() {
            return Err(HostApiError::ShellOperationFailed {
                target: path.to_string(),
                reason: format!("目标文件夹不存在: {}", folder_to_open.display()),
            });
        }

        let folder_str = folder_to_open.to_string_lossy();
        shell_execute_open(&folder_str)
    }

    /// 以管理员权限启动程序。
    /// 使用 ShellExecuteExW 的 "runas" verb 触发 UAC 提升对话框。
    async fn shell_execute_elevation(&self, path: &str) -> Result<(), HostApiError> {
        let program_path = Path::new(path);
        let working_directory = program_path.parent().unwrap_or_else(|| Path::new("."));
        let program_path_wide = get_u16_vec(program_path);
        let working_directory_wide = get_u16_vec(working_directory);

        unsafe {
            let lp_verb = get_u16_vec("runas");
            let mut sei: SHELLEXECUTEINFOW = std::mem::zeroed();
            sei.cbSize = std::mem::size_of::<SHELLEXECUTEINFOW>() as u32;
            sei.lpVerb = PCWSTR::from_raw(lp_verb.as_ptr());
            sei.lpFile = PCWSTR::from_raw(program_path_wide.as_ptr());
            sei.lpDirectory = PCWSTR::from_raw(working_directory_wide.as_ptr());
            sei.nShow = SW_SHOWNORMAL.0;

            if ShellExecuteExW(&mut sei).is_err() {
                let error = GetLastError();
                if error == ERROR_CANCELLED {
                    warn!("用户取消了 UAC 提升请求");
                    return Err(HostApiError::ShellOperationFailed {
                        target: path.to_string(),
                        reason: "用户取消了 UAC 提升请求".to_string(),
                    });
                } else {
                    return Err(HostApiError::ShellOperationFailed {
                        target: path.to_string(),
                        reason: format!("管理员启动失败,错误码: {}", error.to_hresult()),
                    });
                }
            }
        }

        Ok(())
    }

    /// 执行命令字符串。
    /// 按 `mode` 选择呈现方式:`Background` 后台静默(无窗口),`Console` 在可见控制台窗口中执行。
    /// 输入验证:空命令或纯空白命令返回错误。
    async fn shell_execute_command(
        &self,
        command: &str,
        mode: CommandRunMode,
    ) -> Result<(), HostApiError> {
        let command = command.trim();
        if command.is_empty() {
            return Err(HostApiError::ShellOperationFailed {
                target: String::new(),
                reason: "命令为空".to_string(),
            });
        }

        match mode {
            CommandRunMode::Background => run_command_background(command),
            CommandRunMode::Console { keep_open } => run_command_in_console(command, keep_open),
        }
    }
}