如何在没有错误消息或异常的情况下调试 PowerShell 进程

Posted

技术标签:

【中文标题】如何在没有错误消息或异常的情况下调试 PowerShell 进程【英文标题】:How to debug PowerShell process whithout error message or exception 【发布时间】:2018-08-12 04:23:01 【问题描述】:

我正在尝试从我的 .NET 应用程序中运行以下 PowerShell 脚本:

try 
    Start-Process "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" -ArgumentList "--headless --disable-gpu --print-to-pdf=c:\myDir\file.pdf https://www.bing.com"
$x = "Complete" 
$x | Out-File C:\myDir\error.txt

Catch 
    $_ | Out-File C:\myDir\error.txt

简单地说,上面会创建一个基于bing.com网站的pdf

在我的开发环境中,它作为 PowerShell 脚本运行良好。它还在生产服务器上运行良好(同样,作为 PowerShell 脚本)。

当我从生产服务器上的 Web 应用程序调用此 PowerShell 脚本时,会出现此问题。我的 C# 代码是

var command = "c:\myDir\ps.ps1";
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = "powershell.exe";
psi.Arguments = command;
Process process = new Process();
process.StartInfo = psi;
process.Start();

这在我的开发机器上运行良好。它在生产服务器上失败。 error.txt 文件已写入光盘,这表明这不是权限问题。但是,error.txt 文件的内容始终显示“完整”。它永远不会出错。

因此,PowerShell 脚本中的问题似乎永远不会被击中。因此,没有错误消息。 C# 代码中没有抛出异常。无论如何,它不起作用。

如何调试?

或者,如果更简单的话,我很乐意直接运行代码而不是调用 PowerShell 脚本文件,但以下代码也“什么都不做”。

 var command = $"\"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe\" -ArgumentList \"--no-sandbox --headless --disable-gpu --print-to-pdf=imagePath fullUrl";

【问题讨论】:

一方面,您可以通过从等式中删除 PowerShell 来简化它 - 只需使用 Process.Start 直接启动 Chrome。然后,你可以开始重定向进程stdout和stderr,看看是否有错误发生。 我不完全理解@RB。我已经可以证明脚本正在运行,因为它总是更新(或创建)error.txt 文件。我还可以运行实际的 powershell 脚本,它按预期执行 调试的第一条规则 - 简化您的问题...此时您的代码中有一层间接性。删除它并直接记录 stdout 和 stderr 可能会公开其他信息,可以帮助您解决问题。当然,它可能不会 - 但检查大约需要 30 分钟,到目前为止你已经等了一周:) 【参考方案1】:

我能够重现您的问题。这是由于您的生产服务器上的 Web 应用程序在当前未登录的用户下运行。它在分配的应用程序池的身份下运行。如果 Chrome 是在与当前登录用户不同的用户下启动的,则 Chrome 有known issue 无法正常工作。如果您检查该链接,您将看到该问题已在 2012 年 12 月注册,但仍未解决。如果在不同用户下启动 Chrome,您可以轻松重现该问题(按下 Shift 调用快捷上下文菜单中的“以不同用户身份运行”)。在这种情况下,Chrome 不会打开任何页面,只会显示灰屏。

workaround 是用--no-sandbox 开关启动 Chrome。谷歌实际上并不推荐这一点。但是,如果您以自动方式运行 Chrome 以访问受信任的来源,我相信没问题。

所以要解决这个问题,请按以下方式修改脚本中的start-process

start-process "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" -ArgumentList "--no-sandbox --headless --disable-gpu --print-to-pdf=c:\myDir\file.pdf https://www.bing.com"

更新

一开始我低估了这个问题。现在,经过额外的研究和许多尝试过的方法,我可以提出可行的解决方案。

我没有解决您当前从 Web 应用程序直接启动 powershell 和 chrome 的方法。 Chrome 无法启动,并且事件日志中出现以下错误:

Faulting application name: chrome.exe, version: 64.0.3282.186, time stamp: 0x5a8e38d5
Faulting module name: chrome_elf.dll, version: 64.0.3282.186, time stamp: 0x5a8e1e3d
Exception code: 0x80000003
Fault offset: 0x00000000000309b9
Faulting process id: 0x11524
Faulting application start time: 0x01d3bab1a89e3b4f
Faulting application path: C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
Faulting module path: C:\Program Files (x86)\Google\Chrome\Application\64.0.3282.186\chrome_elf.dll
Report Id: e70a5a36-26a4-11e8-ac26-b8ca3a94ba80

