Inno Setup: Unable to unload and delete a DLL required by install from the {tmp} folder at end of install [duplicate] - dll

I'm extending my Inno-Setup script with code that I can best implement in C# in a managed DLL. I already know how to export methods from a managed DLL as functions for use in an unmanaged process. It can be done by IL weaving, and there are tools to automate this:
NetDllExport (written by me)
UnmanagedExports
So after exporting, I can call my functions from Pascal script in an Inno-Setup installer. But then there's one issue: The DLL can't seem to be unloaded anymore. Using Inno-Setup's UnloadDLL(...) has no effect and the file remains locked until the installer exits. Because of this, the setup waits for 2 seconds and then fails to delete my DLL file from the temp directory (or install directory). In fact, it really stays there until somebody cleans up the drive.
I know that managed assemblies cannot be unloaded from an AppDomain anymore, unless the entire AppDomain is shut down (the process exits). But what does it mean to the unmanaged host process?
Is there a better way to allow Inno-Setup to unload or delete my DLL file after loading and using it?

As suggested in other answers, you can launch a separate process at the end of the installation that will take care of the cleanup, after the installation processes finishes.
A simple solution is creating an ad-hoc batch file that loops until the DLL file can be deleted and then also deletes the (now empty) temporary folder and itself.
procedure DeinitializeSetup();
var
FilePath: string;
BatchPath: string;
S: TArrayOfString;
ResultCode: Integer;
begin
FilePath := ExpandConstant('{tmp}\MyAssembly.dll');
if not FileExists(FilePath) then
begin
Log(Format('File %s does not exist', [FilePath]));
end
else
begin
BatchPath :=
ExpandConstant('{%TEMP}\') +
'delete_' + ExtractFileName(ExpandConstant('{tmp}')) + '.bat';
SetArrayLength(S, 7);
S[0] := ':loop';
S[1] := 'del "' + FilePath + '"';
S[2] := 'if not exist "' + FilePath + '" goto end';
S[3] := 'goto loop';
S[4] := ':end';
S[5] := 'rd "' + ExpandConstant('{tmp}') + '"';
S[6] := 'del "' + BatchPath + '"';
if not SaveStringsToFile(BatchPath, S, False) then
begin
Log(Format('Error creating batch file %s to delete %s', [BatchPath, FilePath]));
end
else
if not Exec(BatchPath, '', '', SW_HIDE, ewNoWait, ResultCode) then
begin
Log(Format('Error executing batch file %s to delete %s', [BatchPath, FilePath]));
end
else
begin
Log(Format('Executed batch file %s to delete %s', [BatchPath, FilePath]));
end;
end;
end;

You could add a batch script (in the form of running cmd -c) to be executed at the end of setup that waits for the file to be deletable and deletes it. (just make sure to set the inno option to not wait for the cmd process to complete)
You could also make your installed program detect and delete it on first execution.

As suggested in this Code Project Article : https://www.codeproject.com/kb/threads/howtodeletecurrentprocess.aspx
call a cmd with arguments as shown below.
Process.Start("cmd.exe", "/C ping 1.1.1.1 -n 1 -w 3000 > Nul & Del " + Application.ExecutablePath);
But basically as #Sean suggested, make sure you dont wait for the cmd.exe to exit in your script.

While not exactly an answer to your question, can't you just mark the DLL to be deleted next time the computer is restarted?

Here's what I did, adapted from Martin's great answer. Notice the 'Sleep', this did the trick for me. Because the execution is called in a background thread, that is not a blocker, and leaves sufficient time for InnoSetup to free up the resources.
After doing that, I was able to clean the temporary folder.
// Gets invoked at the end of the installation
procedure DeinitializeSetup();
var
BatchPath: String;
S: TArrayOfString;
FilesPath: TStringList;
ResultCode, I, ErrorCode: Integer;
begin
I := 0
FilesPath := TStringList.Create;
FilesPath.Add(ExpandConstant('{tmp}\DLL1.dll'));
FilesPath.Add(ExpandConstant('{tmp}\DLL2.dll'));
FilesPath.Add(ExpandConstant('{tmp}\DLLX.dll'));
while I < FilesPath.Count do
begin
if not FileExists(FilesPath[I]) then
begin
Log(Format('File %s does not exist', [FilesPath[I]]));
end
else
begin
UnloadDLL(FilesPath[I]);
if Exec('powershell.exe',
FmtMessage('-NoExit -ExecutionPolicy Bypass -Command "Start-Sleep -Second 5; Remove-Item -Recurse -Force -Path %1"', [FilesPath[I]]),
'', SW_HIDE, ewNoWait, ErrorCode) then
begin
Log(Format('Temporary file %s successfully deleted', [ExpandConstant(FilesPath[I])]));
end
else
begin
Log(Format('Error while deleting temporary file: %s', [ErrorCode]));
end;
inc(I);
end;
end;
Exec('powershell.exe',
FmtMessage('-NoExit -ExecutionPolicy Bypass -Command "Start-Sleep -Second 5; Remove-Item -Recurse -Force -Path %1"', [ExpandConstant('{tmp}')]),
'', SW_HIDE, ewNoWait, ErrorCode);
Log(Format('Temporary folder %s successfully deleted', [ExpandConstant('{tmp}')]));
end;

The easy way to do what you want is through an AppDomain. You can unload an AppDomain, just not the initial one. So the solution is to create a new AppDomain, load your managed DLL in that and then unload the AppDomain.
AppDomain ad = AppDomain.CreateDomain("Isolate DLL");
Assembly a = ad.Load(new AssemblyName("MyManagedDll"));
object d = a.CreateInstance("MyManagedDll.MyManagedClass");
Type t = d.GetType();
double result = (double)t.InvokeMember("Calculate", BindingFlags.InvokeMethod, null, d, new object[] { 1.0, 2.0 });
AppDomain.Unload(ad);
Here is what the DLL code looks like...
namespace MyManagedDll
{
public class MyManagedClass
{
public double Calculate(double a, double b)
{
return a + b;
}
}
}

Related

Manipulating MS Word Open Dialog from Delphi

Every time I run this code, I manually confirm that I want to convert "PDF Files".
Is there any way to automate it?
procedure TFWordAnalyzer.Button1Click(Sender: TObject);
var
Clipboard:TClipboard;
WordApp,WordDocument: OleVariant;
begin
try
WordApp := GetActiveOleObject('Word.Application');
except
WordApp := CreateOleObject('Word.Application') ;
end;
WordApp.visible := true;
WordApp.DisplayAlerts := False;
WordDocument := WordApp.Documents.Open('D:\Corpus\Adyg\Hudmir.pdf', true, false);
WordDocument.range.copy;
sleep(1000);//Otherwise it fails
RichEdit1.Clear;
RichEdit1.PasteFromClipboard;
WordDocument.close;
WordApp.Quit;
WordApp := Unassigned;
end;
Both of the problems you have mentioned namely how to avoid the Convert File dialog and the pop-up prompt about saving data copied to the Clipboard can by avoided by specifiying the correct values in calls to Word's automation.
1. Avoiding the Convert File dialog
Update the line of your code which opens the document to
WordDocument := WordApp.Documents.Open('D:\Corpus\Adyg\Hudmir.pdf', ConfirmConversions := False);
This should open the document without the Confirm Conversion dialog being invoked.
Assuming that works, you should be able to reinstate your second and third arguments, so that you have
WordDocument := WordApp.Documents.Open('D:\Corpus\Adyg\Hudmir.pdf', true, false, ConfirmConversions := False);
Note that the syntax
WordApp.Documents.Open([...], ConfirmConversions := False)
was added at the same time (in D2) at the same time as OLE automation via OleVariants, to support optional arguments being passed to automation calls.
PS: With this change, you may find that your call to Sleep in unnecessary. I say "may" because my test code did not need it in the first place.
2. Avoiding the prompt to save the contents of the Clipboard.
This prompt pops if you attempt to close Word using automation when you have copied a large amount of data to the Clipboard.
To avoid it, simply replace
WordApp.Quit;
by
WordApp.Quit(SaveChanges := False);
That should be all you need. The fact that this works is slightly counter-intuitive: As you saved the data from your WordDocument, you might expect that using WordDocument is the way to clear the Clipboard data (see the now-superfluous update below). However, by setting up a test app with a number of checkboxes which control the parameter values for WordDocument.Close and WordApp.Close, and testing the various permutations of their settings, I established that only the value of SaveChanges passed to WordApp.Quit influences this behaviour and avoids the dialog (if it would otherwise have been displayed).
The remainder of this answer can be disregarded but I have let it remain aas it may help future readers with similar problems. I have also added the VBA tag to the q, as I found numerous questions about how to avoid the Clipboard-save prompt in VBA questions, but none with a definitive solution which worked with the code here.
+++++++++++++++++++++++++++++++++++++++++
Update The OP reported that the above code can result in a prompt from
Word as it closes down saying that there is a large amount of data on the Clipboard,
and queried whether it was possible to automate responding to the prompt.
Google found several suggestions for avoiding the prompt, but I found that none
of them worked reliably working with a 17Mb .Pdf file. The method shown below,
which I arrived at by experiment, does seem to work reliably but is not as simple
as I would like it to be.
The reason the prompt occurs is that Word evidently sets up internal pointers to
the contents provided to the Clipboard, so I wondered if setting the text of the
Range to something much smaller and then discarding it before closing the document
would work. I ran into 2 problems:
calling vRange.End := 1 had no effect
trying to call vRange.Set_Text := ' ' provoked an exception claiming that vRange
does not support the Set_Text method despite the fact that the Range interface in
the Word2000.Pas import unit for MS Word clearly does. So, the code below picks up
the Range object contained in the vRange variant and calls Set_Text on that. This works
but means that you need to add Word2000.Pas (or one of the more recent Word import
units) to the Uses clause.
Note that I removed the call to GetActiveOle object to avoid an exception being
raised when it failed, which disrupted debugging.
procedure TForm1.Button1Click(Sender: TObject);
var
Clipboard:TClipboard;
WordApp,
WordDocument: OleVariant;
vRange : OleVariant;
iRange : Range; // or WordRange for D10.3.1+
begin
try
WordApp := CreateOleObject('Word.Application') ;
WordApp.visible := true;
WordApp.DisplayAlerts := False;
try
WordDocument := WordApp.Documents.Open('D:\aaad7\aaaofficeauto\test.pdf', ConfirmConversions := False); //, true, false);
vRange := WordDocument.range;
Label1.Caption := IntToStr(vRange.Start) + ':' + IntToStr(vRange.End);
vRange.copy;
sleep(1000);//Otherwise it fails
RichEdit1.Clear;
RichEdit1.PasteFromClipboard;
// vRange.Set_Text(' '); // This provokes a "Method Set_Text not supported by automation object" exception
// SO ...
// Pick up the Range interface contained in the vRange variant
iRange := IDispatch(WordDocument.range) as Range;
// and call Set_Text on it to set the Range's text to a single space
iRange.Set_Text(' ');
// Clear the iRange interface
iRange := Nil;
finally
// beware that the following discards and changes to the .Pdf
// so if you want to save any changes, you should do it
// before calling iRange.Set_Text
WordDocument.close(False, False, False);
WordDocument := UnAssigned;
end;
finally
WordApp.Quit(False, False, False);
WordApp := Unassigned;
end;
end;
Tested with MS Word 2010 on Windows 10 64-bit/
Here is the working code:
procedure TFWordAnalyzer.Button5Click(Sender: TObject);
var
Clipboard:TClipboard;
WordApp,
WordDocument: OleVariant;
vRange : OleVariant;
iRange : WordRange;
begin
try
WordApp := CreateOleObject('Word.Application') ;
WordApp.visible := true;
WordApp.DisplayAlerts := False;
try
WordDocument := WordApp.Documents.Open('D:\Corpus\Adyg\Hudmir.pdf', ConfirmConversions := False); //, true, false);
vRange := WordDocument.range;
Label1.Caption := IntToStr(vRange.Start) + ':' + IntToStr(vRange.End);
vRange.copy;
sleep(1000);//Otherwise it fails
RichEdit1.Clear;
RichEdit1.PasteFromClipboard;
iRange := IDispatch(WordDocument.range) as WordRange;
iRange.Set_Text(' ');
iRange := Nil;
finally
WordDocument.close(False, False, False);
WordDocument := UnAssigned;
end;
finally
WordApp.Quit(False, False, False);
WordApp := Unassigned;
end;
end;

Perl6: large gzipped files read line by line

I'm trying to read a gz file line by line in Perl6, however, I'm getting blocked:
How to read gz file line by line in Perl6 however, this method, reading everything into :out uses far too much RAM to be usable except on very small files.
I don't understand how to use Perl6's Compress::Zlib to get everything line by line, although I opened an issue on their github https://github.com/retupmoca/P6-Compress-Zlib/issues/17
I'm trying Perl5's Compress::Zlib to translate this code, which works perfectly in Perl5:
use Compress::Zlib;
my $file = "data.txt.gz";
my $gz = gzopen($file, "rb") or die "Error reading $file: $gzerrno";
while ($gz->gzreadline($_) > 0) {
# Process the line read in $_
}
die "Error reading $file: $gzerrno" if $gzerrno != Z_STREAM_END ;
$gz->gzclose() ;
to something like this using Inline::Perl5 in Perl6:
use Compress::Zlib:from<Perl5>;
my $file = 'chrMT.1.vcf.gz';
my $gz = Compress::Zlib::new(gzopen($file, 'r');
while ($gz.gzreadline($_) > 0) {
print $_;
}
$gz.gzclose();
but I can't see how to translate this :(
I'm confused by Lib::Archive example https://github.com/frithnanth/perl6-Archive-Libarchive/blob/master/examples/readfile.p6 I don't see how I can get something like item 3 here
There should be something like
for $file.IO.lines(gz) -> $line { or something like that in Perl6, if it exists, I can't find it.
How can I read a large file line by line without reading everything into RAM in Perl6?
Update Now tested, which revealed an error, now fixed.
Solution #2
use Compress::Zlib;
my $file = "data.txt.gz" ;
my $handle = try open $file or die "Error reading $file: $!" ;
my $zwrap = zwrap($handle, :gzip) ;
for $zwrap.lines {
.print
}
CATCH { default { die "Error reading $file: $_" } }
$handle.close ;
I've tested this with a small gzipped text file.
I don't know much about gzip etc. but figured this out based on:
Knowing P6;
Reading Compress::Zlib's README and choosing the zwrap routine;
Looking at the module's source code, in particular the signature of the zwrap routine our sub zwrap ($thing, :$zlib, :$deflate, :$gzip);
And trial and error, mainly to guess that I needed to pass the :gzip adverb.
Please comment on whether my code works for you. I'm guessing the main thing is whether it's fast enough for the large files you have.
A failed attempt at solution #5
With solution #2 working I would have expected to be able to write just:
use Compress::Zlib ;
.print for "data.txt.gz".&zwrap(:gzip).lines ;
But that fails with:
No such method 'eof' for invocant of type 'IO::Path'
This is presumably because this module was written before the reorganization of the IO classes.
That led me to #MattOates' IO::Handle like object with .lines ? issue. I note no response and I saw no related repo at https://github.com/MattOates?tab=repositories.
I am focusing on the Inline::Perl5 solution that you tried.
For the call to $gz.gzreadline($_): it seems like gzreadline tries to return the line read from the zip file by modifying its input argument $_ (treated as an output argument, but it is not a true Perl 5 reference variable[1]), but the modified value is not returned to the Perl 6 script.
Here is a possoble workaround:
Create a wrapper module in the curent directory, e.g. ./MyZlibWrapper.pm:
package MyZlibWrapper;
use strict;
use warnings;
use Compress::Zlib ();
use Exporter qw(import);
our #EXPORT = qw(gzopen);
our $VERSION = 0.01;
sub gzopen {
my ( $fn, $mode ) = #_;
my $gz = Compress::Zlib::gzopen( $fn, $mode );
my $self = {gz => $gz};
return bless $self, __PACKAGE__;
}
sub gzreadline {
my ( $self ) = #_;
my $line = "";
my $res = $self->{gz}->gzreadline($line);
return [$res, $line];
}
sub gzclose {
my ( $self ) = #_;
$self->{gz}->gzclose();
}
1;
Then use Inline::Perl5 on this wrapper module instead of Compress::Zlib. For example ./p.p6:
use v6;
use lib:from<Perl5> '.';
use MyZlibWrapper:from<Perl5>;
my $file = 'data.txt.gz';
my $mode = 'rb';
my $gz = gzopen($file, $mode);
loop {
my ($res, $line) = $gz.gzreadline();
last if $res == 0;
print $line;
}
$gz.gzclose();
[1]
In Perl 5 you can modify an input argument that is not a reference, and the change will be reflected in the caller. This is done by modifying entries in the special #_ array variable. For example: sub quote { $_[0] = "'$_[0]'" } $str = "Hello"; quote($str) will quote $str even if $str is not passed by reference.

Inno Setup: UnloadDLL does not work on uninstall

I'm working on an Inno Setup script and during uninstall I call a custom DLL to do some Revert operation. Unfortunately, after uninstall is completed the DLL and it's dependencies were not removed, despite the fact I called UnloadDLL and DeleteFile (which returns False).
Why does UnloadDLL fail?
Is there a possibility to load the DLL dynamic with LoadLibrary? I have seen some functions regarding this, but they are all deprecated. The DLL is built with Visual Studio with C interface.
Here's the code:
function Revert(param: String): cardinal;
external 'Revert#{app}\Revert.dll cdecl delayload uninstallonly';
procedure RevertAll();
var
param: String;
dataDirectory: String;
temp: String;
i: Integer;
begin
dataDirectory := ExpandConstant('{commonappdata}\MyAppData');
StringChangeEx(dataDirectory, '\', '\\', True);
param := '{"dataDirectory": "' + dataDirectory + '", "registryPath" : "SOFTWARE\\MyReg\\Key"}';
Revert(param);
temp := ExpandConstant('{app}\Revert.dll');
for i := 0 to 10 do
begin
UnloadDLL(temp);
Sleep(500);
if DeleteFile(temp) then
break;
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if (CurUninstallStep = usUninstall) then
begin
RevertAll();
end
end;
I would like to add a modest contribution given how many hairs I have lost myself trying to remove a DLL upon software removal.
I tried a few things and ended up doing the following:
case CurUninstallStep of
usUninstall:
begin
Exec('powershell.exe', ExpandConstant('-NoExit -ExecutionPolicy Bypass Start-Job -FilePath {%TEMP}\mySoft\CleanUtils.ps1 -ArgumentList {%TEMP}\mySoft\'), '', SW_HIDE, ewNoWait, ErrorCode);
Exec('powershell.exe', ExpandConstant('-NoExit -ExecutionPolicy Bypass -Command "Start-Job -ScriptBlock {{ Remove-Item -Recurse -Force {%TEMP}\mySoft"'), '', SW_HIDE, ewNoWait, ErrorCode);
end;
end;
A few things to notice:
The UnloadDLL didn't work for me either, despite trying everything I could. The DLL would simply not get removed by InnoSetup
The first Exec invokes a PowerShell shell. Notice the -NoExit. Without that, the job would simply exit without running.
The StartJob creates a detached background process that runs after the uninstaller quits.
The first instruction executes the following PowerShell script:
# Input directory
$temp_dir=$args[0]
Get-ChildItem "$temp_dir" -Force -Filter "*.dll" |
Foreach-Object {
$file = $_.FullName
while (Test-Path($file) )
{
try
{
remove-item -Recurse -Force $file
}
catch
{
Start-Sleep -seconds 5
}
}
}
That script iterates over the list of DLLs and removes all the DLLs in the folder.
The second Exec deletes the parent folder. I could have put that instruction into the Powershell script, but I suspect that I would have ended up with the same problem, that is, PowerShell needing that file, thus never ending.
Regarding that second Exec, the other difference is that the job is invoked via a -Command option. Without that, I would encounter the following error:
Start-Job : Cannot bind parameter 'ScriptBlock'. Cannot convert the "-encodedCommand" value of type "System.String" to type "System.Management.Automation.ScriptBlock".
At line:1 char:11
+ Start-Job -ScriptBlock -encodedCommand IABHAGUAdAAtAFAAcgBvAGMAZQBzAH ...
+ ~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Start-Job], ParameterBindingException
+ FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.PowerShell.Commands.StartJobCommand
This is far from perfect, but this is the best I could come up with after almost one week(!) of trying bunch of different things.
Good luck to all the Inno fellows!
Not sure what is the real problem, but unload the DLL manually with Windows API works:
function GetModuleHandle(moduleName: String): LongWord;
external 'GetModuleHandleW#kernel32.dll stdcall';
function FreeLibrary(module: LongWord): Integer;
external 'FreeLibrary#kernel32.dll stdcall';
var
lib: LongWord;
res: integer;
repeat
lib := GetModuleHandle('Revert.dll');
res := FreeLibrary(lib);
until res = 0;

Backup external file before Inno Setup InstallDelete section

I would make a backup of files and folders before the [InstallDelete] section deletes them
[Files]
Source: "{app}\res_mods\configs\wotstat\cache.json"; \
DestDir: "{app}\_backup\res_mods_{#DateTime}\configs\wotstat\"; \
Flags: external skipifsourcedoesntexist uninsneveruninstall
Source: "{app}\res_mods\0.9.17.1\vehicles\*"; \
DestDir:"{app}\_backup\res_mods_{#DateTime}\0.9.17.1\vehicles\"; \
Flags: external skipifsourcedoesntexist createallsubdirs recursesubdirs uninsneveruninstall
This works fine. But, if i check
[InstallDelete]
Type: filesandordirs; Name: "{app}\mods\*.*"; Tasks: cleanres
Type: filesandordirs; Name: "{app}\res_mods\*.*"; Tasks: cleanres
No files are saved
How I can made it work. Thx
The [InstallDelete] section is processed (as one would expect) before the [Files] section. See the installation order.
You can code the backup in the CurStepChanged(ssInstall) event, that happens before the installation starts:
[Code]
procedure CurStepChanged(CurStep: TSetupStep);
var
SourcePath: string;
DestPath: string;
begin
if CurStep = ssInstall then
begin
SourcePath := ExpandConstant('{app}\res_mods\0.9.17.1\vehicles');
DestPath :=
ExpandConstant('{app}\_backup\res_mods_{#DateTime}\0.9.17.1\vehicles');
Log(Format('Backing up %s to %s before installation', [
SourcePath, DestPath]));
if not ForceDirectories(DestPath) then
begin
Log(Format('Failed to create %s', [DestPath]));
end
else
begin
DirectoryCopy(SourcePath, DestPath);
end;
SourcePath := ExpandConstant('{app}\res_mods\configs\wotstat\cache.json');
DestPath :=
ExpandConstant('{app}\_backup\res_mods_{#DateTime}\configs\wotstat');
if not ForceDirectories(DestPath) then
begin
Log(Format('Failed to create %s', [DestPath]));
end
else
begin
if not FileCopy(SourcePath, DestPath + '\cache.json', False) then
begin
Log(Format('Failed to copy %s', [SourcePath]));
end
else
begin
Log(Format('Backed up %s', [SourcePath]));
end;
end;
end;
end;
The code uses the DirectoryCopy function from the Inno Setup: copy folder, subfolders and files recursively in Code section.

How to use Inno Setup to update a database using .sql script

I'd like to compile a setup that will connect to a remote database using the credentials provided by the user, then install few db components using .sql script.
Is that possible using Inno Setup?
More details:
I'd like to have a custom form, asking the user to enter the database address and credentials, then run a command that will execute an sql script that will update the remote database server.
If the update is successful - complete the installation with success.
This is rather general question - I have a lot of customized setups that should connect to different servers/run different scripts - the idea is to build a generic form that will provide this functionality.
I don't think you can have a completely generic form, as for different servers you may need either a single connection string, or a server name and an (optional) port; for some servers you will use system authentication, for others a user name password tuple.
Having said that I will give you a small demo Inno script that asks for server name and port, user name and password, then makes a few tests, then executes an application that is extracted (by code) to the temp directory and will be deleted by the installer. You can use this as a starting point for your scripts. Having a few of such snippets, and including them in your scripts as necessary will probably be all you need:
[Setup]
AppID=DBUpdateTest
AppName=Test
AppVerName=Test 0.1
AppPublisher=My Company, Inc.
DefaultDirName={pf}\Test
DefaultGroupName=Test
DisableDirPage=yes
DisableProgramGroupPage=yes
OutputBaseFilename=setup
PrivilegesRequired=none
[Files]
Source: "isql.exe"; DestDir: "{tmp}"; Flags: dontcopy
Source: "update_V42.sql"; DestDir: "{tmp}"; Flags: dontcopy
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Code]
var
DBPage: TInputQueryWizardPage;
procedure InitializeWizard;
begin
DBPage := CreateInputQueryPage(wpReady,
'Database Connection Information', 'Which database is to be updated?',
'Please specify the server and the connection credentials, then click Next.');
DBPage.Add('Server:', False);
DBPage.Add('Port:', False);
DBPage.Add('User name:', False);
DBPage.Add('Password:', True);
DBPage.Values[0] := GetPreviousData('Server', '');
DBPage.Values[1] := GetPreviousData('Port', '');
DBPage.Values[2] := GetPreviousData('UserName', '');
DBPage.Values[3] := GetPreviousData('Password', '');
end;
procedure RegisterPreviousData(PreviousDataKey: Integer);
begin
SetPreviousData(PreviousDataKey, 'Server', DBPage.Values[0]);
SetPreviousData(PreviousDataKey, 'Port', DBPage.Values[1]);
SetPreviousData(PreviousDataKey, 'UserName', DBPage.Values[2]);
SetPreviousData(PreviousDataKey, 'Password', DBPage.Values[3]);
end;
function NextButtonClick(CurPageID: Integer): Boolean;
var
ResultCode: Integer;
begin
Result := True;
if CurPageID = DBPage.ID then begin
if DBPage.Values[0] = '' then begin
MsgBox('You must enter the server name or address.', mbError, MB_OK);
Result := False;
end else if DBPage.Values[2] = '' then begin
MsgBox('You must enter the user name.', mbError, MB_OK);
Result := False;
end else if DBPage.Values[3] = '' then begin
MsgBox('You must enter the user password.', mbError, MB_OK);
Result := False;
end else begin
ExtractTemporaryFile('isql.exe');
ExtractTemporaryFile('update_V42.sql');
if Exec(ExpandConstant('{tmp}') + '\isql.exe', '--user ' + DBPage.Values[2]
+ ' --password ' + DBPage.Values[3] + ' --database ' + DBPage.Values[0]
+ ':foo --script update_V42.sql', '',
SW_HIDE, ewWaitUntilTerminated, ResultCode)
then begin
// check ResultCode and set Result accordingly
Result := ResultCode = 0;
end else begin
MsgBox('Database update failed:'#10#10 + SysErrorMessage(ResultCode),
mbError, MB_OK);
Result := False;
end;
end;
end;
end;
Beware: I haven't fully tested this, so there may be more code necessary to properly clean everything up. Error handling is definitely missing!