Skip to content

Commit 0700449

Browse files
committed
feat: Improves Windows update robustness and error handling
Enhances process termination logic for Windows, handling output encoding and treating missing processes as successful. Refactors update script to use per-run temp directories, CRLF line endings, and adds backup/retry mechanisms for executable replacement. Launches update scripts in independent consoles to prevent job object interference. Clarifies user guidance on errors and avoids killing the current updater process during update. Updates version to 1.4.1.
1 parent 029e1a5 commit 0700449

2 files changed

Lines changed: 183 additions & 47 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "llbot-cli"
3-
version = "1.4.0"
3+
version = "1.4.1"
44
edition = "2021"
55
description = "LLBot CLI launcher"
66

src/updater.rs

Lines changed: 182 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,77 @@ pub fn check_running_processes() -> Vec<(String, u32)> {
277277
}
278278

279279
#[cfg(target_os = "windows")]
280-
pub fn kill_process(pid: u32) -> bool {
281-
Command::new("taskkill")
282-
.args(["/F", "/PID", &pid.to_string()])
280+
pub fn kill_process(pid: u32) -> Result<(), String> {
281+
fn decode_taskkill_output(bytes: &[u8]) -> String {
282+
use std::ptr;
283+
use winapi::um::stringapiset::MultiByteToWideChar;
284+
use winapi::um::winnls::GetOEMCP;
285+
286+
if bytes.is_empty() {
287+
return String::new();
288+
}
289+
290+
unsafe {
291+
let code_page = GetOEMCP();
292+
let wide_len = MultiByteToWideChar(
293+
code_page,
294+
0,
295+
bytes.as_ptr() as *const i8,
296+
bytes.len() as i32,
297+
ptr::null_mut(),
298+
0,
299+
);
300+
301+
if wide_len <= 0 {
302+
return String::from_utf8_lossy(bytes).to_string();
303+
}
304+
305+
let mut wide = vec![0u16; wide_len as usize];
306+
let written = MultiByteToWideChar(
307+
code_page,
308+
0,
309+
bytes.as_ptr() as *const i8,
310+
bytes.len() as i32,
311+
wide.as_mut_ptr(),
312+
wide_len,
313+
);
314+
315+
if written <= 0 {
316+
return String::from_utf8_lossy(bytes).to_string();
317+
}
318+
319+
String::from_utf16_lossy(&wide)
320+
}
321+
}
322+
323+
let output = Command::new("taskkill")
324+
.args(["/F", "/T", "/PID", &pid.to_string()])
283325
.output()
284-
.map(|o| o.status.success())
285-
.unwrap_or(false)
326+
.map_err(|e| format!("启动 taskkill 失败: {}", e))?;
327+
328+
if output.status.success() {
329+
return Ok(());
330+
}
331+
332+
let stdout = decode_taskkill_output(&output.stdout);
333+
let stderr = decode_taskkill_output(&output.stderr);
334+
let message = if !stderr.trim().is_empty() {
335+
stderr.trim().to_string()
336+
} else if !stdout.trim().is_empty() {
337+
stdout.trim().to_string()
338+
} else {
339+
format!("taskkill 失败,退出码: {:?}", output.status.code())
340+
};
341+
342+
// 进程可能在我们调用前后就退出了;这种“找不到进程”视为成功。
343+
if message.contains("没有找到进程")
344+
|| message.contains("未找到进程")
345+
|| message.to_lowercase().contains("not found")
346+
{
347+
return Ok(());
348+
}
349+
350+
Err(message)
286351
}
287352

288353
#[cfg(not(target_os = "windows"))]
@@ -467,7 +532,13 @@ pub fn run_update(exe_dir: &Path) {
467532

468533
#[cfg(target_os = "windows")]
469534
{
470-
let running = check_running_processes();
535+
let mut running = check_running_processes();
536+
// 避免误杀正在执行更新的当前进程
537+
let current_pid = std::process::id();
538+
running.retain(|(name, pid)| {
539+
!(name.eq_ignore_ascii_case("llbot.exe") && *pid == current_pid)
540+
});
541+
471542
if !running.is_empty() {
472543
println!();
473544
println!("检测到以下进程正在运行:");
@@ -479,10 +550,13 @@ pub fn run_update(exe_dir: &Path) {
479550
if prompt_yes_no("是否关闭这些进程?") {
480551
for (name, pid) in &running {
481552
print!("正在关闭 {}...", name);
482-
if kill_process(*pid) {
483-
println!(" 完成");
484-
} else {
485-
println!(" 失败");
553+
match kill_process(*pid) {
554+
Ok(()) => println!(" 完成"),
555+
Err(e) => {
556+
println!(" 失败");
557+
eprintln!(" 原因: {}", e);
558+
eprintln!(" 提示: 可尝试以管理员身份运行,或手动在任务管理器结束该进程");
559+
}
486560
}
487561
}
488562
println!();
@@ -549,14 +623,22 @@ fn print_update_row(info: &UpdateInfo) {
549623
#[cfg(target_os = "windows")]
550624
fn self_update(tarball_url: &str, exe_dir: &Path) -> Result<(), String> {
551625
use std::env;
626+
use std::time::{SystemTime, UNIX_EPOCH};
627+
use std::os::windows::process::CommandExt;
628+
use winapi::um::winbase::{CREATE_BREAKAWAY_FROM_JOB, CREATE_NEW_CONSOLE};
552629

553630
let current_exe = env::current_exe()
554631
.map_err(|e| format!("获取当前exe路径失败: {}", e))?;
555632
let current_exe_name = current_exe.file_name()
556633
.and_then(|n| n.to_str())
557634
.unwrap_or("llbot.exe");
558635

559-
let temp_dir = exe_dir.join("_cli_update_temp");
636+
let pid = std::process::id();
637+
let ts = SystemTime::now()
638+
.duration_since(UNIX_EPOCH)
639+
.map(|d| d.as_secs())
640+
.unwrap_or(0);
641+
let temp_dir = env::temp_dir().join(format!("llbot-cli-update-{}-{}", pid, ts));
560642
fs::create_dir_all(&temp_dir)
561643
.map_err(|e| format!("创建临时目录失败: {}", e))?;
562644

@@ -568,54 +650,102 @@ fn self_update(tarball_url: &str, exe_dir: &Path) -> Result<(), String> {
568650
let backup_exe = exe_dir.join(format!("{}.bak", current_exe_name));
569651
let batch_script = temp_dir.join("_update.bat");
570652

571-
// 批处理:等待当前进程退出 -> 备份 -> 替换 -> 启动新版本 -> 清理
653+
// 批处理:备份 -> 替换(带重试)-> 启动新版本 -> 清理
572654
let script = format!(
573655
r#"@echo off
574-
chcp 65001 >nul
575-
echo 正在更新 LLBot CLI,请稍候...
576-
577-
:wait
656+
setlocal EnableExtensions
657+
658+
echo Updating LLBot CLI... Please wait.
659+
660+
set "CURRENT={current}"
661+
set "BACKUP={backup}"
662+
set "NEWEXE={new_exe}"
663+
set "TEMPDIR={temp_dir}"
664+
665+
set /a MAX_RETRY=10
666+
667+
echo Backing up current executable...
668+
set /a i=0
669+
:try_backup
670+
if exist "%BACKUP%" del /f /q "%BACKUP%" >nul 2>&1
671+
move /y "%CURRENT%" "%BACKUP%" >nul 2>&1
672+
if not errorlevel 1 goto backup_ok
673+
set /a i+=1
674+
if %i% GEQ %MAX_RETRY% (
675+
echo [ERROR] Failed to backup current executable.
676+
echo It may still be running or you may lack permission.
677+
goto fail
678+
)
679+
echo Waiting for file to be released... (%i%/%MAX_RETRY%)
578680
timeout /t 1 /nobreak >nul
579-
tasklist /FI "PID eq {pid}" 2>NUL | find /I "{pid}" >NUL
580-
if not errorlevel 1 goto wait
581-
582-
echo 备份旧版本...
583-
if exist "{backup}" del /f /q "{backup}"
584-
move /y "{current}" "{backup}"
585-
586-
echo 安装新版本...
587-
copy /y "{new_exe}" "{current}"
588-
589-
if errorlevel 1 (
590-
echo 更新失败,正在恢复...
591-
move /y "{backup}" "{current}"
592-
pause
593-
exit /b 1
681+
goto try_backup
682+
683+
:backup_ok
684+
685+
echo Installing new executable...
686+
set /a i=0
687+
:try_copy
688+
copy /y "%NEWEXE%" "%CURRENT%" >nul 2>&1
689+
if not errorlevel 1 goto copy_ok
690+
set /a i+=1
691+
if %i% GEQ %MAX_RETRY% (
692+
echo [ERROR] Failed to copy new executable. Restoring...
693+
move /y "%BACKUP%" "%CURRENT%" >nul 2>&1
694+
goto fail
594695
)
595-
596-
echo 更新完成!
597-
timeout /t 2 /nobreak >nul
598-
599-
start "" "{current}"
600-
start /b "" cmd /c "timeout /t 3 /nobreak >nul & rmdir /s /q "{temp_dir}" 2>nul"
601-
exit
696+
echo Retry copy... (%i%/%MAX_RETRY%)
697+
timeout /t 1 /nobreak >nul
698+
goto try_copy
699+
700+
:copy_ok
701+
702+
echo Update finished.
703+
echo Press any key to continue...
704+
pause
705+
706+
start "" "%CURRENT%"
707+
start /b "" cmd /c "timeout /t 3 /nobreak >nul & rmdir /s /q ""%TEMPDIR%"" 2>nul"
708+
goto :eof
709+
710+
:fail
711+
echo.
712+
echo Update failed.
713+
echo Tips:
714+
echo 1) Run as Administrator
715+
echo 2) Make sure llbot/pmhq/QQ are stopped
716+
echo 3) Avoid protected install locations
717+
echo.
718+
echo Press any key to close this window...
719+
pause
602720
"#,
603-
pid = std::process::id(),
604721
backup = backup_exe.display(),
605722
current = current_exe.display(),
606723
new_exe = new_exe.display(),
607724
temp_dir = temp_dir.display(),
608725
);
609726

610-
fs::write(&batch_script, &script)
727+
// cmd.exe 对仅 LF 换行的 .bat 解析不稳定,可能导致多行粘连成一行。
728+
// 这里强制写入 CRLF。注意不要写入 UTF-8 BOM,cmd.exe 可能无法正确识别首行指令。
729+
let script_crlf = script.replace("\n", "\r\n");
730+
731+
fs::write(&batch_script, script_crlf.as_bytes())
611732
.map_err(|e| format!("创建更新脚本失败: {}", e))?;
612733

613734
println!("启动更新脚本,程序即将退出...");
614-
615-
Command::new("cmd")
616-
.args(["/C", "start", "", "/MIN", batch_script.to_str().unwrap()])
617-
.spawn()
618-
.map_err(|e| format!("启动更新脚本失败: {}", e))?;
735+
736+
// 说明:有些宿主(VS Code/终端)会把当前进程放进 JobObject,并在进程退出时
737+
// 连带杀掉子进程,导致更新脚本“看起来没有启动”。这里用 BREAKAWAY + NEW_CONSOLE
738+
// 尽量让更新脚本独立存活。
739+
let mut cmd = Command::new("cmd");
740+
// 使用 /C 执行更新脚本。按键等待放在脚本内部,避免外层命令拼接带来的解析差异。
741+
// 另外把工作目录切到临时目录,避免路径引号/转义问题。
742+
cmd.current_dir(&temp_dir)
743+
.arg("/C")
744+
.arg("call _update.bat");
745+
cmd.creation_flags(CREATE_BREAKAWAY_FROM_JOB | CREATE_NEW_CONSOLE);
746+
747+
cmd.spawn()
748+
.map_err(|e| format!("启动更新脚本失败: {}(可能需要管理员权限,或被 JobObject 限制)", e))?;
619749

620750
std::process::exit(0);
621751
}
@@ -624,14 +754,20 @@ exit
624754
fn self_update(tarball_url: &str, exe_dir: &Path) -> Result<(), String> {
625755
use std::env;
626756
use std::os::unix::fs::PermissionsExt;
757+
use std::time::{SystemTime, UNIX_EPOCH};
627758

628759
let current_exe = env::current_exe()
629760
.map_err(|e| format!("获取当前exe路径失败: {}", e))?;
630761
let current_exe_name = current_exe.file_name()
631762
.and_then(|n| n.to_str())
632763
.unwrap_or("llbot");
633764

634-
let temp_dir = exe_dir.join("_cli_update_temp");
765+
let pid = std::process::id();
766+
let ts = SystemTime::now()
767+
.duration_since(UNIX_EPOCH)
768+
.map(|d| d.as_secs())
769+
.unwrap_or(0);
770+
let temp_dir = env::temp_dir().join(format!("llbot-cli-update-{}-{}", pid, ts));
635771
fs::create_dir_all(&temp_dir)
636772
.map_err(|e| format!("创建临时目录失败: {}", e))?;
637773

0 commit comments

Comments
 (0)