即使您将应用程序池配置为使用可以启动 chrome 的某些现有(普通)用户的身份,也会发生此错误。 可能可以配置 IIS 或应用程序池来防止这些错误,但我还没有找到方法。

我的建议是从启动 powershell 进程从控制器操作切换到使用 Windows 任务调度程序调度任务。

以下是完成此任务应采取的步骤:

    在您的生产服务器上创建一个用户,Chrome 将在该用户下启动。我将创建的用户称为“testuser”。 在testuser下登录,启动chrome,打开一些站点。如果没有这一步,流程不会成功,可能是因为缺少 chrome 用户帐户。 为testuser 授予“作为批处理作业登录”权限。此步骤是成功执行testuser 下的计划任务所必需的。该过程在this answer 中进行了描述 将--no-sandbox 参数添加到脚本中,正如我在最初的回答中所描述的那样。 将Process.Start()的代码替换为任务作业的调度。

The easiest way 通过TaskScheduler NuGet 从 .Net 安排任务。将其安装到您的应用程序并添加以下代码:

string powerShellScript = @"c:\myDir\ps.ps1";
string userName = @"YOURCOMP\testuser";
string userPassword = "TestPwd123";

using (TaskService ts = new TaskService())

    TaskDefinition td = ts.NewTask();
    td.Triggers.Add(new RegistrationTrigger
    
        StartBoundary = DateTime.Now,
        EndBoundary = DateTime.Now.AddMinutes(1),
    );
    td.Settings.DeleteExpiredTaskAfter = TimeSpan.FromSeconds(5);
    td.Actions.Add(new ExecAction("powershell.exe", powerShellScript));
    ts.RootFolder.RegisterTaskDefinition($@"Print Pdf - Guid.NewGuid()", td, createType: TaskCreation.Create, userId: userName, password: userPassword, logonType: TaskLogonType.Password);

在上面的代码中,sn -p 更改testuser 的名称和密码。

通过这种方法,您的脚本已成功执行,并且 pdf 已成功打印。

由 OP 更新

如果上述操作仍然失败,请再次检查事件查看器日志。在这种情况下,我遇到了与The machine-default permission settings do not grant Local Activation permission for the COM Server application with CLSID 20FD4E26-8E0F-4F73-A0E0-F27B8C57BE6F and APPID Unavailable 类似的消息的问题,但通过授予permissions for the CLSID 已解决。此外,尝试在任务计划程序中自行运行任务,例如创建一个新任务以简单地启动记事本或类似的,以确保这与您要测试的帐户一起使用。就我而言,我必须使用管理员帐户。

【讨论】:

谢谢。大约 10 小时后我会在办公室进行测试!显然我会回来,但已经 +1 刚试过。可惜没有影响。同样的问题仍然存在 应用程序在生产环境中运行的池的标识是什么?是本地系统帐号吗? 应用程序池标识为ApplicationPoolIdentity 这似乎是一种非常聪明的方法。代码是否创建了一次性任务但留下了它(意思是,看起来好像没有清理操作)【参考方案2】:

我认为除了 CodeFuller 所说的没有带有--no-sandbox 选项的沙箱之外,您还应该禁用所有扩展、同步和书签。

最好有一个 Guest session 别名“无需登录即可浏览”,并带有--bwsi 选项。

有趣的是,在测试过程中,我发现在执行--bwsi 之前使用--disable-extensions 明确禁用扩展会更好,得到更好的pdf 打印输出。

我已经对其进行了测试,对我来说它有效。我期待您的反馈。

Edit1 和 Edit3 - 删除 try...catch 并添加用户和密码并添加 psuser 细节

您可能在域上,所以我调整了脚本以在域上以不同用户身份运行(用户必须具有正确的权限!)

首先创建您的凭据文件:

登录到用户,例如psuser

创建密码文件:

# Encrypt user password and save it to file
Read-Host -AsSecureString | ConvertFrom-SecureString | Out-File 'C:\<your_path>\your_secret_password.txt'

然后使用加密凭据运行以下改进的脚本:

