How to execute 7zip without blocking the InnoSetup interface?

My InnoSetup GUI is frozen while unpacking.

I have a procedure DoUnzip(source: String; targetdir: String) with a kernel

 unzipTool := ExpandConstant('{tmp}\7za.exe'); Exec(unzipTool, ' x "' + source + '" -o"' + targetdir + '" -y', '', SW_HIDE, ewWaitUntilTerminated, ReturnCode); 

This procedure is called several times, and the Exec operation blocks the user interface. There is a very short amount of time between executions where the Inno GUI can be moved / moved.

I know that there are other options for TExecWait instead of ewWaitUntilTerminated , such as ewNoWait and ewWaitUntilIdle , but unfortunately they do not help in this case. Using ewNoWait will result in several unpacking operations being performed simultaneously.

I am looking for a way to perform an external decompression operation and wait for it to complete, but without blocking the user interface. How can I implement this?


Here are my notes and ideas:

Waiting for the completion of the process is blocked if you do not wait in a thread other than the main one. I think some feedback is required that is executed when the unpack operation completes.

I know that InnoSetup does not provide this function out of the box, see https://github.com/jrsoftware/issrc/issues/149

When searching for related questions on StackOverflow, I came across the question of Using a callback to display file names from an external decompression dll (Inno Setup) , where I found Mirals Answer . It uses InnoCallback in combination with another DLL.

I think in my case it could be 7zxa.dll for unzip operation. But he does not accept the callback. Thus, the following code is just a draft concept / idea. One of the problems is that 7zxa.dll does not accept callbacks. Another problem is that the 7zxa API is not really inviting to work.

 [Code] type TMyCallback = procedure(Filename: PChar); // wrapper to tell callback function to InnoCallback function WrapMyCallback(Callback: TMyCallback; ParamCount: Integer): LongWord; external ' WrapCallback@files :innocallback.dll stdcall'; // the call to the unzip dll // P!: the 7zxa.dll doesn't accept a callback procedure DoUnzipDll(Blah: Integer; Foo: String; ...; Callback: LongWord); external ' DoUnzipDll@files :7zxa.dll stdcall'; // the actual callback action procedure MyCallback(Filename: PChar); begin // refresh the GUI end; //----- var Callback : LongWord; // tell innocallback the callback procedure as 1 parameter Callback := WrapMyCallback(@MyCallback, 1); // pass the wrapped callback to the unzip DLL DoUnzipDll(source, target, ..., Callback); procedure DoUnzip(src, target : String); begin DoUnzipDll(ExpandConstant(src), ExpandConstant(target)); end; 

Update

@Rik suggested combining the ShellExecuteEx () WinAPI function with INFINITE WaitForSingleObject.

I have implemented and tested this approach. The code is below.

Unpacking works, but the InnoSetup window can only be moved / moved for a short time between separate unlock operations. During a long start, unzip the GUI completely does not respond - drag / drop button / without cancel. I added BringToFrontAndRestore (), but it seems that the new process has focus.

 const WAIT_OBJECT_0 = $0; WAIT_TIMEOUT = $00000102; SEE_MASK_NOCLOSEPROCESS = $00000040; INFINITE = $FFFFFFFF; { Infinite timeout } type TShellExecuteInfo = record cbSize: DWORD; fMask: Cardinal; Wnd: HWND; lpVerb: string; lpFile: string; lpParameters: string; lpDirectory: string; nShow: Integer; hInstApp: THandle; lpIDList: DWORD; lpClass: string; hkeyClass: THandle; dwHotKey: DWORD; hMonitor: THandle; hProcess: THandle; end; function ShellExecuteEx(var lpExecInfo: TShellExecuteInfo): BOOL; external 'ShellExecuteEx{#AW}@shell32.dll stdcall'; function WaitForSingleObject(hHandle: THandle; dwMilliseconds: DWORD): DWORD; external ' WaitForSingleObject@kernel32.dll stdcall'; function CloseHandle(hObject: THandle): BOOL; external ' CloseHandle@kernel32.dll stdcall'; procedure DoUnzip(source: String; targetdir: String); var unzipTool, unzipParams : String; // path to unzip util ReturnCode : Integer; // errorcode ExecInfo: TShellExecuteInfo; begin // source might contain {tmp} or {app} constant, so expand/resolve it to path name source := ExpandConstant(source); unzipTool := ExpandConstant('{tmp}\7za.exe'); unzipParams := ' x "' + source + '" -o"' + targetdir + '" -y'; ExecInfo.cbSize := SizeOf(ExecInfo); ExecInfo.fMask := SEE_MASK_NOCLOSEPROCESS; ExecInfo.Wnd := 0; ExecInfo.lpFile := unzipTool; ExecInfo.lpParameters := unzipParams; ExecInfo.nShow := SW_HIDE; if not FileExists(unzipTool) then MsgBox('UnzipTool not found: ' + unzipTool, mbError, MB_OK) else if not FileExists(source) then MsgBox('File was not found while trying to unzip: ' + source, mbError, MB_OK) else begin // ShellExecuteEx combined with INFINITE WaitForSingleObject if ShellExecuteEx(ExecInfo) then begin while WaitForSingleObject(ExecInfo.hProcess, INFINITE) <> WAIT_OBJECT_0 do begin InstallPage.Surface.Update; //BringToFrontAndRestore; WizardForm.Refresh(); end; CloseHandle(ExecInfo.hProcess); end; end; end; 
+5
source share
1 answer

As I intended to use INFINITE with WaitForSingleObject , it still blocks the main thread. Then I thought using a shorter timeout with WaitForSingleObject . But the problem is that the main thread remains in the while WaitForSingleObject loop and does not respond to movement. WizardForm.Refresh does not make it movable. It simply updates the form, but does not process other messages (e.g. WM_MOVE ). You need something like Application.ProcessMessages so that windows can move around. Since Inno Setup does not have ProcessMessages , we could create it ourselves.

Below is your code with ProcessMessage . It expects 100 milliseconds to wait WaitForSingleObject , and if it is still pending, it executes ProcessMessage and Refresh . This will allow you to move the window. You can play a little with a value of 100.

Another way could be to save ExecInfo and continue with another part of the installation. On the last page, you can check if the process is complete. If it is not a loop with AppProcessMessage , until it is.

 [Code] #ifdef UNICODE #define AW "W" #else #define AW "A" #endif const WAIT_OBJECT_0 = $0; WAIT_TIMEOUT = $00000102; SEE_MASK_NOCLOSEPROCESS = $00000040; INFINITE = $FFFFFFFF; { Infinite timeout } type TShellExecuteInfo = record cbSize: DWORD; fMask: Cardinal; Wnd: HWND; lpVerb: string; lpFile: string; lpParameters: string; lpDirectory: string; nShow: Integer; hInstApp: THandle; lpIDList: DWORD; lpClass: string; hkeyClass: THandle; dwHotKey: DWORD; hMonitor: THandle; hProcess: THandle; end; function ShellExecuteEx(var lpExecInfo: TShellExecuteInfo): BOOL; external 'ShellExecuteEx{#AW}@shell32.dll stdcall'; function WaitForSingleObject(hHandle: THandle; dwMilliseconds: DWORD): DWORD; external ' WaitForSingleObject@kernel32.dll stdcall'; function CloseHandle(hObject: THandle): BOOL; external ' CloseHandle@kernel32.dll stdcall'; //----------------------- //"Generic" code, some old "Application.ProcessMessages"-ish procedure //----------------------- type TMsg = record hwnd: HWND; message: UINT; wParam: Longint; lParam: Longint; time: DWORD; pt: TPoint; end; const PM_REMOVE = 1; function PeekMessage(var lpMsg: TMsg; hWnd: HWND; wMsgFilterMin, wMsgFilterMax, wRemoveMsg: UINT): BOOL; external ' PeekMessageA@user32.dll stdcall'; function TranslateMessage(const lpMsg: TMsg): BOOL; external ' TranslateMessage@user32.dll stdcall'; function DispatchMessage(const lpMsg: TMsg): Longint; external ' DispatchMessageA@user32.dll stdcall'; procedure AppProcessMessage; var Msg: TMsg; begin while PeekMessage(Msg, WizardForm.Handle, 0, 0, PM_REMOVE) do begin TranslateMessage(Msg); DispatchMessage(Msg); end; end; //----------------------- //----------------------- procedure DoUnzip(source: String; targetdir: String); var unzipTool, unzipParams : String; // path to unzip util ReturnCode : Integer; // errorcode ExecInfo: TShellExecuteInfo; begin // source might contain {tmp} or {app} constant, so expand/resolve it to path name source := ExpandConstant(source); unzipTool := ExpandConstant('{tmp}\7za.exe'); unzipParams := ' x "' + source + '" -o"' + targetdir + '" -y'; ExecInfo.cbSize := SizeOf(ExecInfo); ExecInfo.fMask := SEE_MASK_NOCLOSEPROCESS; ExecInfo.Wnd := 0; ExecInfo.lpFile := unzipTool; ExecInfo.lpParameters := unzipParams; ExecInfo.nShow := SW_HIDE; if not FileExists(unzipTool) then MsgBox('UnzipTool not found: ' + unzipTool, mbError, MB_OK) else if not FileExists(source) then MsgBox('File was not found while trying to unzip: ' + source, mbError, MB_OK) else begin // ShellExecuteEx combined with INFINITE WaitForSingleObject if ShellExecuteEx(ExecInfo) then begin while WaitForSingleObject(ExecInfo.hProcess, 100) = WAIT_TIMEOUT { WAIT_OBJECT_0 } do begin AppProcessMessage; //InstallPage.Surface.Update; //BringToFrontAndRestore; WizardForm.Refresh(); end; CloseHandle(ExecInfo.hProcess); end; end; end; 

(This code is checked and works for me)

+6
source

All Articles