dotnet / command-line-api

Command line parsing, invocation, and rendering of terminal output.
https://github.com/dotnet/command-line-api/wiki
MIT License
3.4k stars 381 forks source link

Working with command line options in Windows Forms doesn't output or return result #1435

Open llu23 opened 3 years ago

llu23 commented 3 years ago

I have a Windows Forms application which also need to have command line options. When the application is started with command line options, it would not load a gui but execute the function this gui is intended to execute, but I am not able to get any output to the console window. Is this possible to do given it's not a Console app?

using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.Reflection;
using System.Runtime.InteropServices;

namespace Greetings
{
    static class Program
    {
        [DllImport("kernel32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool AllocConsole();

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool AttachConsole(uint dwProcessId);

        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern bool FreeConsole();

        const uint ATTACH_PARENT_PROCESS = 0x0ffffffff;

        /// <summary>
        ///  The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            var cmd = new RootCommand
            {
                new Argument<string>("name", "Your name."),
                new Option<string>(new[] {"--greeting", "-g"}, "The greeting to use.")
                {
                    IsRequired = true
                },
            }.WithHandler(nameof(PrintGreeting));

            cmd.Invoke(args);
        }

        private static int PrintGreeting(string name, string greeting, IConsole console)
        {
            if (!AttachConsole(ATTACH_PARENT_PROCESS))
            {
                AllocConsole();
            }

            greeting ??= "Hi";
            console.Out.WriteLine($"{greeting} {name}!");

            FreeConsole();
            return 0;
        }

        private static Command WithHandler(this Command command, string methodName)
        {
            var method = typeof(Program).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static);
            var handler = CommandHandler.Create(method!);
            command.Handler = handler;
            return command;
        }
    }
}
llu23 commented 3 years ago

fixed up Main to return value but still no outputs.

        static int Main(string[] args)
        {
            var cmd = new RootCommand
            {
                new Argument<string>("name", "Your name."),
                new Option<string>(new[] {"--greeting", "-g"}, "The greeting to use.")
                {
                    IsRequired = true
                },
            }.WithHandler(nameof(PrintGreeting));

            return cmd.Invoke(args);
        }
elgonzo commented 3 years ago

I have difficulties in seeing how this would be related to the System.Commandline library. Does it work if you don't involve System.CommandLine functionality but directly invoke AttachConsole/AllocConsole and Console.WriteLine from the Main method of your WinForms project? Note that AttachConsole/AllocConsole stuff is really tricky to get right (if at all. Sometimes it is some project setting, sometimes it is a subtle difference in behavior of a newer framework or maybe of a new VS version/patch level when running in the debugger, that throws a spanner in the works; and which would get only more complicated if one would need console input...)

I recall having had a similar problem in the past outputting help for commandline parameters (not involving System.CommandLine), where i simply gave up and instead opened a messagebox/dialog with the commandline help text...

If you look around on StackOverflow, you'll see many people struggling with this throughout the years.

llu23 commented 3 years ago

Not using System.CommandLine, I am able to see output to the console...

