I want to run a command that the user defined in an INI file.
The commands can be EXE files, or other files (e.g. DOC files) and parameters should be allowed.
Since WinExec() can handle arguments (e.g. "cmd /?"), but ShellExec() can handle Non-EXE files (e.g. "Letter.doc"), I am using a combination of these both.
I am concerned about future Windows versions, because WinExec() is deprecated, and even from the 16 bit era.
Here is my current function:
procedure RunCMD(cmdLine: string; WindowMode: integer);
procedure ShowWindowsErrorMessage(r: integer);
begin
MessageDlg(SysErrorMessage(r), mtError, [mbOK], 0);
end;
var
r, g: Cardinal;
begin
// We need a function which does following:
// 1. Replace the Environment strings, e.g. %SystemRoot% --> ExpandEnvStr
// 2. Runs EXE files with parameters (e.g. "cmd.exe /?") --> WinExec
// 3. Runs EXE files without path (e.g. "calc.exe") --> WinExec
// 4. Runs EXE files without extension (e.g. "calc") --> WinExec
// 5. Runs non-EXE files (e.g. "Letter.doc") --> ShellExecute
// 6. Commands with white spaces (e.g. "C:\Program Files\xyz.exe") must be enclosed in quotes.
cmdLine := ExpandEnvStr(cmdLine);
// TODO: Attention: WinExec() is deprecated, but there is no acceptable replacement
g := WinExec(PChar(cmdLine), WindowMode);
r := GetLastError;
if g = ERROR_BAD_FORMAT then
begin
// e.g. if the user tries to open a non-EXE file
ShellExecute(0, nil, PChar(cmdLine), '', '', WindowMode);
r := GetLastError;
end;
if r <> 0 then ShowWindowsErrorMessage(r);
end;
function ExpandEnvStr(const szInput: string): string;
// http://stackoverflow.com/a/2833147/3544341
const
MAXSIZE = 32768;
begin
SetLength(Result, MAXSIZE);
SetLength(Result, ExpandEnvironmentStrings(pchar(szInput),
#Result[1],length(Result)));
end;
Microsoft recommends using CreateProcess(), but I do not accept it as a real replacement for WinExec().
For example, given following command line:
"C:\Program Files\xyz.exe" /a /b /c
Since ShellExecute() and CreateProcess() require a strict separation of command and arguments, I would have to parse this string myself. Is that really the only way I can go? Has someone written a public available code featuring this functionality?
Additional note: The process should not be attached to the caller. My program will close, right after the command has started.
CreateProcess() is the replacement for WinExec(). The documentation explicitly states as much.
And BTW, the error handling in your original code is completely wrong. You are misusing GetLastError(). In fact, neither WinExec() nor ShellExecute() even report errors with GetLastError() to begin with. So, even if WinExec() or ShellExecute() are successful (and you are not even checking if ShellExecute() succeeds or fails), you risk reporting random errors from earlier API calls.
Try something more like this:
procedure RunCMD(cmdLine: string; WindowMode: integer);
procedure ShowWindowsErrorMessage(r: integer);
var
sMsg: string;
begin
sMsg := SysErrorMessage(r);
if (sMsg = '') and (r = ERROR_BAD_EXE_FORMAT) then
sMsg := SysErrorMessage(ERROR_BAD_FORMAT);
MessageDlg(sMsg, mtError, [mbOK], 0);
end;
var
si: TStartupInfo;
pi: TProcessInformation;
sei: TShellExecuteInfo;
err: Integer;
begin
// We need a function which does following:
// 1. Replace the Environment strings, e.g. %SystemRoot% --> ExpandEnvStr
// 2. Runs EXE files with parameters (e.g. "cmd.exe /?") --> WinExec
// 3. Runs EXE files without path (e.g. "calc.exe") --> WinExec
// 4. Runs EXE files without extension (e.g. "calc") --> WinExec
// 5. Runs non-EXE files (e.g. "Letter.doc") --> ShellExecute
// 6. Commands with white spaces (e.g. "C:\Program Files\xyz.exe") must be enclosed in quotes.
cmdLine := ExpandEnvStr(cmdLine);
{$IFDEF UNICODE}
UniqueString(cmdLine);
{$ENDIF}
ZeroMemory(#si, sizeof(si));
si.cb := sizeof(si);
si.dwFlags := STARTF_USESHOWWINDOW;
si.wShowWindow := WindowMode;
if CreateProcess(nil, PChar(cmdLine), nil, nil, False, 0, nil, nil, si, pi) then
begin
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
Exit;
end;
err := GetLastError;
if (err = ERROR_BAD_EXE_FORMAT) or
(err = ERROR_BAD_FORMAT) then
begin
ZeroMemory(#sei, sizeof(sei));
sei.cbSize := sizeof(sei);
sei.lpFile := PChar(cmdLine);
sei.nShow := WindowMode;
if ShellExecuteEx(#sei) then Exit;
err := GetLastError;
end;
ShowWindowsErrorMessage(err);
end;
Although similar ShellExecute and CreateProcess serve a different purpose.
ShellExecute can open non executable files. It looks up the information in the registry for the associated program for the given file and will execute it. ShellExecute is also great for launching the default web browser and you can pass a URL to it it.
So if you were to pass an TXT file to ShellExecute it would open the associated program such as notepad. However it would fail with CreateProcess.
CreateProcess is a lower level function, that allows you to have better control over the input and output from the the process. For example you can call command line programs that have text output with CreateProcess and capture that output and react according.
Given the concerns you have you will need to use ShellExecute. You will however need to split the command from the parameters. This is would be the first non escaped whitespace character.
I personally rarely call ShellExecute or CreateProcess directly. I tend to use on of the following functions from the JCL that wrap these functions.
JclMiscel.pas
CreateDosProcessRedirected
WinExec32
WinExec32AndWait
WinExecAndRedirectOutput
CreateProcessAsUser
CreateProcessAsUserEx
JclShell.pas
ShellExecEx
ShellExec
ShellExecAndWwait
RunAsAdmin
JclSysUtils.pas
Execute (8 Overloaded versions and is the one I use the most)
Related
From my Win32 app I'm reading and writing HKEY_CURRENT_USER\Software\Embarcadero\BDS\9.0\History Lists\hlRunParameters, that is where the Delphi XE2 IDE writes run-time parameters.
This is the write code:
procedure TFrmCleanIDEParams.BtnWriteClick(Sender: TObject);
var
lReg : TRegistry;
lValue,
lKey : String;
i,
lNrToWrite,
lNrRegVals: Integer;
begin
.....
lKey := Trim(EdtRegKey.Text); // '\Software\Embarcadero\BDS\9.0\History Lists\hlRunParameters'
if lKey = '' then Exit;
if lKey[1] = '\' then lKey := Copy(lKey,2);
lReg := TRegistry.Create(KEY_READ or KEY_WRITE);
lReg.RootKey := HKEY_CURRENT_USER;
if not lReg.OpenKey(lKey,false) then
begin
MessageDlg('Key not found', mtError, mbOKCancel, 0);
Exit;
end;
if not lReg.ValueExists('Count') then
begin
MessageDlg('Value ''Count'' not found', mtError, mbOKCancel, 0);
Exit;
end;
lNrRegVals := lReg.ReadInteger('Count');
lNrToWrite := CLBParams.Items.Count; // TCheckListBox
lReg.WriteInteger('Count',lNrToWrite);
for i := 0 to lNrToWrite-1 do
begin
lValue := 'Item' + IntToStr(i);
lReg.WriteString(lValue,CLBParams.Items[i]);
end;
// Remove the rest:
for i := lNrToWrite to lNrRegVals-1 do
lReg.DeleteValue('Item' + IntToStr(i));
end;
Issues:
In RegEdit I see the key contents changing as expected, but the Delphi IDE does not pick up these changes
Some time (reboot?) later the HKEY_CURRENT_USER key has its old values
I think several things could be the reason, but I'm not sure which ones to attack:
I should not use HKEY_CURRENT_USER, but HKEY_USERS. If this is the case, how do I then get the proper S-1-5-etc that I need to use?
It's a Windows 7 64-bit issue, although both my program and the Delphi IDE are 32 bit. (How) do I then need to change the TRegistry.Create?
I read this Delphi: Read 64-bits registry key from 32-bits process post but that still does not tell me if/when to use different 'access keys'.
Do I always need to use this KEY_WOW64_64KEY value regardless of my app being 32/64 bit? I see that HKEY_CURRENT_USER\Software is shared, not redirected. (How) do I need to treat these differently?
BTW UAC is off, it would be nice if my code worked with UAC on too.
The Delphi IDE will only read these values at start up. But you must make sure that you write the registry values after the IDE has finished writing to them.
You should be using HKEY_CURRENT_USER.
You should not be using an alternate registry view flag because that part of the registry is shared.
UAC won't have any impact here because HKEY_CURRENT_USER is writeable for the standard user token.
The only explanation that makes sense is that another process is modifying the values. My guess is that the Delphi IDE is that process.
I have a rather strange problem:
My program uses ShellExecuteEx to start another program. This works fine when my program runs stand alone, but fails when it gets started from the Delphi IDE where "Started from the Delphi IDE" means either:
Run -> Run (inside the debugger)
Run -> Run without Debugging
ShellExecuteEx returns false and RaiseLastOsError results in the following error message:
System Error. Code: -2146368396.
The COM+ registry database detected a system error.
The same program has another problem that is probably caused by the same issue: The TOpenDialog.Exeucte and TSaveDialog.Execute methods don't do anything. No dialog is shown and the functions return false. Again this works fine when the program runs stand alone. From googling I have found that this is a COM related issue as well.
My program does not contain any COM code, only those functions that the Delphi RTL/VCL automatically calls.
I have placed a breakpoint on CoInitialize and CoInitializeEx and found only one call to CoInitialize which comes from ComObj.InitComObj. There seems to be nothing wrong there.
Here is the code that fails:
function ShellExecEx(const Filename: string; const Parameters: string;
const Verb: string; CmdShow: Integer; _ShowAssociateDialog: Boolean = False): boolean;
var
Sei: TShellExecuteInfo;
begin
FillChar(Sei, SizeOf(Sei), #0);
Sei.cbSize := SizeOf(Sei);
Sei.FMask := SEE_MASK_DOENVSUBST;
if not _ShowAssociateDialog then
Sei.FMask := Sei.FMask or SEE_MASK_FLAG_NO_UI;
Sei.lpFile := PChar(Filename);
if Parameters <> '' then
Sei.lpParameters := PChar(Parameters)
else
Sei.lpParameters := nil;
if Verb <> '' then
Sei.lpVerb := PChar(Verb)
else
Sei.lpVerb := nil;
Sei.nShow := CmdShow;
Result := ShellExecuteEx(#Sei);
end;
// called as:
lEditorFilename := 'C:\Program Files (x86)\Notepad++\notepad++.exe';
lParameterStr := '"D:\source\EditorUi.dfm" -n1540';
if not ShellExecEx(lEditorFilename, lParameterStr, '', SW_SHOWNORMAL) then
RaiseLastOSError;
This is a 32 bit Delphi XE2 program running on Windows 8.1 64 bit.
Any hints what might cause this?
EDIT:
Following the question from David Heffernan regarding env substitution I removed the additional environment variable
lang=de
I had put into the Run -> Parameters dialog to test the German translations. And all of a sudden both effects described above went away. Putting it back, or adding just any environment variable (eg. test=test), reproduced them reliably.
WTF?
i am working on an application that uses FastMM4, from sourceforge.net.
So i have added the FastMM4.pas to the uses clause right at the beginning. In the application i need to run a batch file after FinalizeMemoryManager; in the finalization of unit FastMM4; like this
initialization
RunInitializationCode;
finalization
{$ifndef PatchBCBTerminate}
FinalizeMemoryManager;
RunTheBatFileAtTheEnd; //my code here..calling a procedure
{$endif}
end.
then my code for RunTheBatFileAtTheEnd is :
procedure RunTheBatFileAtTheEnd;
begin
//some code here....
sFilePaTh:=SysUtils.ExtractFilePath(applicaTname)+sFileNameNoextension+'_log.nam';
ShellExecute(applcatiOnHAndle,'open', pchar(sExeName),pchar(sFilePaTh), nil, SW_SHOWNORMAL) ;
end;
For this i need to use SysUtils,shellapi in the uses clause of fastmm4 unit. But using them
this message comes
But if i remove SysUtils,shellapi from the uses it works.
I still need all the features of fastmm4 installed but with SysUtils,shellapi, fastmm4 is not installed
I have a unit of my own but its finalization is executed before fastmm4 finalization.
can anyone tell me can how to fix this problem?
EDIT- 1
unit FastMM4;
//...
...
implementation
uses
{$ifndef Linux}Windows,{$ifdef FullDebugMode}{$ifdef Delphi4or5}ShlObj,{$else}
SHFolder,{$endif}{$endif}{$else}Libc,{$endif}FastMM4Messages,SysUtils,shellapi;
my application
program memCheckTest;
uses
FastMM4,
EDIT-2 :
(after #SertacAkyuz answer),i removed SysUtils and it worked , but i still need to run the batch file to open an external application through RunTheBatFileAtTheEnd. The Reason is ..i want a external application to run only after FastMM4 as been out of the finalization. The sExeName is the application that will run the file sFilePaTh(.nam) . can any one tell how to do this? without uninstalling FastMM4.
FastMM checks to see if the default memory manager is set before installing its own by a call to IsMemoryManagerSet function in 'system.pas'. If the default memory manager is set, it declines setting its own memory manager and displays the message shown in the question.
The instruction in that message about 'fastmm4.pas' should be the first unit in the project's .dpr file has the assumption that 'fastmm4.pas' itself is not modified.
When you modify the uses clause of 'fastmm4.pas', if any of the units that's included in the uses clause has an initialization section, than that section of code have to run before the initialization section of 'fastmm4.pas'. If that code requires allocating/feeing memory via RTL, then the default memory manager is set.
Hence you have to take care changing 'fastmm4.pas' to not to include any such unit in the uses clause, like 'sysutils.pas'.
Below sample code (no error checking, file checking etc..) shows how can you launch FastMM's log file with Notepad (provided the log file exists) without allocating any memory:
var
CmdLine: array [0..300] of Char; // increase as needed
Len: Integer;
SInfo: TStartupInfo;
PInfo: TProcessInformation;
initialization
... // fastmm code
finalization
{$ifndef PatchBCBTerminate}
FinalizeMemoryManager; // belongs to fastmm
// Our application is named 'Notepad' and the path is defined in AppPaths
CmdLine := 'Notepad "'; // 9 Chars (note the opening quote)
Len := windows.GetModuleFileName(0, PChar(#CmdLine[9]), 260) + 8;
// assumes the executable has an extension.
while CmdLine[Len] <> '.' do
Dec(Len);
CmdLine[Len] := #0;
lstrcat(CmdLine, '_MemoryManager_EventLog.txt"'#0); // note the closing quote
ZeroMemory(#SInfo, SizeOf(SInfo));
SInfo.cb := SizeOf(SInfo);
CreateProcess(nil, CmdLine, nil, nil, False,
NORMAL_PRIORITY_CLASS, nil, nil, sInfo, pInfo);
{$endif}
end.
I agree with Sertac's answer, but also would like to give a recommendation, if you insist on using SysUtils.pas. The answer is don't use it, and extract what you need out of it and put it in your own copy. Here's what you would need below - ExtractFilePath used LastDeliminator, which used StrScan, and also 2 constants, so I copied them into this new unit and named it MySysUtils.pas.
This is also widely used for people who don't want to have a bunch of extra code compiled which they will never use (You would have to be absolutely sure it's not used anywhere in any units though).
unit MySysUtils;
interface
const
PathDelim = '\';
DriveDelim = ':';
implementation
function StrScan(const Str: PWideChar; Chr: WideChar): PWideChar;
begin
Result := Str;
while Result^ <> #0 do begin
if Result^ = Chr then
Exit;
Inc(Result);
end;
if Chr <> #0 then
Result := nil;
end;
function LastDelimiter(const Delimiters, S: string): Integer;
var
P: PChar;
begin
Result := Length(S);
P := PChar(Delimiters);
while Result > 0 do begin
if (S[Result] <> #0) and (StrScan(P, S[Result]) <> nil) then
Exit;
Dec(Result);
end;
end;
function ExtractFilePath(const FileName: string): string;
var
I: Integer;
begin
I := LastDelimiter(PathDelim + DriveDelim, FileName);
Result := Copy(FileName, 1, I);
end;
end.
I need to call an external program from Delphi 2006 code with a long list of arguments, specifically to concatenate mutiple PDFs into one file using PDFTK. The full string to be executed has over 512 characters, but both WinExec and ShellExecute have a 512 character limit.
Are there any alternatives to these procedures that have much larger limits?
Just use a temporary BATCH file, containing the commands to be executed.
This will allow also some enhanced features, like calling several PDFTK instance in a row, add backup or copy of files, just in the same process.
Run the batch as SW_SHOWMINIMIZED to have no black console window pop up.
Just found this #SwissDelphiCentre, which seems to work nicely:
procedure ShellExecute_AndWait(FileName: string; Params: string);
var
exInfo: TShellExecuteInfo;
Ph: DWORD;
begin
FillChar(exInfo, SizeOf(exInfo), 0);
with exInfo do
begin
cbSize := SizeOf(exInfo);
fMask := SEE_MASK_NOCLOSEPROCESS or SEE_MASK_FLAG_DDEWAIT;
Wnd := GetActiveWindow();
ExInfo.lpVerb := 'open';
ExInfo.lpParameters := PChar(Params);
lpFile := PChar(FileName);
nShow := SW_SHOWNORMAL;
end;
if ShellExecuteEx(#exInfo) then
Ph := exInfo.HProcess
else
begin
ShowMessage(SysErrorMessage(GetLastError));
Exit;
end;
while WaitForSingleObject(ExInfo.hProcess, 50) <> WAIT_OBJECT_0 do
Application.ProcessMessages;
CloseHandle(Ph);
end;
There are some limits on the length of the names passed to ShellExecute, but these are typically greater than 512 characters. It seems you just need to dynamically allocate the names rather than using a static char array.
If you want to go to the ultimate command line length then you can use CreateProcess which has a limit of 32,768 characters.
As another option you could consider writing the list of arguments to a temporary file. Then you would modify the external program so that it is capable of being passed the path to that file as its command line argument. You would obviously need to also modify the external program so that it could read the file and obtain the long list of files from the temporary file.
I have run program with command-line parameters. How can i wait for it to finish running?
This is my answer : (Thank you all)
uses ShellAPI;
function TForm1.ShellExecute_AndWait(FileName: string; Params: string): bool;
var
exInfo: TShellExecuteInfo;
Ph: DWORD;
begin
FillChar(exInfo, SizeOf(exInfo), 0);
with exInfo do
begin
cbSize := SizeOf(exInfo);
fMask := SEE_MASK_NOCLOSEPROCESS or SEE_MASK_FLAG_DDEWAIT;
Wnd := GetActiveWindow();
exInfo.lpVerb := 'open';
exInfo.lpParameters := PChar(Params);
lpFile := PChar(FileName);
nShow := SW_SHOWNORMAL;
end;
if ShellExecuteEx(#exInfo) then
Ph := exInfo.hProcess
else
begin
ShowMessage(SysErrorMessage(GetLastError));
Result := true;
exit;
end;
while WaitForSingleObject(exInfo.hProcess, 50) <> WAIT_OBJECT_0 do
Application.ProcessMessages;
CloseHandle(Ph);
Result := true;
end;
If I understand your question correctly, you want to execute program in command-line and capture its output in your application rather than in console window. To do so, you can read the output using pipes. Here is an example source code:
Capture the output from a DOS (command/console) Window
Using DSiWin32:
sl := TStringList.Create;
if DSiExecuteAndCapture('cmd.exe /c dir', sl, 'c:\test', exitCode) = 0 then
// exec error
else
// use sl
sl.Free;
Ok, getting the command-line parameters, you use
ParamCount : returns the number of parameters passed to the program on the command-line.
ParamStr : returns a specific parameter, requested by index.
Running Dephi Applications With Parameters
Now, if what you meant is reading and writing to the console, you use
WriteLn : writes a line of text to the console.
ReadLn : reads a line of text from the console as a string.
Delphi Basics
If what you want is to execute a command-line executable, and get the response that this exe writes to the console, the easiest way could be to call the exe from a batch file and redirect the output to another file using >, and then read that file.
For example, if you need to execute the "dir" command and get its output you could have a batch file called getdir.bat that contains the following:
#echo off
dir c:\users\myuser\*.* > output.txt
you could exec that batch file using the API function ShellExecute. You can read about it http://delphi.about.com/od/windowsshellapi/a/executeprogram.htm
Then you can read output file, even using something like a TStringList:
var
output: TStringList;
begin
output := TStringList.Create();
output.LoadFromFile('output.txt');
...