$username = 'psuser' # This needs to be adjusted to correct user you are using 
$domain = <your_domain> # adjust to your needs

$encrypted_passwd = get-content 'C:\<your_path>\your_secret_password.txt' | ConvertTo-securestring



# Setting process invocation parameters.
   $process_start_info = New-Object -TypeName System.Diagnostics.ProcessStartInfo
   $process_start_info.CreateNoWindow = $true
   $process_start_info.UseShellExecute = $false
   $process_start_info.RedirectStandardOutput = $true
   $process_start_info.RedirectStandardError = $true
   $process_start_info.UserName = $username
   $process_start_info.Domain = $domain
   $process_start_info.Password = $encrypted_passwd
   $process_start_info.Verb = 'runas'
   $process_start_info.FileName = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
   $process_start_info.Arguments = '--no-sandbox --disable-extensions --bwsi --headless --disable-gpu  --print-to-pdf=C:\prg\PowerShell\test\chrome_file.pdf https://www.bing.com'

   # Creating process object.
   $process = New-Object -TypeName System.Diagnostics.Process
   $process.StartInfo = $process_start_info

   # Start the process
   [Void]$process.Start()

   $process.WaitForExit()

   # synchronous output - captures everything
   $output = $process.StandardOutput.ReadToEnd()
   $output += $process.StandardError.ReadToEnd()

   Write-Output $output

在脚本调试过程中我遇到了这些错误:

a) 当您想针对 AD 服务器进行验证但它不可用时:

Exception calling "Start" with "0" argument(s): "There are currently no logon servers available to service the logon request"
At C:\prg\PowerShell\test\chrome_print.ps1:56 char:12
+            [Void]$process.Start()
+            ~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : Win32Exception

Exception calling "WaitForExit" with "0" argument(s): "No process is associated with this object."
At C:\prg\PowerShell\test\chrome_print.ps1:58 char:12
+            $process.WaitForExit()
+            ~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : InvalidOperationException

You cannot call a method on a null-valued expression.
At C:\prg\PowerShell\test\chrome_print.ps1:61 char:12
+            $output = $process.StandardOutput.ReadToEnd()
+            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

You cannot call a method on a null-valued expression.
At C:\prg\PowerShell\test\chrome_print.ps1:62 char:12
+            $output += $process.StandardError.ReadToEnd()
+            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

b) 脚本中缺少域信息:

Exception calling "Start" with "0" argument(s): "The stub received bad data"                            
At C:\prg\PowerShell\test\chrome_print.ps1:39 char:12                                                   
+            [Void]$process.Start()                                                                     
+            ~~~~~~~~~~~~~~~~~~~~~~                                                                     
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException                           
    + FullyQualifiedErrorId : Win32Exception                                                            

Exception calling "WaitForExit" with "0" argument(s): "No process is associated with this object."      
At C:\prg\PowerShell\test\chrome_print.ps1:41 char:12                                                   
+            $process.WaitForExit()                                                                     
+            ~~~~~~~~~~~~~~~~~~~~~~                                                                     
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException                           
    + FullyQualifiedErrorId : InvalidOperationException                                                 

You cannot call a method on a null-valued expression.                                                   
At C:\prg\PowerShell\test\chrome_print.ps1:44 char:12                                                   
+            $output = $process.StandardOutput.ReadToEnd()                                              
+            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                                              
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException                                
    + FullyQualifiedErrorId : InvokeMethodOnNull                                                        

You cannot call a method on a null-valued expression.                                                   
At C:\prg\PowerShell\test\chrome_print.ps1:45 char:12                                                   
+            $output += $process.StandardError.ReadToEnd()                                              
+            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                                              
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException                                
    + FullyQualifiedErrorId : InvokeMethodOnNull     

打印 pdf

stderr 消息:

[0313/112937.660:ERROR:gpu_process_transport_factory.cc(1009)] Lost UI shared context.
[0313/112937.662:ERROR:instance.cc(49)] Unable to locate service manifest for metrics
[0313/112937.662:ERROR:service_manager.cc(890)] Failed to resolve service name: metrics
[0313/112938.152:ERROR:instance.cc(49)] Unable to locate service manifest for metrics
[0313/112938.153:ERROR:service_manager.cc(890)] Failed to resolve service name: metrics
[0313/112942.876:INFO:headless_shell.cc(566)] Written to file C:\prg\PowerShell\test\chrom e_file.pdf.

Edit2 使用 ASP.NET

添加 windows 帐户模拟

使用 ASP.NET 模拟 Windows 帐户: ASP.NET 用户未传递到新线程(默认情况下)。当您想调用 PowerShell 脚本时,它会在具有不同凭据的其他线程中调用(当您有专门的域认证用户来运行上述脚本时,您可以使用上述脚本克服这一点)。默认脚本在内置账号NT AUTHORITY\NETWORK SERVICE下执行。

这些步骤是在ASP.NET 级别克服它:

1) 在 IIS 中启用 Windows 身份验证 a) 先安装(这是windows 2008 R2截图):

b) 在您的 IIS 上启用它:

将其更改为启用:

2) 更改您网站的 web.config 以正确处理模拟

编辑网站目录中的 web.config 文件。为了执行当前用户的安全上下文(AD)的服务器端代码。

找到xml标签:&lt;system.web&gt;并添加两个新元素以启用windows authentication

<authentication mode="Windows" />
<identity impersonate="True" />

3) 正确编写代码以调用进程内 PowerShell 脚本

您需要调整您的 ASP.NET 代码,使您拥有 powershell 运行空间,并在管道中调用运行空间内的脚本

一个简单的例子:

// You need to create a Runspace. Each other pipeline you create will run in the same Runspace
// Do it only once, all others will be pipelined
RunspaceConfiguration powershellConfiguration = RunspaceConfiguration.Create();
var powershellRunspace = RunspaceFactory.CreateRunspace(powershellConfiguration);
powershellRunspace.Open();

// create a pipeline the cmdlet invocation
using ( Pipeline psPipeline = powershellRunspace.CreatePipeline() )
    // Define the command to be executed in this pipeline
    Command script = new Command("PowerShell_script");

    // Add any parameter(s) to the command
    script.Parameters.Add("Param1", "Param1Value");

    // Add it to the pipeline
    psPipeline.Commands.Add(script);

    try 
        // Invoke() the script
        var results = psPipeline.Invoke();
        // work with the results
     catch (CmdletInvocationException exception) 
    // Any exceptions here - for the invoked process
    

4) 修改aspnet.config 允许模拟跨线程

此步骤允许您以当前的模拟用户身份运行。

您必须修改服务器的 aspnet.config 文件。 在configurationruntime中添加两个xml元素:

<configuration>
<runtime>
...
<legacyImpersonationPolicy enabled="true" />
<alwaysFlowImpersonationPolicy enabled="false" />
</runtime>
</configuration>

【讨论】:

可悲的是,同样的问题仍在继续:s @MyDaftQuestions 你能告诉我你的标准输出和标准错误吗? 我可以,但请给我大约 8 小时,直到我可以测试它 还有一个问题。您在哪个用户下在生产环境中运行它? 我不知道@tukan 的答案。我怎么知道?我只是发布网站,它似乎只是工作。如果您的意思是在 IIS 中,它设置为在 ApplicationPoolIdentity 下运行【参考方案3】:

您必须重定向标准输入和标准输出,以便它从 powershell.exe 将其发送回父进程(您的网络应用程序)。我修改了你的代码示例来做到这一点:

var command = "c:\myDir\ps.ps1";
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = "powershell.exe";
psi.Arguments = command;

psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;

Process process = new Process();
process.StartInfo = psi;
process.Start();

process.WaitForExit();

Console.WriteLine(process.StandardOutput);
Console.WriteLine(process.StandardError);

【讨论】:

你的意思是process.StandardError 虽然我发现这很有用,但并没有帮助。如果您看到@CodeFuller 帖子,他会提到异常被抛出并登录到事件查看器中。因此,这篇文章将无济于事,因为 Chrome 正在大肆宣传是有原因的

以上是关于如何在没有错误消息或异常的情况下调试 PowerShell 进程的主要内容,如果未能解决你的问题,请参考以下文章

如何捕获 Flex 中的所有异常?

如何在没有有用的调用堆栈的情况下调试难以重现的崩溃?

调试与异常

如何在 C++Builder 发布模式下找到异常,但不是调试

如何在没有 Visual Studio 的情况下调试 .net 应用程序

如何从它的消息中分离异常类型