using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace Greetings
{
    static class Program
    {
        [DllImport("kernel32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool AllocConsole();

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool AttachConsole(uint dwProcessId);

        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern bool FreeConsole();

        const uint ATTACH_PARENT_PROCESS = 0x0ffffffff;

        /// <summary>
        ///  The main entry point for the application.
        /// </summary>
        [STAThread]
        static int Main(string[] args)
        {
            return PrintGreeting();
        }

        private static int PrintGreeting()
        {
            if (!AttachConsole(ATTACH_PARENT_PROCESS))
            {
                AllocConsole();
            }

            Console.WriteLine();
            Console.WriteLine("hello");

            SendKeys.SendWait("{ENTER}");
            FreeConsole();
            return 0;
        }
    }
}
elgonzo commented 3 years ago

Err... Your PrintGreetings method now looks substantially different from what you used with System.CommandLine as shown in your first comment (two lines written with the first one being just an empty line, plus a SendKey after writing to the console...) This does not allow to make any conclusions. Use exactly the same PrintGreetings method in your command handler and in your Main method without System.Commandline and compare the results.

llu23 commented 3 years ago

Here's using one args, I just didn't want to parse the args... main thing is that the output is appear when i'm not using System.CommandLine


using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace Greetings
{
    static class Program
    {
        [DllImport("kernel32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool AllocConsole();

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool AttachConsole(uint dwProcessId);

        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern bool FreeConsole();

        const uint ATTACH_PARENT_PROCESS = 0x0ffffffff;

        /// <summary>
        ///  The main entry point for the application.
        /// </summary>
        [STAThread]
        static int Main(string[] args)
        {
            return PrintGreeting(args[0]);
        }

        private static int PrintGreeting(string msg)
        {
            if (!AttachConsole(ATTACH_PARENT_PROCESS))
            {
                AllocConsole();
            }

            Console.WriteLine();

            Console.WriteLine(msg);

            SendKeys.SendWait("{ENTER}");
            FreeConsole();
            return 0;
        }
    }
}
elgonzo commented 3 years ago

No. That's not what i was trying to say. Use the PrintGreeting method with the two Console.WriteLine and the SendKeys.SendWait invocation in your commandhandler (together with the System.CommandLine command setup) and see what result you get there. Do NOT use different PrintGreetings implementations.

llu23 commented 3 years ago

in the System.CommandLine usage one, changed to below and didn't see output...

private static int PrintGreeting(string name, string greeting, IConsole console)
        {
            if (!AttachConsole(ATTACH_PARENT_PROCESS))
            {
                AllocConsole();
            }

            greeting ??= "Hi";

            Console.WriteLine("");
            Console.WriteLine($"{greeting} {name}!");
            SendKeys.SendWait("{ENTER}");

            FreeConsole();
            return 1;
        }
elgonzo commented 3 years ago

Hmm...

Just for test purposes, what if you remove the IConsole console argument from the PrintGreetings method but otherwise leave the code unchanged? Do you see console output then?

My suspicion here is that maybe creating and setting up an IConsole instance by System.CommandLine locks in the runtime's console configuration before your handler (and thus AttachConsole/AllocConsole) got the chance to run.

To further confirm or invalidate my suspicion, what do you get if you call AttachConsole/AllocConsole before you do cmd.Invoke(args); (and not in PrintGreeting) -- in other words, setting up the console before System.CommandLine is going to process the commandline:

        [STAThread]
        static void Main(string[] args)
        {
            // ----------------------------------------------------------------
            // Setting up console config before System.CommandLine gets a chance to do its magic
            // ----------------------------------------------------------------

            if (!AttachConsole(ATTACH_PARENT_PROCESS))
            {
                AllocConsole();
            }

            var cmd = new RootCommand
            {
                new Argument<string>("name", "Your name."),
                new Option<string>(new[] {"--greeting", "-g"}, "The greeting to use.")
                {
                    IsRequired = true
                },
            }.WithHandler(nameof(PrintGreeting));

            cmd.Invoke(args);
        }
        private static int PrintGreeting(string name, string greeting, IConsole console)
        {
            // if (!AttachConsole(ATTACH_PARENT_PROCESS))
            // {
            //     AllocConsole();
            // }

            greeting ??= "Hi";

            Console.WriteLine("");
            Console.WriteLine($"{greeting} {name}!");
            SendKeys.SendWait("{ENTER}");

            FreeConsole();
            return 1;
        }

Note that this is just meant for testing in trying to hone in on the issue and possibly finding a solution/workaround. I am not suggesting that this would be the recommended approach (at least not at this time, given how little we know about the characteristics of this issue).

llu23 commented 3 years ago

no difference

elgonzo commented 3 years ago

Dang. Unfortunately i have no further idea and i can't help any further right now (no Windows box at hand to tinker with the code myself, and dotnetfiddle doesn't support neither WinForms nor Windows-specific API calls). It's a real headscratcher, i'll give you that...

llu23 commented 3 years ago

oh sorry.... I missed the bit where you moved the attachedConsole to Main... that worked!

elgonzo commented 3 years ago

Ah, okay, that is actually helpful to know. While i don't really have a specific advice to offer, knowing that the processing done by System.CommandLine has some effect with regard to console configuration (probably indirectly by using/accessing some functionalities or properties offered by System.Console), i would like to suggest the following unless someone else can offer an easier/better advice that directly addresses the problem:

Download the source code of System.CommandLine here from Github and use it instead of the pre-built System.Command library from nuget. This allows you to single-step debug through code System.CommandLine executes when your Main method invokes cmd.Invoke(args);. When single-stepping, just watch out for the code accessing or calling anything from System.Console. If you see such, make a note of the classes and methods that call/access which System.Console method/property as well as any variable/field/property values that have an influence on the control flow that led to the call/access of the respective System.Console method/property. This should hopefully paint a more detailed picture of what aspect or behavior of System.CommandLine renders AttachConsole/AllocConsole in your command handler ineffective and of what could be done to solve/workaround/mitigate the issue...

KathleenDollard commented 3 years ago

Assuming this is a .NET Core app...

I believe the problem is that now that everything is a Console app under the hood, we had to do a special thing by default in WinForms to avoid always displaying the Command window (which most of the time for most people is not desirable).

I believe you can fix this with the following in your project file:

<ItemGroup>
    <OutputType>exe</OutputType>
</ItemGroup>

Or replacing the output type if it is present and "winexe"

Please let us know if this fixes the problem. And also, if there is a search string you really wish had taken you right to the answer, I can pass it on to our docs folks to try to make this easier to find.