4

I have a WPF application that starts a new process using Process.Start(ProcessStartInfo info).

How do I get the group process ID of the process so that I can send a Ctrl+C signal using GenerateConsoleCtrlEvent? https://msdn.microsoft.com/en-us/library/windows/desktop/ms683155%28v=vs.85%29.aspx

However, I can't seem to find the group process ID of the console window in the new process that is created. It has a session ID for the cur windows user and a process ID.

edit: I finally got my program to work but I still never found a true answer to my real question.

I was able to send ctrl c to a process by using GenerateConsoleCtrlEvent to broadcast to all processes in a console.

However, I was not able to figure out how to get the process group of a process that is running. You can of course save the process group if you create a new process (it should be the ID of the process that called createprocess with the creation flag for create new process group). However, I cannot find anything related to actually grabbing this ID if you are not making a new group yourself and just wish to know the group that a process belongs to. Surely this information is stored somewhere and can be retrieved!

I can get the parent ID in windows NT versions using this function: Fetching parent process Id from child process

However, this does not guarantee the same process group. I am starting to conclude that windows does not have get process group id from process id function.

Linux has a simple getpgrp function that does want I am looking for. I don't understand why windows has a process group if I can't get the value of it

Community
  • 1
  • 1
James Joshua Street
  • 3,259
  • 10
  • 42
  • 80
  • This link may be helpful for you: http://stackoverflow.com/questions/8043375/cleanly-killing-a-console-application-from-within-a-group-of-console-application – Nazmul May 15 '15 at 20:35
  • Your premise is incorrect. From the documentation for GenerateConsoleCtrlEvent: "Generates a CTRL+C signal. This signal cannot be generated for process groups. If dwProcessGroupId is nonzero, this function will succeed, but the CTRL+C signal will not be received by processes within the specified process group." Since you want to send a control-C, finding the process group ID will not help you. – Harry Johnston May 16 '15 at 03:42
  • @HarryJohnston, the docs are incomplete and misleading. What actually happens is that using the creation flag `CREATE_NEW_PROCESS_GROUP` causes Ctrl+C (but not Ctrl+Break) to be initially disabled in a child process, but this state can be inherited as well, so it's not unique to creating a new group. A process that needs Ctrl+C events should manually enable them via `SetConsoleCtrlHandler(NULL, FALSE)`. – Eryk Sun Jul 08 '19 at 06:17
  • 1
    There's no way to determine the process group ID of an arbitrary process. Since at least Windows 8 it's the `ProcessGroupId` field of the `ProcessParameters` in the Process Environment Block (PEB). But the offset of this field is subject to change between Windows versions, and there's no public API to get it. The console host is sent the group ID when the client-side code in kernelbase.dll initializes the console connection. It accesses the field in the process parameters directly; there isn't even an internal function for this. – Eryk Sun Jul 08 '19 at 06:25
  • This would be good to know since `GenerateConsoleCtrlEvent` is buggy if we send an event to a process ID that's not a group ID. In this case, it acts as if we sent the event to group 0, i.e. every process that's attached to the console. – Eryk Sun Jul 08 '19 at 06:32

2 Answers2

1

The documentation for GenerateConsoleCtrlEvent states (emphasis mine):

The identifier of the process group to receive the signal. A process group is created when the CREATE_NEW_PROCESS_GROUP flag is specified in a call to the CreateProcess function. The process identifier of the new process is also the process group identifier of a new process group.

So if your processes are in a group, the WPF application's PID should be the group ID.

Jason Watkins
  • 3,766
  • 1
  • 25
  • 39
  • i haven't gotten it working yet despite sending it to the PID of the WPF app, but this seems like it should be the right answer – James Joshua Street May 15 '15 at 16:13
  • i'm guessing my app is swallowing the ctrl c event. but i don't know how to prevent that swallow. For a console app, I could use Console.CancelKeyPress, but what to do for a wpf app – James Joshua Street May 15 '15 at 16:25
  • I realized there is one other issue i didn't think of. how do I know that the WPF app is the GID owner. maybe both the wpf app, the cmd.exe, and the program being called in the terminal are all children of a higher level process. In which case, the GID would be the process of this unknown. Maybe the simplest solution is for me to just make a new process group on the creation of the process running CMD – James Joshua Street May 15 '15 at 16:35
0

Rather than using GenerateConsoleCtrlEvent, here is how I have found to send CTRL-C to a process. FYI, in this case, I didn't ever need to find the group process ID.

using System;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

public class ConsoleAppManager
{
    private readonly string appName;
    private readonly Process process = new Process();
    private readonly object theLock = new object();
    private SynchronizationContext context;
    private string pendingWriteData;

