@@ -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" ) ]
550624fn 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 ! (
573655r#"@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%)
578680timeout /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
624754fn 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