This is looking like an impossible task. Absolutely nothing I've found works. The question is how to cleanly close a console application started with Process.Start that has been started with no console window and without using shell execute: (ProcessStartInfo.CreateNoWindow = true; ProcessStartInfo.UseShellExecute = false;
).
这看起来像是一项不可能的任务。绝对没有我找到的作品。问题是如何干净地关闭以Process.Start启动的控制台应用程序,该控制器应用程序在没有控制台窗口且没有使用shell执行的情况下启动:( ProcessStartInfo.CreateNoWindow = true; ProcessStartInfo.UseShellExecute = false;)。
It is given that the application being started will shut down "cleanly" if it receives a ctrl-c or ctrl-break signal, but there seems to be no way to send it one that works (particularly GenerateConsoleCtrlEvent).
如果它接收到ctrl-c或ctrl-break信号,那么正在启动的应用程序将“干净地”关闭,但似乎没有办法发送它有效(特别是GenerateConsoleCtrlEvent)。
- Process.Kill doesn't work. It leaves corrupt files behind due to abrupt killing of the process.
- Process.Kill不起作用。由于突然杀死进程,它会留下损坏的文件。
- Process.CloseMainWindow doesn't work. There is no main window in this case, so the function returns false and does nothing.
- Process.CloseMainWindow不起作用。在这种情况下没有主窗口,因此该函数返回false并且不执行任何操作。
- Calling EnumThreadWindows on all threads for the process and sending a WM_CLOSE to every window does nothing, and there aren't any thread windows anyway.
- 在进程的所有线程上调用EnumThreadWindows并向每个窗口发送WM_CLOSE都没有做任何事情,并且无论如何都没有任何线程窗口。
- GenerateConsoleCtrlEvent doesn't work. It's only useful for processes in the same group (which .NET gives you no control over), with an unwanted side effect of closing the calling process anyway. The function does not allow you to specify a process id.
- GenerateConsoleCtrlEvent不起作用。它仅对同一组中的进程有用(.NET无法控制),无论如何都会关闭调用进程产生不必要的副作用。该功能不允许您指定进程ID。
Whoever can provide code that accepts a "Process" object started with the parameters above which results in a clean shutdown of the started process without affecting the calling process will be marked as the answer. Use 7z.exe (7-zip archiver) as an example console app, which begins compressing a large file, and will leave a corrupt, unfinished file behind if not terminated cleanly.
任何能够提供接受“Process”对象的代码的人都将被标记为答案,该代码以上述参数开始,这些对象导致已启动进程的干净关闭而不影响调用进程。使用7z.exe(7-zip归档程序)作为示例控制台应用程序,它开始压缩大文件,如果没有干净地终止,将留下损坏的未完成文件。
Until someone provides a functional example or code that leads to a functional example, this question is unanswered. I have seen dozens of people asking this question and dozens of answers online, and none of them work. .NET seems to provide no support for cleanly closing a console application given its process id, which is odd considering it's started with a .NET Process object. Part of the problem is the inability to create a process in a new process group, which makes using GenerateConsoleCtrlEvent useless. There has to be a solution to this.
在有人提供功能性示例或代码导致功能示例之前,这个问题没有答案。我见过几十个人在网上问这个问题和几十个答案,但都没有。 .NET似乎不支持干净地关闭控制台应用程序给定其进程ID,考虑到它是从.NET进程对象开始的,这很奇怪。部分问题是无法在新进程组中创建进程,这使得使用GenerateConsoleCtrlEvent无用。必须有一个解决方案。
2 个解决方案
#1
2
This is a bit late so you might not use it anymore, but perhaps it will help others...
这有点晚了所以你可能不再使用它,但也许它会帮助别人......
You are overthinking this. The problem is that you can only signal a break from a process that shares the same console - the solution should be rather obvious.
你是在思考这个问题。问题是你只能在共享同一控制台的进程中发出中断信号 - 解决方案应该是相当明显的。
Create a console project. This project will launch the target application the usual way:
创建一个控制台项目。该项目将以通常的方式启动目标应用程序:
var psi = new ProcessStartInfo();
psi.FileName = @"D:\Test\7z\7z.exe";
psi.WorkingDirectory = @"D:\Test\7z\";
psi.Arguments = "a output.7z input.bin";
psi.UseShellExecute = false;
var process = Process.Start(psi);
UseShellExecute
is the important part - this ensures that the two applications are going to share the same console.
UseShellExecute是重要的部分 - 这可确保两个应用程序将共享同一个控制台。
This allows you to send the break to your helper application, which will be passed to the hosted application as well:
这允许您将break发送到您的帮助应用程序,该应用程序也将传递给托管应用程序:
Console.CancelKeyPress += (s, e) => e.Cancel = true;
Thread.Sleep(1000);
GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, 0);
This will break the hosted application a second after it is started. Easily, safely. The CancelKeyPress
isn't required - I only put it there to make it obvious that you can break the hosted process, and still keep on running. In the real helper application, this could be used for some notifications or something like that, but it's not really required.
这将在托管应用程序启动后的第二天中断。轻松,安全。 CancelKeyPress不是必需的 - 我只是把它放在那里,以显示你可以打破托管进程,并继续运行。在真正的帮助应用程序中,这可以用于某些通知或类似的东西,但它并不是真正需要的。
Now you only need a way to signal the helper application to issue the break command - the easiest way would be to just use a simple console input, but that might interfere with the hosted application. If that's not an option for you, a simple mutex will work fine:
现在,您只需要一种方法来指示帮助应用程序发出break命令 - 最简单的方法是使用简单的控制台输入,但这可能会干扰托管应用程序。如果这不是你的选择,一个简单的互斥锁将正常工作:
using (var mutex = Mutex.OpenExisting(args[0]))
using (var processWaitHandle = new SafeWaitHandle(process.Handle, false))
using (var processMre = new ManualResetEvent(false) { SafeWaitHandle = processWaitHandle })
{
var which = WaitHandle.WaitAny(new WaitHandle[] { mutex, processMre });
if (which == 0)
{
Console.WriteLine("Got signalled.");
GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, 0);
}
else if (which == 1)
{
Console.WriteLine("Exitted normally.");
}
}
This will wait either for a signal on the mutex, or for the hosted application to exit. To launch the helper application, all you need to do is this:
这将等待互斥锁上的信号或托管应用程序退出。要启动帮助应用程序,您需要做的就是:
var mutexName = Guid.NewGuid().ToString();
mutex = new Mutex(true, mutexName);
var process = Process.Start(@"TestBreak.exe", mutexName);
And to issue the break, just release the mutex:
要发布中断,只需释放互斥锁:
mutex.ReleaseMutex();
That's it. If you need tighter control, using something like a named pipe might be a better option, but if you only need the break signal, a mutex will do just fine. Any arguments you need to pass can be passed as arguments to the helper application, and you could even make this work with shell scripts (just use the helper application to run anything you need to run and break).
而已。如果你需要更严格的控制,使用命名管道之类的东西可能是更好的选择,但如果你只需要中断信号,互斥锁就可以了。您需要传递的任何参数都可以作为参数传递给帮助应用程序,您甚至可以使用shell脚本(只需使用帮助应用程序运行您需要运行和中断的任何内容)。
#2
1
I spent several hours trying to figure this one out myself. As you mentioned, the web is replete with answers that simply don't work. A lot of people suggest using GenerateConsoleCtrlEvent, but they don't provide any context, they just provide useless code snippets. The solution below uses GenerateConsoleCtrlEvent, but it works. I've tested it.
我花了几个小时试图自己解决这个问题。正如您所提到的,网络上充满了根本无法解决的答案。很多人建议使用GenerateConsoleCtrlEvent,但它们不提供任何上下文,它们只提供无用的代码片段。下面的解决方案使用GenerateConsoleCtrlEvent,但它的工作原理。我测试过了。
Note that this is a WinForms app and the process I'm starting and stopping is FFmpeg. I haven't tested the solution with anything else. I am using FFmpeg here to record a video and save the output to a file called "video.mp4".
请注意,这是一个WinForms应用程序,我正在启动和停止的过程是FFmpeg。我没有用其他任何东西来测试解决方案。我在这里使用FFmpeg来录制视频并将输出保存到名为“video.mp4”的文件中。
The code below is the contents of my Form1.cs file. This is the file that Visual Studio creates for you when you create a WinForms solution.
下面的代码是我的Form1.cs文件的内容。这是Visual Studio在创建WinForms解决方案时为您创建的文件。
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
namespace ConsoleProcessShutdownDemo {
public partial class Form1 : Form {
BackgroundWorker worker;
Process currentProcess;
public Form1() {
InitializeComponent();
}
private void Worker_DoWork(object sender, DoWorkEventArgs e) {
const string outFile = "video.mp4";
var info = new ProcessStartInfo();
info.UseShellExecute = false;
info.CreateNoWindow = true;
info.FileName = "ffmpeg.exe";
info.Arguments = string.Format("-f gdigrab -framerate 60 -i desktop -crf 0 -pix_fmt yuv444p -preset ultrafast {0}", outFile);
info.RedirectStandardInput = true;
Process p = Process.Start(info);
worker.ReportProgress(-1, p);
}
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e) {
currentProcess = (Process)e.UserState;
}
private void btnStart_Click(object sender, EventArgs e) {
btnStart.Enabled = false;
btnStop.Enabled = true;
worker = new BackgroundWorker();
worker.WorkerSupportsCancellation = true;
worker.WorkerReportsProgress = true;
worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerAsync();
}
private void btnStop_Click(object sender, EventArgs e) {
btnStop.Enabled = false;
btnStart.Enabled = true;
if (currentProcess != null)
StopProgram(currentProcess);
}
//MAGIC BEGINS
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AttachConsole(uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern bool FreeConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);
[DllImport("Kernel32", SetLastError = true)]
private static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add);
enum CtrlTypes {
CTRL_C_EVENT = 0,
CTRL_BREAK_EVENT,
CTRL_CLOSE_EVENT,
CTRL_LOGOFF_EVENT = 5,
CTRL_SHUTDOWN_EVENT
}
private delegate bool HandlerRoutine(CtrlTypes CtrlType);
public void StopProgram(Process proc) {
int pid = proc.Id;
FreeConsole();
if (AttachConsole((uint)pid)) {
SetConsoleCtrlHandler(null, true);
GenerateConsoleCtrlEvent(CtrlTypes.CTRL_C_EVENT, 0);
Thread.Sleep(2000);
FreeConsole();
SetConsoleCtrlHandler(null, false);
}
proc.WaitForExit();
proc.Close();
}
//MAGIC ENDS
}
}
}
#1
2
This is a bit late so you might not use it anymore, but perhaps it will help others...
这有点晚了所以你可能不再使用它,但也许它会帮助别人......
You are overthinking this. The problem is that you can only signal a break from a process that shares the same console - the solution should be rather obvious.
你是在思考这个问题。问题是你只能在共享同一控制台的进程中发出中断信号 - 解决方案应该是相当明显的。
Create a console project. This project will launch the target application the usual way:
创建一个控制台项目。该项目将以通常的方式启动目标应用程序:
var psi = new ProcessStartInfo();
psi.FileName = @"D:\Test\7z\7z.exe";
psi.WorkingDirectory = @"D:\Test\7z\";
psi.Arguments = "a output.7z input.bin";
psi.UseShellExecute = false;
var process = Process.Start(psi);
UseShellExecute
is the important part - this ensures that the two applications are going to share the same console.
UseShellExecute是重要的部分 - 这可确保两个应用程序将共享同一个控制台。
This allows you to send the break to your helper application, which will be passed to the hosted application as well:
这允许您将break发送到您的帮助应用程序,该应用程序也将传递给托管应用程序:
Console.CancelKeyPress += (s, e) => e.Cancel = true;
Thread.Sleep(1000);
GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, 0);
This will break the hosted application a second after it is started. Easily, safely. The CancelKeyPress
isn't required - I only put it there to make it obvious that you can break the hosted process, and still keep on running. In the real helper application, this could be used for some notifications or something like that, but it's not really required.
这将在托管应用程序启动后的第二天中断。轻松,安全。 CancelKeyPress不是必需的 - 我只是把它放在那里,以显示你可以打破托管进程,并继续运行。在真正的帮助应用程序中,这可以用于某些通知或类似的东西,但它并不是真正需要的。
Now you only need a way to signal the helper application to issue the break command - the easiest way would be to just use a simple console input, but that might interfere with the hosted application. If that's not an option for you, a simple mutex will work fine:
现在,您只需要一种方法来指示帮助应用程序发出break命令 - 最简单的方法是使用简单的控制台输入,但这可能会干扰托管应用程序。如果这不是你的选择,一个简单的互斥锁将正常工作:
using (var mutex = Mutex.OpenExisting(args[0]))
using (var processWaitHandle = new SafeWaitHandle(process.Handle, false))
using (var processMre = new ManualResetEvent(false) { SafeWaitHandle = processWaitHandle })
{
var which = WaitHandle.WaitAny(new WaitHandle[] { mutex, processMre });
if (which == 0)
{
Console.WriteLine("Got signalled.");
GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, 0);
}
else if (which == 1)
{
Console.WriteLine("Exitted normally.");
}
}
This will wait either for a signal on the mutex, or for the hosted application to exit. To launch the helper application, all you need to do is this:
这将等待互斥锁上的信号或托管应用程序退出。要启动帮助应用程序,您需要做的就是:
var mutexName = Guid.NewGuid().ToString();
mutex = new Mutex(true, mutexName);
var process = Process.Start(@"TestBreak.exe", mutexName);
And to issue the break, just release the mutex:
要发布中断,只需释放互斥锁:
mutex.ReleaseMutex();
That's it. If you need tighter control, using something like a named pipe might be a better option, but if you only need the break signal, a mutex will do just fine. Any arguments you need to pass can be passed as arguments to the helper application, and you could even make this work with shell scripts (just use the helper application to run anything you need to run and break).
而已。如果你需要更严格的控制,使用命名管道之类的东西可能是更好的选择,但如果你只需要中断信号,互斥锁就可以了。您需要传递的任何参数都可以作为参数传递给帮助应用程序,您甚至可以使用shell脚本(只需使用帮助应用程序运行您需要运行和中断的任何内容)。
#2
1
I spent several hours trying to figure this one out myself. As you mentioned, the web is replete with answers that simply don't work. A lot of people suggest using GenerateConsoleCtrlEvent, but they don't provide any context, they just provide useless code snippets. The solution below uses GenerateConsoleCtrlEvent, but it works. I've tested it.
我花了几个小时试图自己解决这个问题。正如您所提到的,网络上充满了根本无法解决的答案。很多人建议使用GenerateConsoleCtrlEvent,但它们不提供任何上下文,它们只提供无用的代码片段。下面的解决方案使用GenerateConsoleCtrlEvent,但它的工作原理。我测试过了。
Note that this is a WinForms app and the process I'm starting and stopping is FFmpeg. I haven't tested the solution with anything else. I am using FFmpeg here to record a video and save the output to a file called "video.mp4".
请注意,这是一个WinForms应用程序,我正在启动和停止的过程是FFmpeg。我没有用其他任何东西来测试解决方案。我在这里使用FFmpeg来录制视频并将输出保存到名为“video.mp4”的文件中。
The code below is the contents of my Form1.cs file. This is the file that Visual Studio creates for you when you create a WinForms solution.
下面的代码是我的Form1.cs文件的内容。这是Visual Studio在创建WinForms解决方案时为您创建的文件。
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
namespace ConsoleProcessShutdownDemo {
public partial class Form1 : Form {
BackgroundWorker worker;
Process currentProcess;
public Form1() {
InitializeComponent();
}
private void Worker_DoWork(object sender, DoWorkEventArgs e) {
const string outFile = "video.mp4";
var info = new ProcessStartInfo();
info.UseShellExecute = false;
info.CreateNoWindow = true;
info.FileName = "ffmpeg.exe";
info.Arguments = string.Format("-f gdigrab -framerate 60 -i desktop -crf 0 -pix_fmt yuv444p -preset ultrafast {0}", outFile);
info.RedirectStandardInput = true;
Process p = Process.Start(info);
worker.ReportProgress(-1, p);
}
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e) {
currentProcess = (Process)e.UserState;
}
private void btnStart_Click(object sender, EventArgs e) {
btnStart.Enabled = false;
btnStop.Enabled = true;
worker = new BackgroundWorker();
worker.WorkerSupportsCancellation = true;
worker.WorkerReportsProgress = true;
worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerAsync();
}
private void btnStop_Click(object sender, EventArgs e) {
btnStop.Enabled = false;
btnStart.Enabled = true;
if (currentProcess != null)
StopProgram(currentProcess);
}
//MAGIC BEGINS
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AttachConsole(uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern bool FreeConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);
[DllImport("Kernel32", SetLastError = true)]
private static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add);
enum CtrlTypes {
CTRL_C_EVENT = 0,
CTRL_BREAK_EVENT,
CTRL_CLOSE_EVENT,
CTRL_LOGOFF_EVENT = 5,
CTRL_SHUTDOWN_EVENT
}
private delegate bool HandlerRoutine(CtrlTypes CtrlType);
public void StopProgram(Process proc) {
int pid = proc.Id;
FreeConsole();
if (AttachConsole((uint)pid)) {
SetConsoleCtrlHandler(null, true);
GenerateConsoleCtrlEvent(CtrlTypes.CTRL_C_EVENT, 0);
Thread.Sleep(2000);
FreeConsole();
SetConsoleCtrlHandler(null, false);
}
proc.WaitForExit();
proc.Close();
}
//MAGIC ENDS
}
}
}