Skip to content

Retaining the StandardInputWriter prevents write from happening / being flushed #186

Closed

Description

@gsabran

Describe the bug
The StandardInputWriter is not writing or flushing its content until it is deallocated.

To Reproduce

import Testing
import System
import Subprocess

class TestSubprocessTests {
    @Test func example() async throws {
        var retainedInputIO: StandardInputWriter?
        _ = try await Subprocess.run(
          .path("/bin/zsh"),
          arguments: Arguments(["-c"] + ["cat"]),
          environment: .inherit,
          workingDirectory: nil)
        { execution, inputIO, outputIO, errorIO in
            retainedInputIO = inputIO
            Task {
                let bytesWritten = try await inputIO.write("Hello, world!")
                print("bytesWritten: \(bytesWritten)")
            }
            Task {
                for try await line in outputIO.lines() {
                    print("received line: \(line)")
                }
            }
        }
        _ = retainedInputIO
    }
}

Expected behavior
With the line retainedInputIO = inputIO commented out, the test output is

􀟈 Test example() started.
bytesWritten: 13
received line: Hello, world!
􁁛 Test example() passed after 0.008 seconds.

and with this line uncommented I do not see anything received in stdout. The test starts (and never completes, because the program keeps running). Output:

􀟈 Test example() started.
bytesWritten: 13
<test hangs>

For context, I'm working with a long lived process (MCP over stdio) and I need to be able to continuously read from and write to the subprocess. It also seems that the StandardInputWriter has no flush API that I could use to remediate this issue. Adding new lines to the written content doesn't help with flushing

Activity

  1. jakepetroules commented on Oct 21, 2025

    @jakepetroules
    Contributor

    I think this isn't a valid use of the API and that some of the types provided in the body closure should probably be ~Escapable to prevent this.

    Can you restructure your code to ensure that the body closure does not return until you're finished reading and writing? For example, see my PR here which wraps Subprocess in a ProcessController abstraction for long-lived processing: swiftlang/swift-build#638

  2. gsabran commented on Oct 21, 2025

    @gsabran
    Author

    Not returning before completing reading / writing doesn't seem to help. Here's what I tried:

    class TestSubprocessTests {
        @Test func example() async throws {
            _ = try await Subprocess.run(
              .path("/bin/zsh"),
              arguments: Arguments(["-c"] + ["cat"]),
              environment: .inherit,
              workingDirectory: nil)
            { execution, inputIO, outputIO, errorIO in
                await withCheckedContinuation { continuation in
                    Task {
                        let bytesWritten = try await inputIO.write("Hello, world!")
                        print("bytesWritten: \(bytesWritten)")
                    }
                    Task {
                        for try await line in outputIO.lines() {
                            print("received line: \(line)")
                        }
                        continuation.resume(returning: Void())
                    }
                }
                print("exiting body closure")
            }
        }
    }

    which still leads to:

    􀟈 Test example() started.
    bytesWritten: 13
    <test hangs>
    

    My use case is that I want to launch a process, and while it is running have access to stdin / stdout in other parts of my code. The subprocess might run for as long as my app is running. It sounds that making those types ~Escapable would prevent this? This would be unfortunate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions