在安装更新之前关闭程序的运行版本(Inno Setup)
Posted
技术标签:
【中文标题】在安装更新之前关闭程序的运行版本(Inno Setup)【英文标题】:Close running version of program before installing update (Inno Setup) 【发布时间】:2011-03-31 06:54:18 【问题描述】:这应该很简单,我需要在安装程序启动时停止运行任何以前版本的程序。
大多数人建议创建一个 exe
来执行此操作并在 Inno Setup 开始之前调用它。我使用 AutoIt 创建了一个exe
,它会杀死我程序的所有进程。问题是我不知道如何让 Inno Setup 在安装任何东西之前调用它。
如何在安装文件之前调用可执行文件?
或者,如果我可以检测一个程序是否正在运行并告诉用户关闭它,那也可以。
【问题讨论】:
【参考方案1】:如果应用程序有 Mutex,您可以在 Inno Setup 安装程序中添加 AppMutex
值,它将显示一条消息告诉用户停止程序。您可以通过使用 SysInternals Process Explorer 并选择程序/进程并查看下窗格中的句柄 (CTRL-H) 来找到 Mutex(如果有的话)。
这是一个知识库文章的链接,其中提到了几种方法:http://www.vincenzo.net/isxkb/index.php?title=Detect_if_an_application_is_running
或者,您可以在InitializeSetup
中尝试此(未测试)代码:
[Setup]
;If the application has Mutex, uncomment the line below, comment the InitializeSetup function out, and use the AppMutex.
;AppMutex=MyApplicationMutex
[Code]
const
WM_CLOSE = 16;
function InitializeSetup : Boolean;
var winHwnd: Longint;
retVal : Boolean;
strProg: string;
begin
Result := True;
try
//Either use FindWindowByClassName. ClassName can be found with Spy++ included with Visual C++.
strProg := 'Notepad';
winHwnd := FindWindowByClassName(strProg);
//Or FindWindowByWindowName. If using by Name, the name must be exact and is case sensitive.
strProg := 'Untitled - Notepad';
winHwnd := FindWindowByWindowName(strProg);
Log('winHwnd: ' + IntToStr(winHwnd));
if winHwnd <> 0 then
Result := PostMessage(winHwnd,WM_CLOSE,0,0);
except
end;
end;
【讨论】:
感谢 Mirtheil,这正是我所需要的。其他人都提供了合理的答案,但事实证明这是完美的解决方案。 实际上,它对任何东西都不起作用。一旦使用了正确的窗口名称,它就可以在 Windows 7 和其他操作系统上运行。我还添加了 FindWindowByWindowName 的替代方法,即 FindWindowByClassName。如果程序的窗口名称发生变化,FindWindowByClassName 可能是更好的选择。 对于 Inno 5.5.0+ 有一个新的解决方案,正如我在 late response 中对这个问题所解释的那样。 根据documentation,最好使用PrepareToInstall
函数而不是InitializeSetup
来关闭应用进行更新。
发送WM_CLOSE
并不意味着应用程序将退出。如果它设置了“关闭到系统托盘”怎么办?你能为这种情况提出解决方案吗?我想可以发送自定义WM_USER+X
消息,或者在更糟糕的情况下执行taskkill
/tskill
。【参考方案2】:
在版本 5.5.0(2012 年 5 月发布)中,Inno Setup 增加了对 Windows Vista 和更高版本上的 Restart Manager API 的支持。
引用自 MSDN 链接文档(强调我的):
软件安装和更新需要重新启动系统的主要原因是一些正在更新的文件当前正被正在运行的应用程序或服务使用。 重启管理器可以关闭和重启除关键应用程序和服务之外的所有应用程序和服务。这将释放正在使用的文件并允许完成安装操作。它还可以消除或减少完成安装或更新所需的系统重启次数。
好处是:您无需在安装程序或应用程序中编写自定义代码来要求用户关闭它,或自动关闭它。
如果您希望您的应用程序在更新完成后重新启动,您必须先从您的应用程序中调用RegisterApplicationRestart
函数。
新指令的默认值会关闭安装程序的 [Files]
部分中包含的所有 .exe、.dll 和 .chm 文件。
与之相关的更改是(来自发行说明):
添加了新的[Setup]
部分指令:CloseApplications
,默认为yes
。如果设置为 yes 并且安装程序没有静默运行,如果安装程序检测到应用程序使用需要由[Files]
或[InstallDelete]
部分更新的文件,安装程序现在将暂停在准备安装向导页面,显示应用程序并询问用户是否安装程序应在安装完成后自动关闭应用程序并重新启动它们。如果设置为 yes 并且安装程序正在静默运行,则安装程序将始终关闭并重新启动此类应用程序,除非通过命令行告知不要这样做(见下文)。 添加了新的[Setup]
部分指令:CloseApplicationsFilter
,默认为*.exe,*.dll,*.chm
。控制安装程序将检查哪些文件正在使用中。将此设置为*.*
可以提供更好的检查,但会牺牲速度。 添加了新的[Setup]
部分指令:RestartApplications
,默认为yes
。注意:为了让安装程序能够在安装完成后重新启动应用程序,该应用程序需要使用 WindowsRegisterApplicationRestart
API 函数。 添加了安装程序支持的新命令行参数:/NOCLOSEAPPLICATIONS
和/NORESTARTAPPLICATIONS
。这些可用于覆盖新的CloseApplications
和RestartApplications
指令。 新增[Code]
支持功能:RmSessionStarted
。TWizardForm
:添加了新的PreparingMemo
属性。
【讨论】:
CloseApplicationsFilter
是许多应用程序的关键。展开过滤器以包含导致问题的文件类型。
卸载时如何要求相同?请帮忙
有人可以帮助我使用RegisterApplicationRestart
的标志吗?我的应用程序在系统重启后已经自动启动(例如它是一个自动启动服务)所以我猜我应该使用RestartNoReboot = 8
?同样在 Inno 安装程序安装新版本(更新)的情况下,使用 RestartNoPatch = 4
是否意味着如果更新失败,它不会自动重启应用程序?
flags参数的含义在docs.microsoft.com/en-us/windows/desktop/api/winbase/…中有记载【参考方案3】:
我尝试使用接受的答案(以及 jachguate 的跟进),但它不会杀死我的应用程序。看起来部分原因是我的应用程序窗口没有与之关联的文本,但无论真正的原因是什么,我使用 shell 命令来杀死它并且有效。在 [code] 部分,您要添加以下函数。在复制安装文件之前调用它。
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
ErrorCode: Integer;
begin
ShellExec('open', 'taskkill.exe', '/f /im MyProg.exe','',SW_HIDE,ewNoWait,ErrorCode);
end;
【讨论】:
【参考方案4】:如果您使用 InnoSetup,您可以考虑让 InnoSetup 安装程序执行 Windows SendBroadcastMessage,并让您的应用程序侦听该消息。当您的应用程序收到消息时,它应该自行关闭。
我自己使用 InnoSetup 安装程序完成了这项工作,效果很好。
【讨论】:
这行不通,因为该程序已经发布多年,我也需要关闭旧版本。 @Conor 你能告诉我你是怎么做到的吗? @Kainix:我的应用程序是用 Delphi 编写的。谷歌“DefaultHandler windows 消息广播”或类似的,你应该找到自己的方式。【参考方案5】:这是一个 Inno Setup 脚本的链接,如果它检测到程序正在运行,它会提示用户关闭目标程序。用户关闭程序后,可以点击“重试”按钮继续安装:
http://www.domador.net/extras/code-samples/inno-setup-close-a-program-before-reinstalling-it/
此脚本基于 Inno Setup Extensions 知识库中的一个更简单的脚本:
http://www.vincenzo.net/isxkb/index.php?title=Call_psvince.dll_on_install_and_uninstall
【讨论】:
【参考方案6】:InnoSetup 允许您将 Pascal 脚本附加到构建过程中的各个位置。尝试附加一个调用 ShellExecute 的脚本。 (如果脚本引擎还没有,您可能必须将其导入到脚本引擎中。)
【讨论】:
脚本引擎有 Exec() 所以这不是问题。我不知道如何编写帕斯卡代码来提取捆绑的 exe 文件并运行它。【参考方案7】:如果您愿意编写自己的 DLL,可以使用 TlHelp32.pas 的工具帮助 API 来确定正在运行的应用程序,然后使用 EnumWindows 为它们获取窗口句柄,然后向窗口句柄发送 WM_CLOSE .
这有点痛苦,但应该可以: 我有一些我和朋友不久前开发的实用程序包装类。不记得我们是否基于其他人的代码。
TWindows.ProcessISRunning 和 TWindows.StopProcess 可能会有所帮助。
interface
uses
Classes,
Windows,
SysUtils,
Contnrs,
Messages;
type
TProcess = class(TObject)
public
ID: Cardinal;
Name: string;
end;
TWindow = class(TObject)
private
FProcessID: Cardinal;
FProcessName: string;
FHandle: THandle;
FProcessHandle : THandle;
function GetProcessHandle: THandle;
function GetProcessID: Cardinal;
function GetProcessName: string;
public
property Handle : THandle read FHandle;
property ProcessName : string read GetProcessName;
property ProcessID : Cardinal read GetProcessID;
property ProcessHandle : THandle read GetProcessHandle;
end;
TWindowList = class(TObjectList)
private
function GetWindow(AIndex: Integer): TWindow;
protected
public
function Add(AWindow: TWindow): Integer; reintroduce;
property Window[AIndex: Integer]: TWindow read GetWindow; default;
end;
TProcessList = class(TObjectList)
protected
function GetProcess(AIndex: Integer): TProcess;
public
function Add(AProcess: TProcess): Integer; reintroduce;
property Process[AIndex: Integer]: TProcess read GetProcess; default;
end;
TWindows = class(TObject)
protected
public
class function GetHWNDFromProcessID(ProcessID: Cardinal; BuildList: Boolean = True): THandle;
class function GetProcessList: TProcessList;
class procedure KillProcess(ProcessName: string);
class procedure StopProcess(ProcessName: string);
class function ExeIsRunning(ExeName: string): Boolean;
class function ProcessIsRunning(PID: Cardinal): Boolean;
end;
implementation
uses
Forms,
Math,
PSAPI,
TlHelp32;
const
cRSPUNREGISTERSERVICE = 0;
cRSPSIMPLESERVICE = 1;
type
TProcessToHWND = class(TObject)
public
ProcessID: Cardinal;
HWND: Cardinal;
end;
function RegisterServiceProcess(dwProcessID, dwType: DWord): DWord; stdcall; external 'KERNEL32.DLL';
function GetDiskFreeSpaceEx(lpDirectoryName: PChar;
var lpFreeBytesAvailableToCaller, lpTotalNumberOfBytes: TLargeInteger;
lpTotalNumberOfFreeBytes: PLargeInteger): Boolean; stdcall;external 'KERNEL32.DLL' name 'GetDiskFreeSpaceExA'
var
GProcessToHWNDList: TObjectList = nil;
function EnumerateWindowsProc(hwnd: HWND; lParam: LPARAM): BOOL; stdcall;
var
proc: TProcessToHWND;
begin
if Assigned(GProcessToHWNDList) then
begin
proc := TProcessToHWND.Create;
proc.HWND := hwnd;
GetWindowThreadProcessID(hwnd, proc.ProcessID);
GProcessToHWNDList.Add(proc);
Result := True;
end
else
Result := False; // stop enumeration
end;
TWindows
class function TWindows.ExeIsRunning(ExeName: string): Boolean;
var
processList: TProcessList;
i: Integer;
begin
Result := False;
processList := GetProcessList;
try
for i := 0 to processList.Count - 1 do
begin
if (UpperCase(ExeName) = UpperCase(processList[i].Name)) or
(UpperCase(ExeName) = UpperCase(ExtractFileName(processList[i].Name))) then
begin
Result := True;
Break;
end;
end;
finally
processList.Free;
end;
end;
class function TWindows.GetHWNDFromProcessID(
ProcessID: Cardinal; BuildList: Boolean): THandle;
var
i: Integer;
begin
Result := 0;
if BuildList or (not Assigned(GProcessToHWNDList)) then
begin
GProcessToHWNDList.Free;
GProcessToHWNDList := TObjectList.Create;
EnumWindows(@EnumerateWindowsProc, 0);
end;
for i := 0 to GProcessToHWNDList.Count - 1 do
begin
if TProcessToHWND(GProcessToHWNDList[i]).ProcessID = ProcessID then
begin
Result := TProcessToHWND(GProcessToHWNDList[i]).HWND;
Break;
end;
end;
end;
class function TWindows.GetProcessList: TProcessList;
var
handle: THandle;
pe: TProcessEntry32;
process: TProcess;
begin
Result := TProcessList.Create;
handle := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
pe.dwSize := Sizeof(pe);
if Process32First(handle, pe) then
begin
while True do
begin
process := TProcess.Create;
process.Name := pe.szExeFile;
process.ID := pe.th32ProcessID;
Result.Add(process);
if not Process32Next(handle, pe) then
Break;
end;
end;
CloseHandle(handle);
end;
function EnumWindowsProc(Ahwnd : HWND; // handle to parent window
ALParam : Integer) : BOOL;stdcall;
var
List : TWindowList;
Wnd : TWindow;
begin
Result := True;
List := TWindowList(ALParam);
Wnd := TWindow.Create;
List.Add(Wnd);
Wnd.FHandle := Ahwnd;
end;
class procedure TWindows.KillProcess(ProcessName: string);
var
handle: THandle;
pe: TProcessEntry32;
begin
// Warning: will kill all process with ProcessName
// NB won't work on NT 4 as Tool Help API is not supported on NT
handle := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
try
pe.dwSize := Sizeof(pe);
if Process32First(handle, pe) then
begin
while True do begin
if (UpperCase(ExtractFileName(pe.szExeFile)) = UpperCase(ExtractFileName(ProcessName))) or
(UpperCase(pe.szExeFile) = UpperCase(ProcessName)) then
begin
if not TerminateProcess(OpenProcess(PROCESS_TERMINATE, False,
pe.th32ProcessID), 0) then
begin
raise Exception.Create('Unable to stop process ' + ProcessName + ': Error Code ' + IntToStr(GetLastError));
end;
end;
if not Process32Next(handle, pe) then
Break;
end;
end;
finally
CloseHandle(handle);
end;
end;
class function TWindows.ProcessIsRunning(PID: Cardinal): Boolean;
var
processList: TProcessList;
i: Integer;
begin
Result := False;
processList := GetProcessList;
try
for i := 0 to processList.Count - 1 do
begin
if processList[i].ID = PID then
begin
Result := True;
Break;
end;
end;
finally
processList.Free;
end;
end;
class procedure TWindows.StopProcess(ProcessName: string);
var
processList: TProcessList;
i: Integer;
hwnd: THandle;
begin
// Warning: will attempt to stop all process with ProcessName
if not Assigned(GProcessToHWNDList) then
GProcessToHWNDList := TObjectList.Create
else
GProcessToHWNDList.Clear;
// get list of all current processes
processList := GetProcessList;
// enumerate windows only once to determine the window handle for the processes
if EnumWindows(@EnumerateWindowsProc, 0) then
begin
for i := 0 to processList.Count - 1 do
begin
if UpperCase(ExtractFileName(processList[i].Name)) = UpperCase(ExtractFileName(ProcessName)) then
begin
hwnd := GetHWNDFromProcessID(processList[i].ID, False);
SendMessage(hwnd, WM_CLOSE, 0, 0);
end;
end;
end;
end;
TProcessList
function TProcessList.Add(AProcess: TProcess): Integer;
begin
Result := inherited Add(AProcess);
end;
function TProcessList.GetProcess(AIndex: Integer): TProcess;
begin
Result := TProcess(Items[AIndex]);
end;
TWindowList
function TWindowList.Add(AWindow: TWindow): Integer;
begin
Result := inherited Add(AWindow);
end;
function TWindowList.GetWindow(AIndex: Integer): TWindow;
begin
Result := TWindow(Items[AIndex]);
end;
TWindow
function TWindow.GetProcessHandle: THandle;
begin
if FProcessHandle = 0 then
FProcessHandle := OpenProcess(Windows.SYNCHRONIZE or Windows.PROCESS_TERMINATE,
True, FProcessID);
Result := FProcessHandle;
end;
function TWindow.GetProcessID: Cardinal;
var
Pid : Cardinal;
begin
if FProcessID = 0 then
begin
Pid := 1;
GetWindowThreadProcessId(Handle, Pid);
FProcessID := Pid;
end;
Result := FProcessID;
end;
function TWindow.GetProcessName: string;
var
Buffer : packed array [1..1024] of char;
len : LongWord;
begin
FillChar(Buffer, SizeOf(Buffer), 0);
if FProcessName = '' then
begin
len := GetWindowModuleFileName(Handle, @Buffer[1], 1023);
FProcessName := Copy(Buffer, 1, Len);
end;
Result := FProcessName;
end;
end.
【讨论】:
在 InnoSetup 中编译失败,谁能修复它?我以前从未使用过帕斯卡。【参考方案8】:我使用WMIC 取得了成功:
procedure CurStepChanged(CurStep: TSetupStep);
var
ResultCode: Integer;
wmicommand: string;
begin
// before installing any file
if CurStep = ssInstall then
begin
wmicommand := ExpandConstant('PROCESS WHERE "ExecutablePath like ''app\%%''" DELETE');
// WMIC "like" expects escaped backslashes
StringChangeEx(wmicommand, '\', '\\', True);
// you can/should add an "if" around this and check the ResultCode
Exec('WMIC', wmicommand, '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
end;
您也可以在InitializeSetup
中执行此操作,但如果您这样做,请记住您还没有访问app
常量。我的程序不要求安装路径,但你的可能。
【讨论】:
【参考方案9】:好吧,我认为执行此操作的更简单方法可能是在 Delphi 中创建一个 DLL,用于检测您的程序是否正在运行并要求用户关闭它,将该 DLL 放入您的设置中并使用标志“dontcopy”(检查在 http://www.jrsoftware.org/ishelp/ 下 Pascal Scripting \ 以使用 DLL 为例)。
顺便说一句,下次使用互斥锁时,Inno Setup 也支持,而且更容易。
编辑:对于提取文件(如果您想使用您提到的那个 .exe),只需使用 ExtractTemporaryFile()。
【讨论】:
【参考方案10】:在 [Setup] 部分添加 CloseApplications=true。
如果设置为 yes 或 force 并且安装程序未静默运行,如果安装程序检测到应用程序使用需要由 [Files] 或 [InstallDelete] 部分更新的文件,安装程序将在准备安装向导页面暂停,显示应用程序并询问用户安装程序是否应在安装完成后自动关闭应用程序并重新启动它们。
【讨论】:
CloseApplications=true
是默认值。 @jachguate 的答案已经涵盖了这一点。以上是关于在安装更新之前关闭程序的运行版本(Inno Setup)的主要内容,如果未能解决你的问题,请参考以下文章
在关闭Inno Setup到Windows之前,发送关于重新检查PATH环境变量的消息[重复]。
Inno Setup:安装其他安装程序并在继续安装之前运行它