2

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.

Fabricio Araujo
  • 3,810
  • 3
  • 28
  • 43
  • 1
    Since I have 2 excellent answers, I have an very enjoyable dilemma. I'll check Rob's one since I'm looking more for an explanation than code - which doesn't devalue Remy's answer in any way. Thank you both. – Fabricio Araujo Aug 27 '15 at 20:54

2 Answers2

5

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.

Rob Kennedy
  • 161,384
  • 21
  • 275
  • 467
  • Oh-oh.... That's really a domino bug as it's working the wrong way on 6+ different programs interpreting the command-line the wrong way. Changing the launcher may crash the entire system. So I'll reinforce the programs under my umbrella to behave correctly under the right call. And talk to my boss about it. – Fabricio Araujo Aug 26 '15 at 21:22
  • 2
    If your programs will *only* be started by your launcher, and your launcher will *only* launch those programs you control, then you can leave everything the way it is today. I.e., you can choose to ignore the convention of providing the program name as the first parameter. It's only safe when you control the entire system. It won't support your programs being started in other ways (like Windows Explorer, cmd.exe, or Delphi's debugger). It will only "crash the entire system" if your system has been interpreting command-line parameters incorrectly all along. I think *that's* what you should fix. – Rob Kennedy Aug 26 '15 at 21:30
  • Actually I'll only change the interpretation to allow to start the programs I'm working from the IDE as well from the launcher. It'll help a lot in a debugging task I'm doing here. But thank you and Remy for the detailed answers. Tomorrow I'll check the accepted one. – Fabricio Araujo Aug 26 '15 at 21:35
3

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
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • No, it is not the same. `+ sParam` requires `sParam` to contain leading whitespace, and `+ ' ' + Trim(sParam)` forces a space character even if `sParam` is empty or contains only whitespace. I really meant to use `Trim(' ' + sParam)` so that all unnecessary whitespace is stripped follow the module name if `sParam` is empty or contains only whitespace. I suppose `CmdLine := Trim(AnsiQuotedStr(sExe, '"') + ' ' + sParam);` would also work. – Remy Lebeau Aug 26 '15 at 21:34
  • 1
    Imagine if `sParam` is empty. `sExe + Trim(' ' + sParam)` -> `sExe + Trim(' ' + '')` -> `sExe + Trim(' ')` -> `sExe` by itself. Whereas `sExe + ' ' + Trim(sParam)` -> `sExe + ' ' + Trim('')` -> `sExe + ' '`. An unwanted trailing space. Harmless, for sure, but why include it when it is not needed? And it contributes to the total length of `lpCommandLine`, which has a finite length to begin with, – Remy Lebeau Aug 26 '15 at 21:40
  • I have updated my answer to use `TrimRight(sExe + ' ' + sParam)` instead. No more ambiguity. – Remy Lebeau Aug 26 '15 at 21:43
  • That looks better. I assume that even `Trim(sExe + ' ' + sParam)` should do. – Rudy Velthuis Aug 26 '15 at 21:45
  • Yes, though the first character will always be `"` because of the use of `AnsiQuotedStr()`, so no need to waste processing time trying to trim the left-hand side, only the right-hand side may need it. – Remy Lebeau Aug 26 '15 at 21:46
  • Ok, agreed. `TrimRight` is better. – Rudy Velthuis Aug 26 '15 at 21:47