Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore ERROR_PIPE_NOT_CONNECTED in WindowsConsoleStream.Write #75734

Merged
merged 1 commit into from
Oct 27, 2022

Conversation

Chicken-Bones
Copy link
Contributor

I don't know under what circumstances ERROR_PIPE_NOT_CONNECTED is produced instead of ERROR_BROKEN_PIPE, but some of our users were experiencing exceptions due to this error code.

ERROR_BROKEN_PIPE and ERROR_PIPE_NOT_CONNECTED are treated the same in many other locations in the dotnet src.

I don't know under what circumstances `ERROR_PIPE_NOT_CONNECTED` is produced instead of `ERROR_BROKEN_PIPE`, but some of our users were experiencing exceptions due to this error code. 

`ERROR_BROKEN_PIPE` and `ERROR_PIPE_NOT_CONNECTED` are treated the same in many other locations in the dotnet src.
@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Sep 16, 2022
@ghost
Copy link

ghost commented Sep 16, 2022

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

Issue Details

I don't know under what circumstances ERROR_PIPE_NOT_CONNECTED is produced instead of ERROR_BROKEN_PIPE, but some of our users were experiencing exceptions due to this error code.

ERROR_BROKEN_PIPE and ERROR_PIPE_NOT_CONNECTED are treated the same in many other locations in the dotnet src.

Author: Chicken-Bones
Assignees: -
Labels:

area-System.Console

Milestone: -

@stephentoub
Copy link
Member

This seems reasonable.

Is there a reason we wouldn't also do so earlier in the same file here?

if (errorCode == Interop.Errors.ERROR_NO_DATA || errorCode == Interop.Errors.ERROR_BROKEN_PIPE)

What about in these FileStream/RandomAccess cases?



case Interop.Errors.ERROR_BROKEN_PIPE: // logically success with 0 bytes read (write end of pipe closed)

case Interop.Errors.ERROR_BROKEN_PIPE: // For pipes, ERROR_BROKEN_PIPE is the normal end of the pipe.



It'd be good to understand in what situations we'd actually get ERROR_PIPE_NOT_CONNECTED vs ERROR_BROKEN_PIPE and make sure we're consistently handling each everywhere we should be.

cc: @adamsitnik

@Chicken-Bones
Copy link
Contributor Author

Unfortunately I couldn't figure out the difference from google searches. ERROR_PIPE_NOT_CONNECTED shows up a lot in relation to named pipes, so maybe the particular user for which this bugfix worked had some named pipe redirect in their console?

@adamsitnik adamsitnik self-assigned this Oct 25, 2022
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on reading various websites and our own comments my understanding is following:

  • ERROR_BROKEN_PIPE should be reported when client or server closes the handle (easy to reproduce)
  • ERROR_PIPE_NOT_CONNECTED should be reported after server disconnects (not closes).

In my opinion it's perfectly fine to "ignore" ERROR_PIPE_NOT_CONNECTED for Console, as we don't have the control over named pipe server.

I am not sure about other places pointed by @stephentoub like FileStream or RandomAccess, where this error might mean that somebody has disconnected the server on purpose. I would expect that we throw in such scenarios.

Speaking of which, I've prepared following repro app to demonstrate the diffrences in behaviours:

using System.IO.Pipes;
using System.Text;

namespace NamedPipes
{
    internal class Program
    {
        // usage: dotnet run -c Release server $pipeName
        //        dotnet run -c Release clientPipe $pipeName
        //        dotnet run -c Release clientFileStream $pipeName
        static async Task Main(string[] args)
        {
            string pipeName = args[1];
            CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));

            if (args[0] == "server")
            {
                using NamedPipeServerStream server = new(pipeName, PipeDirection.In, maxNumberOfServerInstances: 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);

                Console.WriteLine("Server waiting for client to connect");
                await server.WaitForConnectionAsync(timeout.Token);
                Console.WriteLine("Server established a connection!");

                using StreamReader streamReader = new StreamReader(server, Encoding.UTF8);
                Console.WriteLine(await streamReader.ReadLineAsync());
                
                Console.WriteLine("Will disconnect now!");
                await server.DisposeAsync();

                // don't quit immediately (handle is closed and client gets a different error)
                await Task.Delay(TimeSpan.FromSeconds(10));
            }
            else if (args[0] == "clientPipe")
            {
                using NamedPipeClientStream client = new(".", pipeName, PipeDirection.Out, PipeOptions.Asynchronous);

                Console.WriteLine("Client waiting to connect to a server");
                await client.ConnectAsync(timeout.Token);
                Console.WriteLine("Client connected!");

                await client.WriteAsync(Encoding.UTF8.GetBytes($"Line 1 {Environment.NewLine}"), timeout.Token);
                await client.WriteAsync(Encoding.UTF8.GetBytes($"Line 2 {Environment.NewLine}"), timeout.Token);
            }
            else if (args[0] == "clientFileStream")
            {
                using FileStream fileStream = new($@"\\.\pipe\{pipeName}", FileMode.Open, FileAccess.Write, FileShare.None, 0, FileOptions.Asynchronous);

                Console.WriteLine("FileStream connected!");

                await fileStream.WriteAsync(Encoding.UTF8.GetBytes($"Line 1 {Environment.NewLine}"), timeout.Token);
                await fileStream.WriteAsync(Encoding.UTF8.GetBytes($"Line 2 {Environment.NewLine}"), timeout.Token);
            }
        }
    }
}

In one terminal, I run:

dotnet run -c Release server unique

Then in other:

dotnet run -c Release clientFileStream unique

The error is ignored (no exception being thrown):

FileStream connected!

And again, I start the server in first tab, and then pipe client in other:

dotnet run -c Release clientPipe unique
PS C:\Users\adsitnik\source\repos\NamedPipes_Disconnect> dotnet run -c Release clientPipe unique
Client waiting to connect to a server
Client connected!
Unhandled exception. System.IO.IOException: Pipe is broken.
   at System.IO.Pipes.PipeStream.WriteAsyncCore(ReadOnlyMemory`1 buffer, CancellationToken cancellationToken)
   at System.IO.Pipes.PipeStream.WriteAsync(ReadOnlyMemory`1 buffer, CancellationToken cancellationToken)
   at NamedPipes.Program.Main(String[] args) in C:\Users\adsitnik\source\repos\NamedPipes_Disconnect\Program.cs:line 42
   at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.ExecutionContextCallback(Object s)
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext(Thread threadPoolThread)
   at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.ExecuteFromThreadPool(Thread threadPoolThread)
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
--- End of stack trace from previous location ---
   at NamedPipes.Program.Main(String[] args) in C:\Users\adsitnik\source\repos\NamedPipes_Disconnect\Program.cs:line 42
   at NamedPipes.Program.<Main>(String[] args)

@stephentoub I think that we should extend this PR to ignore the error for reading from console as you have suggested and then send another one which should unify the edge case scenarios handling for FileStream/RandomAccess and pipe clients and servers. What do you think?

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed change LGTM, I am going to send a separate PR with error handling unification.

Thank you for your contribution @Chicken-Bones !

@adamsitnik adamsitnik merged commit da72713 into dotnet:main Oct 27, 2022
@Chicken-Bones Chicken-Bones deleted the patch-1 branch October 27, 2022 12:09
@stephentoub
Copy link
Member

What do you think?

Was going to say "sounds good", but as this is now merged, a separate PR is fine :)

@adamsitnik
Copy link
Member

Follow up PR: #77543

@adamsitnik adamsitnik added this to the 8.0.0 milestone Oct 28, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Nov 27, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Console community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants