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

[Analyzer Proposal]: Warn on use of StreamReader.EndOfStream in async methods #98834

Open
stephentoub opened this issue Feb 22, 2024 · 6 comments · May be fixed by dotnet/roslyn-analyzers#7390
Labels
api-approved API was approved in API review, it can be implemented area-System.IO code-analyzer Marks an issue that suggests a Roslyn analyzer
Milestone

Comments

@stephentoub
Copy link
Member

StreamReader.EndOfStream is a pernicious little property.
https://learn.microsoft.com/en-us/dotnet/api/system.io.streamreader.endofstream?view=net-8.0

It seems harmless, like it's just checking something quick and easy. And if there's already some data buffered in the StreamReader, it is: if there's remaining data to read, it's not at the end of the stream. But if there's no data buffered, the only way it can know if it's at the end of the stream is to try to read from the stream and wait until either at least a byte of data is available or it gets back eof. That is a synchronous blocking operation.

In the last month, I've seen several uses of EndOfStream along the lines of:

while (!reader.EndOfStream)
{
    string? line = await reader.ReadLineAsync();
    ...
}

This is problematic because while the developer obviously intended to do their I/O asynchronously, there's a really good chance most of it will end up being done synchronously as part of EndOfStream, making the API synchronously instead of asynchronously blocking. Separately, even if this were intended to be entirely synchronous, this doesn't actually need to use EndOfStream: ReadLine{Async} will give back null if at EOF, so that can be used instead of EndOfStream.

While there's rarely a good reason to use EndOfStream, I'm not sure it rises to the level of obsoleting it. But we should have an analyzer that warns on any use of it in an async method, as it's almost always going to be doing the wrong thing there.

@stephentoub stephentoub added area-System.IO code-analyzer Marks an issue that suggests a Roslyn analyzer labels Feb 22, 2024
@ghost
Copy link

ghost commented Feb 22, 2024

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

Issue Details

StreamReader.EndOfStream is a pernicious little property.
https://learn.microsoft.com/en-us/dotnet/api/system.io.streamreader.endofstream?view=net-8.0

It seems harmless, like it's just checking something quick and easy. And if there's already some data buffered in the StreamReader, it is: if there's remaining data to read, it's not at the end of the stream. But if there's no data buffered, the only way it can know if it's at the end of the stream is to try to read from the stream and wait until either at least a byte of data is available or it gets back eof. That is a synchronous blocking operation.

In the last month, I've seen several uses of EndOfStream along the lines of:

while (!reader.EndOfStream)
{
    string? line = await reader.ReadLineAsync();
    ...
}

This is problematic because while the developer obviously intended to do their I/O asynchronously, there's a really good chance most of it will end up being done synchronously as part of EndOfStream, making the API synchronously instead of asynchronously blocking. Separately, even if this were intended to be entirely synchronous, this doesn't actually need to use EndOfStream: ReadLine{Async} will give back null if at EOF, so that can be used instead of EndOfStream.

While there's rarely a good reason to use EndOfStream, I'm not sure it rises to the level of obsoleting it. But we should have an analyzer that warns on any use of it in an async method, as it's almost always going to be doing the wrong thing there.

Author: stephentoub
Assignees: -
Labels:

area-System.IO, code-analyzer

Milestone: -

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Feb 22, 2024
@stephentoub
Copy link
Member Author

stephentoub commented Feb 23, 2024

Why is EndOfStream bad (ignoring redundancy from null returns)? Sure, that property getter will be synchronous, but then the await will bounce the task to the thread pool.

I don't understand the comment. EndOfStream itself calls a synchronous read method on the stream. By the time it returns, the damage is done.

@stephentoub
Copy link
Member Author

stephentoub commented Feb 23, 2024

Ah. I was not aware that it would perform a read

Yup, as I wrote above, "But if there's no data buffered, the only way it can know if it's at the end of the stream is to try to read from the stream and wait until either at least a byte of data is available or it gets back eof. That is a synchronous blocking operation."

@adamsitnik adamsitnik removed the untriaged New issue has not been triaged by the area owner label Feb 23, 2024
@stephentoub stephentoub added the api-ready-for-review API is ready for review, it is NOT ready for implementation label Feb 23, 2024
@stephentoub stephentoub added this to the 9.0.0 milestone May 1, 2024
@stephentoub stephentoub modified the milestones: 9.0.0, 10.0.0 Jul 22, 2024
@bartonjs
Copy link
Member

bartonjs commented Jul 25, 2024

Video

  • If we find multiple places that we would want to do this we should invent an attribute, but for now we can just special-case the StreamReader.EndOfStream property

Category: Reliability
Visibility: Warning

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jul 25, 2024
@mpidash
Copy link
Contributor

mpidash commented Aug 25, 2024

I would like to work on this analyzer if that's okay :)

@lindexi
Copy link
Contributor

lindexi commented Sep 5, 2024

I'm really looking forward to this feature.

My WPF application is hangs because of this problem.

I wrote the Chinese blog about this.

And the demo code:

var fooStream = new FooStream();
var streamReader = new StreamReader(fooStream);

while (!streamReader.EndOfStream)
{
    var line = await streamReader.ReadLineAsync();
    if (line is null)
    {
        break;
    }
}

class FooStream : Stream
{
    public FooStream()
    {
        _buffer = "123\r\n"u8.ToArray();
    }

    private readonly byte[] _buffer;

    public override void Flush()
    {
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        // 模拟卡顿
        Thread.Sleep(10000);

        if (count >= _buffer.Length)
        {
            count = _buffer.Length;

            Array.Copy(_buffer, 0, buffer, offset, count);
        }

        return count;
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        return offset;
    }

    public override void SetLength(long value)
    {
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
    }

    public override bool CanRead => true;
    public override bool CanSeek => false;
    public override bool CanWrite => false;
    public override long Length => long.MaxValue;
    public override long Position { get; set; }
}

You can find all my demo code in github

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-approved API was approved in API review, it can be implemented area-System.IO code-analyzer Marks an issue that suggests a Roslyn analyzer
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants
@stephentoub @mpidash @adamsitnik @bartonjs @lindexi and others