dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.05k stars 4.69k forks source link

`Environment.UserInteractive` returns True on mac, even when the process is run in a non-interactive terminal #66530

Open justinmchase opened 2 years ago

justinmchase commented 2 years ago

Description

I suspect it has something to do with this line of code:

https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Environment.UnixOrBrowser.cs#L15

public static bool UserInteractive => true;

Reproduction Steps

On a mac or linux system...

Console.WriteLine(Environment.UserInteractive);
> sh -c 'dotnet run'
True

Expected behavior

> sh -c 'dotnet run'
False

Actual behavior

> sh -c 'dotnet run'
True

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

You can check your process by running the following commands and observing the options. If i is present then it is interactive and if i is not present, then it is non-interactive. I would expect Environment.UserInteractive to be false in the non-interactive cases.

> echo $-
himBH
> sh -c 'echo $-'
hbc
> sh -ci 'echo $-'
himBHc
dotnet-issue-labeler[bot] commented 2 years ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

danmoseley commented 2 years ago

This API was inherited from .NET Framework, where it was implemented as "Is there a Windows window-station with visible windows". Callers used it to see whether they were running in a non interactive Windows service. Of course, it's not named as such.

When porting it we discussed Unix https://github.com/dotnet/runtime/issues/770#issuecomment-564700467 and the thinking was on Unix, what one would expect is "Is there an attached terminal". That's a concept that exists on Windows as well, and is not the same as "Is there a Windows window-station with visible windows" so it would likely be best exposed as a new API if Console.IsInputRedirected is not enough.

Is Console.IsInputRedirected sufficient here?

ghost commented 2 years ago

Tagging subscribers to this area: @dotnet/area-system-runtime See info in area-owners.md if you want to be subscribed.

Issue Details
### Description I suspect it has something to do with this line of code: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Environment.UnixOrBrowser.cs#L15 > public static bool UserInteractive => true; ### Reproduction Steps On a mac or linux system... ```cs Console.WriteLine(Environment.UserInteractive); ``` ```sh > sh -c 'dotnet run' True ``` ### Expected behavior ```sh > sh -c 'dotnet run' False ``` ### Actual behavior ```sh > sh -c 'dotnet run' True ``` ### Regression? _No response_ ### Known Workarounds _No response_ ### Configuration _No response_ ### Other information You can check your process by running the following commands and observing the options. If `i` is present then it is interactive and if `i` is not present, then it is non-interactive. I would expect `Environment.UserInteractive` to be false in the non-interactive cases. ```sh > echo $- himBH > sh -c 'echo $-' hbc > sh -ci 'echo $-' himBHc ```
Author: justinmchase
Assignees: -
Labels: `area-System.Runtime`, `untriaged`
Milestone: -
justinmchase commented 2 years ago

@danmoseley I believe it is not sufficient, for example:

Console.WriteLine("red? " + Console.IsInputRedirected);
Console.WriteLine("int? " + Environment.UserInteractive);
> sh -c 'dotnet run'
red? False
int? True
> sh -ci 'dotnet run'
red? False
int? True

Console.IsInputRedirected returns False in both of these cases. I believe the input can be not-redirected but also non-interactive.


Also, btw hi @danmoseley, its been a while! We used to work together on the IE dev tools back before they closed the St. Paul office. I see you're on .net platform now, very cool. Keep up the good work!

danmoseley commented 2 years ago

Oh hey @justinmchase I should have recognized the name! Yeah I'm happier writing C# than JavaScript..

@tmds thoughts about how a Unix API would work?

Justin what is your use case here?

justinmchase commented 2 years ago

My usecase is that we have a cli tool that we run locally for certain tasks and also we use it in our ci (Jenkins) and some other automation scenarios. The tool is complex enough that in a few situations we want to have an interactive terminal which prompts the user for input using Sharprompt.

We want to be totally sure that in a non-interactive scenario (e.g. Jenkins) it never hangs waiting for input. Essentially we started looking this up and naturally gravitated to Environment.UserInteractive and after experimenting with it in multiple environments could not figure out why it was saying true in our CI pipeline... Then we found the complex algorithm => true 😆

Testing with Console.IsInputRedirected in Jenkins that does return True, and of course other scenarios too, such as if you pipe to stdin.

