Fabricio Araujo
Fabricio Araujo

Reputation: 3820

Why the quantity of command-line arguments changes when the application is started by the IDE vs an launcher?

Considering the following command-line arguments

"alfa" "beta" "4"

When I specify the Run>Parameters... for an project I'm working the application shows on Process Explorer as command-line:

"c:\myapp\myapp.exe" "alfa" "beta" "4"

And ParamCount shows 4 parameters. But when I start the same executable from an launcher application (which does access control), Process Explorer shows:

"alfa" "beta" "4"

ParamCount show 3 paramers. The command-line was extracted from the launcher application. In theory it would work, since when started from launcher the application work flawlessly. When started from IDE it tries to do StrToInt on the "4" above, but retrieves just the "beta" parameter instead.

Sample code from launcher application:

var
  StartupInfo: TSTARTUPINFO;
  ProcessInfo: PROCESS_INFORMATION;
  CurrentDirPath: String;
begin
  Result := 0;
  ZeroMemory(@StartupInfo, SizeOf(StartupInfo));
  StartupInfo.cb := SizeOf(StartupInfo);
  DirCorrente := ExtractFilePath(sExe);

  if CreateProcess(PChar(sExe), PChar(sParam), nil, nil, true,
    NORMAL_PRIORITY_CLASS, nil, PChar(CurrentDirPath),
    StartupInfo, ProcessInfo) then

The content of sParam is the command-line arguments above and sExe is the executable path. Why this happens?

NOTE:I already devised how to change the command-line arguments interpretation to be robust for this edge case - the point here is WHY this happens.

Upvotes: 2

Views: 533

Answers (2)

Remy Lebeau
Remy Lebeau

Reputation: 596332

The second parameter is passed as-is as the command-line to the launched process. Most RTLs (including Delphi's) expect the first delimited value in the command-line to be the EXE path. This is stated in the CreateProcess() documentation:

If both lpApplicationName and lpCommandLine are non-NULL, the null-terminated string pointed to by lpApplicationName specifies the module to execute, and the null-terminated string pointed to by lpCommandLine specifies the command line. The new process can use GetCommandLine to retrieve the entire command line. Console processes written in C can use the argc and argv arguments to parse the command line. Because argv[0] is the module name, C programmers generally repeat the module name as the first token in the command line.

The OS handles that automatically when a user launches an executable, but an application has to manage it manually when launching a process via code.

The launcher is not including the EXE path as the first delimited value of the command-line that it passes to CreateProcess(). It needs to do so:

var
  StartupInfo: TSTARTUPINFO;
  ProcessInfo: PROCESS_INFORMATION;
  ...
  CmdLine: String;
begin
  Result := 0;
  ZeroMemory(@StartupInfo, SizeOf(StartupInfo));
  StartupInfo.cb := SizeOf(StartupInfo);
  ...
  CmdLine := TrimRight(AnsiQuotedStr(sExe, '"') + ' ' + sParam);
  ...    
  if CreateProcess(PChar(sExe), PChar(CmdLine), ...) then

In which case, it can omit the first parameter value altogether, per the CreateProcess() documentation:

The lpApplicationName parameter can be NULL. In that case, the module name must be the first white space–delimited token in the lpCommandLine string.

var
  StartupInfo: TSTARTUPINFO;
  ProcessInfo: PROCESS_INFORMATION;
  ...
  CmdLine: String;
begin
  Result := 0;
  ZeroMemory(@StartupInfo, SizeOf(StartupInfo));
  StartupInfo.cb := SizeOf(StartupInfo);
  ...
  CmdLine := TrimRight(AnsiQuotedStr(sExe, '"') + ' ' + sParam);
  ...    
  if CreateProcess(nil, PChar(CmdLine), ...) then

Upvotes: 3

Rob Kennedy
Rob Kennedy

Reputation: 163287

Your launcher program isn't calling CreateProcess properly. Consider this excerpt from the documentation (emphasis added):

If both lpApplicationName and lpCommandLine are non-NULL, the null-terminated string pointed to by lpApplicationName specifies the module to execute, and the null-terminated string pointed to by lpCommandLine specifies the command line. The new process can use GetCommandLine to retrieve the entire command line. Console processes written in C can use the argc and argv arguments to parse the command line. Because argv[0] is the module name, C programmers generally repeat the module name as the first token in the command line.

Ignore the bit about "C programmers"; it applies to everyone writing programs for Windows, regardless of the language.

Your launcher is providing values for both the lpApplicationName and lpCommandLine parameters, but it is not following the convention of repeating the program file name as the first parameter in the command line. Delphi's ParamStr and ParamCount functions know to follow the convention, so they skip the first token on the command line. If the caller didn't follow the convention, then the receiver ends up thinking the intended second parameter is really the first, the third is really the second, and so on.

Upvotes: 5

Related Questions