    public ConsoleAppManager(string appName)
    {
        this.appName = appName;

        this.process.StartInfo.FileName = this.appName;
        this.process.StartInfo.RedirectStandardError = true;
        this.process.StartInfo.StandardErrorEncoding = Encoding.UTF8;

        this.process.StartInfo.RedirectStandardInput = true;
        this.process.StartInfo.RedirectStandardOutput = true;
        this.process.EnableRaisingEvents = true;
        this.process.StartInfo.CreateNoWindow = true;

        this.process.StartInfo.UseShellExecute = false;

        this.process.StartInfo.StandardOutputEncoding = Encoding.UTF8;

        this.process.Exited += this.ProcessOnExited;
    }

    public event EventHandler<string> ErrorTextReceived;
    public event EventHandler ProcessExited;
    public event EventHandler<string> StandartTextReceived;

    public int ExitCode
    {
        get { return this.process.ExitCode; }
    }

    public bool Running
    {
        get; private set;
    }

    public void ExecuteAsync(params string[] args)
    {
        if (this.Running)
        {
            throw new InvalidOperationException(
                "Process is still Running. Please wait for the process to complete.");
        }

        string arguments = string.Join(" ", args);

        this.process.StartInfo.Arguments = arguments;

        this.context = SynchronizationContext.Current;

        this.process.Start();
        this.Running = true;

        new Task(this.ReadOutputAsync).Start();
        new Task(this.WriteInputTask).Start();
        new Task(this.ReadOutputErrorAsync).Start();
    }

    public void Write(string data)
    {
        if (data == null)
        {
            return;
        }

        lock (this.theLock)
        {
            this.pendingWriteData = data;
        }
    }

    public void WriteLine(string data)
    {
        this.Write(data + Environment.NewLine);
    }

    protected virtual void OnErrorTextReceived(string e)
    {
        EventHandler<string> handler = this.ErrorTextReceived;

        if (handler != null)
        {
            if (this.context != null)
            {
                this.context.Post(delegate { handler(this, e); }, null);
            }
            else
            {
                handler(this, e);
            }
        }
    }

    protected virtual void OnProcessExited()
    {
        EventHandler handler = this.ProcessExited;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }

    protected virtual void OnStandartTextReceived(string e)
    {
        EventHandler<string> handler = this.StandartTextReceived;

        if (handler != null)
        {
            if (this.context != null)
            {
                this.context.Post(delegate { handler(this, e); }, null);
            }
            else
            {
                handler(this, e);
            }
        }
    }

    private void ProcessOnExited(object sender, EventArgs eventArgs)
    {
        this.OnProcessExited();
    }

    private async void ReadOutputAsync()
    {
        var standart = new StringBuilder();
        var buff = new char[1024];
        int length;

        while (this.process.HasExited == false)
        {
            standart.Clear();

            length = await this.process.StandardOutput.ReadAsync(buff, 0, buff.Length);
            standart.Append(buff.SubArray(0, length));
            this.OnStandartTextReceived(standart.ToString());
            Thread.Sleep(1);
        }

        this.Running = false;
    }

    private async void ReadOutputErrorAsync()
    {
        var sb = new StringBuilder();

        do
        {
            sb.Clear();
            var buff = new char[1024];
            int length = await this.process.StandardError.ReadAsync(buff, 0, buff.Length);
            sb.Append(buff.SubArray(0, length));
            this.OnErrorTextReceived(sb.ToString());
            Thread.Sleep(1);
        }
        while (this.process.HasExited == false);
    }

    private async void WriteInputTask()
    {
        while (this.process.HasExited == false)
        {
            Thread.Sleep(1);

            if (this.pendingWriteData != null)
            {
                await this.process.StandardInput.WriteLineAsync(this.pendingWriteData);
                await this.process.StandardInput.FlushAsync();

                lock (this.theLock)
                {
                    this.pendingWriteData = null;
                }
            }
        }
    }
}

Then, in actually running the process and sending the CTRL-C in my main app:

            DateTime maxStartDateTime = //... some date time;
            DateTime maxEndDateTime = //... some later date time
            var duration = maxEndDateTime.Subtract(maxStartDateTime);
            ConsoleAppManager appManager = new ConsoleAppManager("myapp.exe");
            string[] args = new string[] { "args here" };
            appManager.ExecuteAsync(args);
            await Task.Delay(Convert.ToInt32(duration.TotalSeconds * 1000) + 20000);

            if (appManager.Running)
            {
                // If stilll running, send CTRL-C
                appManager.Write("\x3");
            }

For details, please see Redirecting standard input of console application

user8128167
  • 6,929
  • 6
  • 66
  • 79