After reading more about what it means to have an "interactive shell", I think its a little confusing because it seems to just be an option on the shell itself and I guess I have been assuming it thus confers the "non-interactive" status on to child processes as well.

One other interesting thing to note is that non-interactive shells seem to be unable to create interactive shells...

> sh -c 'sh -ci "echo $-"'
hBc

And so I guess I have been assuming that non-interactive shells should not be able to create interactive dotnet applications either...

So if stdin/stdout is redirected I should definitely not attempt consider my process as "interactive" but if the shell that created my process is non-interactive and stdin/stdout is not redirected then, the question is, should it still be considered interactive? Technically all I need in my dotnet app is for the non-redirected stdin/stdout but it seems to me like the interactive option on the shell is there to tell me not to try. I could make my own parameter in my app --no-interactive to get a comparable effect but I just feel like it should be picking it up from the shell somehow...

I'm not sure if thats possible, or maybe I'm still misunderstanding but it does seem to me like there are two different settings here.

That being said, the Environment.UserInteractive documentation is rather tautological and definitely contributed to my confusion. I think it would have helped to have more documentation on this function. Perhaps it's even worth adding the ObsoleteAttribute?

It certainly would help to add a little more context such as:

This function always returns true in unix systems. See [Console.IsInputRedirected] for for more information.

justinmchase commented 2 years ago

So playing around with this more. I can see that just because the input is redirected that doesn't mean that the process can't be interactive... I can have a process spawn other processes, redirect their input and output and basically proxy through to that process. From the child process' perspective it's interactive but the streams are redirected.

However It seems like there really is no standard for this, I certainly don't see any process level flags or way that dotnet could possibly detect this automatically. As far as I can tell its up to the various processes to themselves define how to signal to them that they are interactive, and what that means.

For example bash and sh themselves both have the -i flag, but that doesn't necessarily have any impact on the processes they spawn. Similarly docker run has a -i flag and also python.

Here is python's implementation:

static int
stdin_is_interactive(const PyConfig *config)
{
    return (isatty(fileno(stdin)) || config->interactive);
}

So basically, it becomes interactive if stdin is not redirected or if you explicitly tell it to be via the commandline args.

So I'm still not totally certain but it does seem like the quality of being "interactive" or not is more like an intent which you really can't infer from the redirection status of stdin/stdout all by themselves, but its up to the application itself to define a signal and definition of "interactive".

Therefore I don't think there really is anything dotnet can do here technically to improve the situation, it seems fairly endemic at the OS level. Probably a process level flag that propagates from parent to child by default, would solve it but that sounds way out of the scope for any individual platform.

Probably just improving the docs on Environment.UserInteractive a bit would help, get people pointed in the right direction. Maybe add a blurb about adding their own -i argument as well.

alexrp commented 2 years ago

isatty(stdin) (which is used by Console.IsInputRedirected) is the right thing to use.

