从C#中执行多个命令在相同的环境下

5
我正在开发一个小型的C# GUI工具,它可以获取一些C++代码并在通过一些向导后进行编译。如果我在运行著名的vcvarsall.bat之后从命令提示符中运行它,这一切都很好。现在,我希望用户不需要先进入命令提示符,而是让程序调用vcvars,然后调用nmake和其他我需要的工具。为了使这个工具正常工作,vcvars设置的环境变量显然应该被保留。
我该如何做呢?
我找到的最好的解决方案是创建一个临时的cmd/bat脚本来调用其他工具,但我想知道是否有更好的方法。
更新:我已经尝试过批处理文件和cmd。当使用批处理文件时,vcvars会终止整个批处理执行,所以我的第二个命令(即nmake)不会被执行。我目前的解决方法是这样的(缩短版):
string command = "nmake";
string args = "";
string vcvars = "...vcvarsall.bat";
ProcessStartInfo info = new ProcessStartInfo();
info.WorkingDirectory = workingdir;
info.FileName = "cmd";
info.Arguments = "/c \"" + vcvars + " x86 && " + command + " " + args + "\"";
info.CreateNoWindow = true;
info.UseShellExecute = false;
info.RedirectStandardOutput = true;
Process p = Process.Start(info);

这个方法可以工作,但是cmd调用的输出结果没有被捕获。我正在寻找更好的解决方案。


目前我打开了一个VC命令提示符,然后启动我的程序。该程序创建了一个ProcessStartInfo对象,其中包含适当的WorkingDir、Filename="namke"和redirected output...在那里,nmake继承了我的程序环境,而我的程序环境则继承了vc环境。 - johannes
好奇:给我点踩的人,你能解释一下吗?谢谢。 - johannes
我认为我对以下问题的回答可能会对您有所帮助。https://dev59.com/IHVC5IYBdhLWcg3weBE-#280584 - kenny
3个回答

3

我有几个不同的建议:

  1. 您可以研究使用MSBuild而不是NMake

    它更为复杂,但可以直接从.NET控制,并且是VS 2010及更早版本的C#/VB等项目的项目文件格式。

  2. 您可以使用一个小助手程序捕获环境并将其注入到您的进程中

    这可能有点过度,但它是可行的。vsvarsall.bat仅仅设置了一些环境变量,所以您只需要记录运行它的结果,然后重放到您创建的进程的环境中即可。

助手程序(envcapture.exe)非常简单,它只列出其环境中的所有变量并将它们打印到标准输出。这是整个程序代码;将其放在 Main() 中:

XElement documentElement = new XElement("Environment");
foreach (DictionaryEntry envVariable in Environment.GetEnvironmentVariables())
{
    documentElement.Add(new XElement(
        "Variable",
        new XAttribute("Name", envVariable.Key),
        envVariable.Value
        ));
}

Console.WriteLine(documentElement);

您也许可以只调用 set 而不是这个程序并解析输出,但如果任何环境变量包含换行符,这可能会导致错误。

在您的主程序中:

首先,必须捕获由vcvarsall.bat初始化的环境。为此,我们将使用类似于 cmd.exe /s /c " "...\vcvarsall.bat" x86 && "...\envcapture.exe" "的命令行。vcvarsall.bat修改了环境,然后envcapture.exe将其打印出来。然后,主程序捕获该输出并将其解析为字典。(注意:这里的 vsVersion 可能是90、100或110等)

private static Dictionary<string, string> CaptureBuildEnvironment(
    int vsVersion, 
    string architectureName
    )
{
    // assume the helper is in the same directory as this exe
    string myExeDir = Path.GetDirectoryName(
        Assembly.GetExecutingAssembly().Location
        );
    string envCaptureExe = Path.Combine(myExeDir, "envcapture.exe");
    string vsToolsVariableName = String.Format("VS{0}COMNTOOLS", vsVersion);
    string envSetupScript = Path.Combine(
        Environment.GetEnvironmentVariable(vsToolsVariableName),
        @"..\..\VC\vcvarsall.bat"
        );

    using (Process envCaptureProcess = new Process())
    {
        envCaptureProcess.StartInfo.FileName = "cmd.exe";
        // the /s and the extra quotes make sure that paths with
        // spaces in the names are handled properly
        envCaptureProcess.StartInfo.Arguments = String.Format(
            "/s /c \" \"{0}\" {1} && \"{2}\" \"",
            envSetupScript,
            architectureName,
            envCaptureExe
            );
        envCaptureProcess.StartInfo.RedirectStandardOutput = true;
        envCaptureProcess.StartInfo.RedirectStandardError = true;
        envCaptureProcess.StartInfo.UseShellExecute = false;
        envCaptureProcess.StartInfo.CreateNoWindow = true;

        envCaptureProcess.Start();

        // read and discard standard error, or else we won't get output from
        // envcapture.exe at all
        envCaptureProcess.ErrorDataReceived += (sender, e) => { };
        envCaptureProcess.BeginErrorReadLine();

        string outputString = envCaptureProcess.StandardOutput.ReadToEnd();

        // vsVersion < 110 prints out a line in vcvars*.bat. Ignore 
        // everything before the first '<'.
        int xmlStartIndex = outputString.IndexOf('<');
        if (xmlStartIndex == -1)
        {
            throw new Exception("No environment block was captured");
        }
        XElement documentElement = XElement.Parse(
            outputString.Substring(xmlStartIndex)
            );

        Dictionary<string, string> capturedVars 
            = new Dictionary<string, string>();

        foreach (XElement variable in documentElement.Elements("Variable"))
        {
            capturedVars.Add(
                (string)variable.Attribute("Name"),
                (string)variable
                );
        }
        return capturedVars;
    }
}

