Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions console_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,16 +212,19 @@ func checkConsole(f File) error {
return nil
}

// newMaster creates a Console from one of the process's standard streams
// (os.Stdin, os.Stdout, or os.Stderr); any other file is rejected.
// newMaster creates a Console from one of the process's standard streams,
// identified by its underlying console handle: os.Stdin, os.Stdout,
// os.Stderr, or any File reporting one of their handles through Fd() — a
// wrapper decorating a standard stream is accepted, since the handle is what
// designates the console object. Files with any other handle are rejected.
//
// Read, Write, Fd, and Name are delegated to f. The console-mode operations
// (SetRaw, Reset, Size, and DisableEcho) act on the process's standard
// handles rather than f alone: on Windows these streams share a single
// underlying console object, so mode and size queries apply to that console
// as a whole.
func newMaster(f File) (Console, error) {
if f != os.Stdin && f != os.Stdout && f != os.Stderr {
if fd := f.Fd(); fd != os.Stdin.Fd() && fd != os.Stdout.Fd() && fd != os.Stderr.Fd() {
return nil, errors.New("creating a console from a file is not supported on windows")
}
m := &master{f: f}
Expand Down
37 changes: 37 additions & 0 deletions console_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,40 @@ func TestConsoleFromFile_Delegation(t *testing.T) {
t.Fatalf("subprocess failed: %v\n%s", err, stderr.String())
}
}

// fileWrapper decorates an *os.File without being one of the os.Std*
// package variables — the shape callers hand over when their CLI streams
// wrap a standard stream (see docker/compose#14086).
type fileWrapper struct {
*os.File
}

// TestNewMaster_StandardStreamIdentity verifies that newMaster identifies a
// standard stream by its console handle rather than by Go value identity: a
// File decorating a standard stream reports the same handle through Fd() and
// designates the same console object, so it must be accepted like the os.Std*
// value it wraps. Files carrying any other handle stay rejected.
//
// initStdios tolerates redirected streams, so this runs in headless CI.
func TestNewMaster_StandardStreamIdentity(t *testing.T) {
for _, f := range []*os.File{os.Stdin, os.Stdout, os.Stderr} {
if _, err := newMaster(f); err != nil {
t.Errorf("newMaster(%s): %v", f.Name(), err)
}
if _, err := newMaster(fileWrapper{f}); err != nil {
t.Errorf("newMaster(wrapper{%s}): %v", f.Name(), err)
}
}

f, err := os.CreateTemp(t.TempDir(), "console")
if err != nil {
t.Fatal(err)
}
defer f.Close()
if _, err := newMaster(f); err == nil {
t.Error("newMaster accepted a regular file")
}
if _, err := newMaster(fileWrapper{f}); err == nil {
t.Error("newMaster accepted a wrapped regular file")
}
}
Loading