Your observations are correct: You can technically make any child process interactive by redirecting its standard I/O streams to a TTY device. (It's quite odd and unusual to do this, though.) If this is done, the process is interactive and isatty (and by extension Console.IsInputRedirected) will indicate that.

The takeaway here is that Is*Redirected is just unfortunate naming carried over from Win32 console semantics.

alexrp commented 2 years ago

Btw, it's worth noting that the notion of "interactive" can be more nuanced in Unix land. You can have all standard I/O streams redirected to non-TTY files and still have a controlling terminal attached. You can read from and write to it by opening /dev/tty. Windows has something a bit similar with CONIN$ and CONOUT$.

This is very esoteric, though, and it's almost never something you should actually want to make use of. Users will generally expect a program to run interactively based simply on isatty(stdin).

justinmchase commented 2 years ago

@alexrp Right but suppose I have a server with a background process, not connected to tty isatty(stdin) == false and Console.IsInputRedirected == false....

Yet, that process is a child of another process which is hooked up to a queue and a database. Elsewhere there is a user at a browser typing into a text box which is pushing messages into a websocket, which ends up going into the queue, which gets popped and written to the redirected stdin of my app, I write back out, which gets written into the db which ultimately gets rendered back to the users browser as html. No tty is found, yet is my process "interactive"? Should I actually do anything differently in knowing that the streams are redirected and not tty in that case?

I think the answer is yes, my process is interactive in that case. And like many of these other apps I referenced, if you have no explicit signal from the user then they use the presence of tty to guess if they should be interactive or not but if the user explicitly passes -i then that can override it. And in that case, even with redirected streams and no detectable tty I should still behave interactively and trust that the caller knows what they're asking of me.

alexrp commented 2 years ago

isatty(stdin) == false and Console.IsInputRedirected == false....

How would that happen?

https://github.com/dotnet/runtime/blob/6e26872d0a282aa71ea792c3550a3cb0e8bf4e71/src/libraries/System.Console/src/System/ConsolePal.Unix.cs#L681-L697

No tty is found, yet is my process "interactive"?

In the traditional Unix sense, no.

Should I actually do anything differently in knowing that the streams are redirected and not tty in that case?

Depends on your app, I suppose? I could imagine that a TUI app might choose to run in some kind of batch mode when run non-interactively.

I think the answer is yes, my process is interactive in that case. And like many of these other apps I referenced, if you have no explicit signal from the user then they use the presence of tty to guess if they should be interactive or not but if the user explicitly passes -i then that can override it. And in that case, even with redirected streams and no detectable tty I should still behave interactively and trust that the caller knows what they're asking of me.

This all sounds highly subjective and app-specific. I don't know that there's a right or wrong answer to that.

justinmchase commented 2 years ago

Sorry I mean Console.IsInputRedirected == true my bad.

justinmchase commented 2 years ago

This all sounds highly subjective and app-specific.

Yeah I agree, I think thats why this is so confusing. Even trying to read around the web for the answers here it seems like peoples recommendations vary wildly and constantly lack precision and it just seems like there is no actual definition of "interactive" technically speaking.

Also, this might be a bug though:

https://github.com/dotnet/runtime/blob/main/src/libraries/System.Console/src/System/ConsolePal.Unix.cs#L123-L128

if (Console.IsInputRedirected)
{
  // We could leverage Console.Read() here however
  // windows fails when stdin is redirected.
  throw new InvalidOperationException(SR.InvalidOperation_ConsoleReadKeyOnFile);
}

If we go with the notion that the app decided to run interactively even with input being redirected, then should be up to the app to look at Console.IsInputRedirected and / or their own arguments and decide to call this function or not.

The error message here seems to indicate that it could work except that Windows has an issue with it... In an ideal world whatever issue there is in Windows would get fixed and then this would be enabled for both environments. If its unfixable in Windows... then I guess then next best thing would be to only have this check in Windows? That kind of cross platform difference sounds pretty awful but it does seem like a shame that this is disabled on unix due to the sins of windows.

justinmchase commented 2 years ago

Vim can accept input from redirected stdin, for example, here I'm sending ESC and theoretically you could send any commands through the terminal this way as well.

> vim -n hi.txt << EOF
ihi:x
EOF
Vim: Warning: Input is not from a terminal
> cat hi.txt
hi

I don't think I could implement this in .net right now, because of that exception which would throw an error when I tried to stream input through redirected input.

Console.WriteLine($"in: {Console.IsInputRedirected}");

var stop = false;
while (!stop)
{
  var k = Console.ReadKey();
  Console.WriteLine($"b={k.Key}");
  if (k.Key == ConsoleKey.Escape)
    stop = true;
}
> dotnet run << EOF
> hi
> EOF
in: True
Cannot read keys when either application does not have a console or when console input has been redirected. Try Console.Read.

And of course Console.Read() doesn't seem to do the right thing either, maybe I could make it work? But I'd probably have to re-implement all of ReadKey() I'm guessing.

Maybe an optional parameter on ReadKey could allow you to opt-in?

public static ConsoleKeyInfo ReadKey(bool intercept, bool readRedirectedInput = false)
{
  if (Console.IsInputRedirected && !readRedirectedInput)
  {
    // We could leverage Console.Read() here however
    // windows fails when stdin is redirected.

    // todo: Update this string to tell the user to optionally enable redirected input processing...
    throw new InvalidOperationException(SR.InvalidOperation_ConsoleReadKeyOnFile);
  }
  // ...
}
tannergooding commented 2 years ago

This API is under System.Runtime but based on the above discussion, @dotnet/area-system-console seems like a better area...

tmds commented 2 years ago

We want to be totally sure that in a non-interactive scenario (e.g. Jenkins) it never hangs waiting for input.

Whatever reads the input should act on the EOF value, like Console.ReadLine returning null.