【问题标题】:Acceptable replacement for WinExec()?WinExec() 的可接受替代品?
【发布时间】:2015-09-26 22:31:09
【问题描述】:

我想运行用户在 INI 文件中定义的命令。

命令可以是EXE文件,也可以是其他文件(如DOC文件)和参数。

由于 WinExec() 可以处理参数(例如“cmd /?”),但 ShellExec() 可以处理非 EXE 文件(例如“Letter.doc”),因此我将两者结合使用。

我担心未来的 Windows 版本,因为 WinExec() 已被弃用,甚至从 16 位时代开始。

这是我目前的功能:

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 建议使用 CreateProcess(),但我不接受它作为 WinExec() 的真正替代品。

例如,给定以下命令行:

"C:\Program Files\xyz.exe" /a /b /c

由于 ShellExecute() 和 CreateProcess() 需要严格分离命令和参数,我必须自己解析这个字符串。这真的是我唯一可以走的路吗?是否有人编写了具有此功能的公开可用代码?

附加说明:进程不应附加到调用者。我的程序将在命令启动后立即关闭。

【问题讨论】:

  • 请注意,CreateProcess 要求您将参数与命令分开。检查文档。推荐的做法是为第一个参数传递NULL,在第二个参数中传递整个命令行。
  • “自己解析这个字符串” - 不是真的。您需要做的就是找到第一个未加引号的空格,以了解命令的结束位置和参数的开始位置。 ShellExecuteEx 可以满足您的所有需求。
  • @HarryJohnston 我用 CreateProcess() 试过了,但它似乎不接受非 EXE 文件。例如,INI 文件不会与其关联的应用程序一起打开:pastebin.com/ye7XMSCm
  • WinExec 也没有,是吗?但乔纳森是对的,将命令字符串拆分为 ShellExecute 或 ShellExecuteEx 所需的两部分并不难。
  • CreateProcess 替换 WinExec。文档清楚地说明了这一点。您的错误检查严重损坏。 ShellExecute 无论如何都不支持正确检查错误。使用 ShellExecuteEx。需要更多地关注文档。

标签: delphi winapi


【解决方案1】:

CreateProcess()WinExec() 的替代品。文档明确说明了这一点。

顺便说一句,您原始代码中的错误处理是完全错误的。你在滥用GetLastError()。事实上,WinExec()ShellExecute() 都没有报告 GetLastError() 开始的错误。因此,即使 WinExec()ShellExecute() 成功(并且您甚至没有检查 ShellExecute() 是成功还是失败),您也可能会报告早期 API 调用的随机错误。

试试这样的:

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;

【讨论】:

  • 非常感谢您提供这个不错的解决方案! GetLastError 确实在我的系统上工作,但它可能只是一个应该避免的无证行为,当然。我发现 CreateProcess() 返回 BAD_EXE_FORMAT 而不是 BAD_FORMAT - 令人惊讶的是,SysErrorMessage(BAD_EXE_FORMAT) 返回一个空字符串(在德语 Windows 10 上)。
  • 顺便问一下,你为什么只在 Unicode 中使用UniqueString?我从来没有完全理解UniqueString() [即使在阅读了 Embarcadero 文档之后] - 据我所知,ExpandEnvStr() 确实创建了一个字符串 [=WideString],并且通过将字符串作为函数结果返回,引用计数器应该仍然是 1 ?
  • GetLastError 在您的代码中所写的任何地方都不起作用。
  • @DavidHeffernan 我说的是 Remy 的代码。 CreateProcess 提供正确的 GetLastError ,但错误 193 (BAD_EXE_FORMAT) 的 SysErrorMessage 在德文版 Windows 中为空。
  • 我的评论是为了回应你的说法 GetLastError 确实在我的系统上工作,但它可能只是一个应该避免的无证行为,当然。错误检查您问题中的代码从来都不是正确的,也从来没有工作过。如果它似乎有效,那是巧合。
【解决方案2】:

虽然相似的ShellExecuteCreateProcess 有不同的用途。

ShellExecute 可以打开非可执行文件。它在注册表中查找给定文件的关联程序的信息并将执行它。 ShellExecute 也非常适合启动默认 Web 浏览器,您可以将 URL 传递给它。

因此,如果您要将 TXT 文件传递​​给 ShellExecute,它将打开相关的程序,例如记事本。但是它会因 CreateProcess 失败。

CreateProcess 是一个较低级别的函数,它允许您更好地控制进程的输入和输出。例如,您可以使用 CreateProcess 调用具有文本输出的命令行程序并捕获该输出并做出相应的反应。

考虑到您的顾虑,您将需要使用 ShellExecute。但是,您需要将命令与参数分开。这将是第一个非转义的空白字符。

我个人很少直接调用 ShellExecute 或 CreateProcess。我倾向于使用包装这些函数的JCL 中的以下函数。

JclMiscel.pas

  • CreateDosProcessRedirected
  • WinExec32
  • WinExec32AndWait
  • WinExecAndRedirectOutput
  • CreateProcessAsUser
  • CreateProcessAsUserEx

JclShell.pas

  • ShellExecEx
  • ShellExec
  • ShellExecAndWwait
  • RunAsAdmin

JclSysUtils.pas

  • 执行(8 个重载版本,是我使用最多的版本)

【讨论】:

  • 感谢您的推荐。对于目前的项目,我希望尽可能少使用第三方代码,但是我会在接下来的项目中关注 JCL,因为我经常听说它具有食物功能; +1
猜你喜欢
  • 2011-12-31
  • 2014-12-20
  • 1970-01-01
  • 1970-01-01
  • 2013-04-09
  • 1970-01-01
  • 2016-11-06
  • 1970-01-01
  • 2015-12-07
相关资源
最近更新 更多