I would like to know if it's possible to run a command and get its exit code. I have seen that it's possible to capture the stdout of the process, but I have not found anything about the exit code. Is it possible to do this on Linux?
Delphi version: 10.4
OS: Ubuntu 18.04
This is the unit I use to run the command and get its output:
unit TestRunCommand;
interface
uses
System.SysUtils,
Posix.Base,
Posix.Fcntl;
type
TStreamHandle = pointer;
function popen(const command: MarshaledAString; const _type: MarshaledAString): TStreamHandle; cdecl; external libc name _PU + 'popen';
function pclose(filehandle: TStreamHandle): int32; cdecl; external libc name _PU + 'pclose';
function fgets(buffer: pointer; size: int32; Stream: TStreamHandle): pointer; cdecl; external libc name _PU + 'fgets';
function RunCommand(const acommand: MarshaledAString): String; forward;
implementation
function RunCommand(const acommand: MarshaledAString): String;
// run a linux shell command and return output
var
handle: TStreamHandle;
data: array [0 .. 511] of uint8;
function bufferToString(buffer: pointer; maxSize: uint32): string;
var
cursor: ^uint8;
endOfBuffer: nativeuint;
begin
if not assigned(buffer) then
exit;
cursor := buffer;
endOfBuffer := nativeuint(cursor) + maxSize;
while (nativeuint(cursor) < endOfBuffer) and (cursor^ <> 0) do
begin
result := result + chr(cursor^);
cursor := pointer(succ(nativeuint(cursor)));
end;
end;
begin
result := '';
handle := popen(acommand, 'r');
try
while fgets(#data[0], sizeof(data), handle) <> nil do
begin
result := bufferToString(#data[0], sizeof(data));
end;
finally
pclose(handle);
end;
end;
end.
Related
Is it possible to get an output of an Exec'ed executable?
I want to show the user an info query page, but show the default value of MAC address in the input box. Is there any other way to achieve this?
Yes, use redirection of the standard output to a file:
[Code]
function NextButtonClick(CurPage: Integer): Boolean;
var
TmpFileName, ExecStdout: string;
ResultCode: integer;
begin
if CurPage = wpWelcome then begin
TmpFileName := ExpandConstant('{tmp}') + '\ipconfig_results.txt';
Exec('cmd.exe', '/C ipconfig /ALL > "' + TmpFileName + '"', '', SW_HIDE,
ewWaitUntilTerminated, ResultCode);
if LoadStringFromFile(TmpFileName, ExecStdout) then begin
MsgBox(ExecStdout, mbInformation, MB_OK);
{ do something with contents of file... }
end;
DeleteFile(TmpFileName);
end;
Result := True;
end;
Note that there may be more than one network adapter, and consequently several MAC addresses to choose from.
I had to do the same (execute command line calls and get the result) and came up with a more general solution.
It also fixes strange bugs if quoted paths are used in the actual calls by using the /S flag for cmd.exe.
// Exec with output stored in result.
// ResultString will only be altered if True is returned.
function ExecWithResult(
Filename, Params, WorkingDir: String; ShowCmd: Integer;
Wait: TExecWait; var ResultCode: Integer; var ResultString: String): Boolean;
var
TempFilename: String;
Command: String;
ResultStringAnsi: AnsiString;
begin
TempFilename := ExpandConstant('{tmp}\~execwithresult.txt');
// Exec via cmd and redirect output to file.
// Must use special string-behavior to work.
Command :=
Format('"%s" /S /C ""%s" %s > "%s""', [
ExpandConstant('{cmd}'), Filename, Params, TempFilename]);
Result :=
Exec(ExpandConstant('{cmd}'), Command, WorkingDir, ShowCmd, Wait, ResultCode);
if not Result then
Exit;
LoadStringFromFile(TempFilename, ResultStringAnsi); // Cannot fail
// See https://stackoverflow.com/q/20912510/850848
ResultString := ResultStringAnsi;
DeleteFile(TempFilename);
// Remove new-line at the end
if (Length(ResultString) >= 2) and
(ResultString[Length(ResultString) - 1] = #13) and
(ResultString[Length(ResultString)] = #10) then
Delete(ResultString, Length(ResultString) - 1, 2);
end;
Usage:
Success :=
ExecWithResult('ipconfig', '/all', '', SW_HIDE, ewWaitUntilTerminated,
ResultCode, ExecStdout) and
(ResultCode = 0);
The result can also be loaded into a TStringList object to get all lines:
Lines := TStringList.Create;
Lines.Text := ExecStdout;
// ... some code ...
Lines.Free;
I have a software which requires the default browser installed on user computer.
Is there a way that I can get it?
Thanks
An solution that correctly works on modern versions of Windows cannot be based on association with http protocol, as that's no longer reliable. It should rather be based on a solution like the answer by #GregT to How to determine the Windows default browser (at the top of the start menu).
So something like:
function GetBrowserCommand: string;
var
UserChoiceKey: string;
HtmlProgId: string;
begin
UserChoiceKey :=
'Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.html\UserChoice';
if RegQueryStringValue(HKCU, UserChoiceKey, 'ProgId', HtmlProgId) then
begin
Log(Format('ProgID to registered for .html is [%s].', [HtmlProgId]));
if RegQueryStringValue(HKCR, HtmlProgId + '\shell\open\command', '', Result) then
begin
Log(Format('Command for ProgID [%s] is [%s].', [HtmlProgId, Result]));
end;
end;
{ Fallback for old version of Windows }
if Result = '' then
begin
if RegQueryStringValue(HKCR, 'http\shell\open\command', '', Result) then
begin
Log(Format('Command registered for http: [%s].', [Result]));
end;
end;
end;
If you want to extract browser path from the command, use a code like:
function ExtractProgramPath(Command: string): string;
var
P: Integer;
begin
if Copy(Command, 1, 1) = '"' then
begin
Delete(Command, 1, 1);
P := Pos('"', Command);
end
else P := 0;
if P = 0 then
begin
P := Pos(' ', Command);
end;
Result := Copy(Command, 1, P - 1);
end;
(based on Executing UninstallString in Inno Setup)
Take this:
function GetBrowser() : String;
var
RegistryEntry: String;
Browser: String;
Limit: Integer ;
begin
if RegQueryStringValue(HKEY_CLASSES_ROOT, 'http\shell\open\command', '', RegistryEntry) then
begin
Limit := Pos('.exe' ,RegistryEntry)+ Length('.exe');
Browser := Copy(RegistryEntry, 1, Limit );
MsgBox('Your browser: ' + Browser , mbInformation, MB_OK);
end;
end;
I am using PrivilegesRequired=lowest in my Inno Setup script. If setup is running elevated, i.e. IsAdminLoggedOn or IsPowerUserLoggedOn reports TRUE, how can I determine if the elevated user account is the same account from which setup was launched?
My script can do different things accordingly.
You can use WTSQuerySessionInformation to retrieve an account username for the current Windows logon session.
function WTSQuerySessionInformation(
hServer: THandle; SessionId: Cardinal; WTSInfoClass: Integer;
var pBuffer: DWord; var BytesReturned: DWord): Boolean;
external 'WTSQuerySessionInformationW#wtsapi32.dll stdcall';
procedure WTSFreeMemory(pMemory: DWord);
external 'WTSFreeMemory#wtsapi32.dll stdcall';
procedure RtlMoveMemoryAsString(Dest: string; Source: DWord; Len: Integer);
external 'RtlMoveMemory#kernel32.dll stdcall';
const
WTS_CURRENT_SERVER_HANDLE = 0;
WTS_CURRENT_SESSION = -1;
WTSUserName = 5;
function GetCurrentSessionUserName: string;
var
Buffer: DWord;
BytesReturned: DWord;
QueryResult: Boolean;
begin
QueryResult :=
WTSQuerySessionInformation(
WTS_CURRENT_SERVER_HANDLE, WTS_CURRENT_SESSION, WTSUserName, Buffer,
BytesReturned);
if not QueryResult then
begin
Log('Failed to retrieve username');
Result := '';
end
else
begin
SetLength(Result, (BytesReturned div 2) - 1);
RtlMoveMemoryAsString(Result, Buffer, BytesReturned);
WTSFreeMemory(Buffer);
Log(Format('Retrieved username "%s"', [Result]));
end;
end;
(The code is for Unicode version of Inno Setup – The only version as of Inno Setup 6).
You can then compare the result against GetUserNameString.
You may need to add a domain name into the comparison.
If you need the full account name of the current user (e.g., authority\username format), you can use the GetUserNameExW Windows API function. The below snippet demonstrates how to call this function from Inno Setup:
const
ERROR_MORE_DATA = 234;
function GetUserNameExW(NameFormat: Integer; lpNameBuffer: string; var nSize: DWORD): Boolean;
external 'GetUserNameExW#secur32.dll stdcall';
function GetFullUserName(): string;
var
NumChars: DWORD;
OutStr: string;
begin
result := '';
NumChars := 0;
if (not GetUserNameExW(2, '', NumChars)) and (DLLGetLastError() = ERROR_MORE_DATA) then
begin
SetLength(OutStr, NumChars);
if GetUserNameExW(2, OutStr, NumChars) then
result := Copy(OutStr, 1, NumChars);
end;
end;
(The value 2 passed to the first parameter (NameFormat) in the GetUserNameExW function corresponds to NameSamCompatible in the EXTENDED_NAME_FORMAT enumeration.)
In case of an upgrade / re-installation, is there a way to discard the /TYPE and /COMPONENTS parameter value passed on the command line to the installer and instead use the previously used values ?
I can read the values used earlier from Registry (or alternatively make out the details based on existence of files assuming they have not been manually altered)
I have read the following threads and can disable the "Select Components" page in UI mode
Inno Setup Skip "Select Components" page when /Type command-line parameter is specified
InnoSetup: Disable components page on upgrade
However, if the aforesaid parameters are passed from command line, they seem to override the defaults.
You cannot discard them.
What you can do is to check if those parameters were provided and if they were:
Re-launch the installer without them (show below), or
Read the previously selected type and components from registry and re-set the controls accordingly.
Re-launching the installer without /TYPE= and /COMPONENTS=
const
UninstallKey =
'Software\Microsoft\Windows\CurrentVersion\Uninstall\' +
'{#SetupSetting("AppId")}_is1';
function IsUpgrade: Boolean;
var
Value: string;
begin
Result :=
(RegQueryStringValue(HKLM, UninstallKey, 'UninstallString', Value) or
RegQueryStringValue(HKCU, UninstallKey, 'UninstallString', Value)) and
(Value <> '');
end;
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;
lpParameters: string; lpDirectory: string; nShowCmd: Integer): THandle;
external 'ShellExecuteW#shell32.dll stdcall';
function InitializeSetup(): Boolean;
var
Params, S: string;
Relaunch: Boolean;
I, RetVal: Integer;
begin
Result := True;
if IsUpgrade then
begin
Relaunch := False;
// Collect current instance parameters
for I := 1 to ParamCount do
begin
S := ParamStr(I);
if (CompareText(Copy(S, 1, 7), '/TYPES=') = 0) or
(CompareText(Copy(S, 1, 12), '/COMPONENTS=') = 0) then
begin
Log(Format('Will re-launch due to %s', [S]));
Relaunch := True;
end
else
begin
// Unique log file name for the child instance
if CompareText(Copy(S, 1, 5), '/LOG=') = 0 then
begin
S := S + '-sub';
end;
// Do not pass our /SL5 switch
// This should not be needed since Inno Setup 6.2,
// see https://groups.google.com/g/innosetup/c/pDSbgD8nbxI
if CompareText(Copy(S, 1, 5), '/SL5=') <> 0 then
begin
Params := Params + AddQuotes(S) + ' ';
end;
end;
end;
if not Relaunch then
begin
Log('No need to re-launch');
end
else
begin
Log(Format('Re-launching setup with parameters [%s]', [Params]));
RetVal :=
ShellExecute(0, '', ExpandConstant('{srcexe}'), Params, '', SW_SHOW);
Log(Format('Re-launching setup returned [%d]', [RetVal]));
Result := (RetVal > 32);
// if re-launching of this setup succeeded, then...
if Result then
begin
Log('Re-launching succeeded');
// exit this setup instance
Result := False;
end
else
begin
Log(Format('Elevation failed [%s]', [SysErrorMessage(RetVal)]));
end;
end;
end;
end;
The code is for Unicode version of Inno Setup.
The code can be further improved to keep the master installer waiting for the child installer to complete. When can make a difference, particularly if the installer is executed by some automatic deployment process.
Is it possible to get an output of an Exec'ed executable?
I want to show the user an info query page, but show the default value of MAC address in the input box. Is there any other way to achieve this?
Yes, use redirection of the standard output to a file:
[Code]
function NextButtonClick(CurPage: Integer): Boolean;
var
TmpFileName, ExecStdout: string;
ResultCode: integer;
begin
if CurPage = wpWelcome then begin
TmpFileName := ExpandConstant('{tmp}') + '\ipconfig_results.txt';
Exec('cmd.exe', '/C ipconfig /ALL > "' + TmpFileName + '"', '', SW_HIDE,
ewWaitUntilTerminated, ResultCode);
if LoadStringFromFile(TmpFileName, ExecStdout) then begin
MsgBox(ExecStdout, mbInformation, MB_OK);
{ do something with contents of file... }
end;
DeleteFile(TmpFileName);
end;
Result := True;
end;
Note that there may be more than one network adapter, and consequently several MAC addresses to choose from.
I had to do the same (execute command line calls and get the result) and came up with a more general solution.
It also fixes strange bugs if quoted paths are used in the actual calls by using the /S flag for cmd.exe.
// Exec with output stored in result.
// ResultString will only be altered if True is returned.
function ExecWithResult(
Filename, Params, WorkingDir: String; ShowCmd: Integer;
Wait: TExecWait; var ResultCode: Integer; var ResultString: String): Boolean;
var
TempFilename: String;
Command: String;
ResultStringAnsi: AnsiString;
begin
TempFilename := ExpandConstant('{tmp}\~execwithresult.txt');
// Exec via cmd and redirect output to file.
// Must use special string-behavior to work.
Command :=
Format('"%s" /S /C ""%s" %s > "%s""', [
ExpandConstant('{cmd}'), Filename, Params, TempFilename]);
Result :=
Exec(ExpandConstant('{cmd}'), Command, WorkingDir, ShowCmd, Wait, ResultCode);
if not Result then
Exit;
LoadStringFromFile(TempFilename, ResultStringAnsi); // Cannot fail
// See https://stackoverflow.com/q/20912510/850848
ResultString := ResultStringAnsi;
DeleteFile(TempFilename);
// Remove new-line at the end
if (Length(ResultString) >= 2) and
(ResultString[Length(ResultString) - 1] = #13) and
(ResultString[Length(ResultString)] = #10) then
Delete(ResultString, Length(ResultString) - 1, 2);
end;
Usage:
Success :=
ExecWithResult('ipconfig', '/all', '', SW_HIDE, ewWaitUntilTerminated,
ResultCode, ExecStdout) and
(ResultCode = 0);
The result can also be loaded into a TStringList object to get all lines:
Lines := TStringList.Create;
Lines.Text := ExecStdout;
// ... some code ...
Lines.Free;