稍后,当您想在构建环境中运行命令时,只需使用先前捕获的环境变量替换新进程中的环境变量即可。每次运行程序时,每个参数组合只需要调用一次CaptureBuildEnvironment。但不要在运行之间尝试保存它,否则它会过期。

static void Main()
{
    string command = "nmake";
    string args = "";

    Dictionary<string, string> buildEnvironment = 
        CaptureBuildEnvironment(100, "x86");

    ProcessStartInfo info = new ProcessStartInfo();
    // the search path from the adjusted environment doesn't seem
    // to get used in Process.Start, but cmd will use it.
    info.FileName = "cmd.exe";
    info.Arguments = String.Format(
        "/s /c \" \"{0}\" {1} \"",
        command,
        args
        );
    info.CreateNoWindow = true;
    info.UseShellExecute = false;
    info.RedirectStandardOutput = true;
    info.RedirectStandardError = true;
    foreach (var i in buildEnvironment)
    {
        info.EnvironmentVariables[(string)i.Key] = (string)i.Value;
    }

    using (Process p = Process.Start(info))
    {
        // do something with your process. If you're capturing standard output,
        // you'll also need to capture standard error. Be careful to avoid the
        // deadlock bug mentioned in the docs for
        // ProcessStartInfo.RedirectStandardOutput. 
    }
}

如果您使用此方法,请注意,如果缺少或失败vcvarsall.bat,则可能会出现严重问题,并且在其他语言环境下可能会遇到问题。

选项一不可行,因为整个系统更加复杂且超出了我的控制范围。我如何在vcvars调用后获取环境? - johannes
我已经添加了更多关于这三个代码片段实际执行的澄清注释。 - Jonathan Myers
你试过了吗?- 我现在手头没有Windows电脑,但是当我运行我的代码时,由于CreateNoWindow创建了一个不可见的窗口并将输出粘贴到那里,因此无法捕获cmd输出,使其通过进程的StandardOutput流无法访问。 - johannes
CreateNoWindow 不是问题所在。它实际上并不创建一个不可见的控制台窗口;它只是告诉程序不要创建一个新的窗口。问题似乎是 cmd.exe 在启动进程时不会将标准输出句柄转发给它,除非它还有一个有效的标准错误句柄。我已经在我的答案中添加了一个解决方法。 - Jonathan Myers
哈!process.RedirectStandardError = true; 这就是我真正需要的。我仍然认为这个解决方案有点取巧,但我会采用这种方法。谢谢! - johannes

1

收集所需的所有数据,生成批处理文件并使用Process类运行它可能是最好的方法。 正如您所写的那样,您正在重定向输出,这意味着您必须设置UseShellExecute = false;,因此我认为除了从批处理文件中调用SET之外,没有其他设置变量的方法。


是的,正如所说,这是我目前的解决方案想法...虽然它有很多缺陷(很难看出哪一步在出错时中断,很难分割输出日志文件,...),所以我正在寻找更好的解决方案 :-) - johannes
@johannes 其实并不难,只需要处理 process.ErrorDataReceived 事件,并在进程之间插入一些 echo 即可。 - VladL
是的,然后解析输出并希望回显序列真正唯一等等...就像说的那样:这是可能的,但很烦人。 - johannes
1
@johannes 但是为什么不在每个命令之前添加类似于**ECHO *命令名称的东西呢? - VladL

0

编辑:添加nmake调用的具体用例

过去我需要获取各种“构建路径信息”,这就是我使用的方法-您可能需要在此处或那里进行微调,但基本上,vcvars所做的唯一事情就是设置一堆路径;这些辅助方法获取这些路径名称,您只需要将它们传递到启动信息中即可:

public static string GetFrameworkPath()
{
    var frameworkVersion = string.Format("v{0}.{1}.{2}", Environment.Version.Major, Environment.Version.Minor, Environment.Version.Build);
    var is64BitProcess = Environment.Is64BitProcess;
    var windowsPath = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
    return Path.Combine(windowsPath, "Microsoft.NET", is64BitProcess ? "Framework64" : "Framework", frameworkVersion);  
}

public static string GetPathToVisualStudio(string version)
{   
    var is64BitProcess = Environment.Is64BitProcess;
    var registryKeyName = string.Format(@"Software\{0}Microsoft\VisualStudio\SxS\VC7", is64BitProcess ? @"Wow6432Node\" : string.Empty);
    var vsKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(registryKeyName);
    var versionExists = vsKey.GetValueNames().Any(valueName => valueName.Equals(version));
    if(versionExists)
    {
        return vsKey.GetValue(version).ToString();
    }
    else
    {
        return null;
    }
}

你可以通过类似以下方式利用这些内容:

var paths = new[]
    { 
        GetFrameworkPath(), 
        GetPathToVisualStudio("10.0"),
        Path.Combine(GetPathToVisualStudio("10.0"), "bin"),
    };  

var previousPaths = Environment.GetEnvironmentVariable("PATH").ToString();
var newPaths = string.Join(";", previousPaths.Split(';').Concat(paths));
Environment.SetEnvironmentVariable("PATH", newPaths);

var startInfo = new ProcessStartInfo()
{
    FileName = "nmake",
    Arguments = "whatever you'd pass in here",
};
var process = Process.Start(startInfo);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接