@@ -65,10 +65,13 @@ func newPty(opt ...Option) (*ptyWindows, error) {
65
65
0 ,
66
66
uintptr (unsafe .Pointer (& pty .console )),
67
67
)
68
- if int32 (ret ) < 0 {
68
+ // CreatePseudoConsole returns S_OK on success, as per:
69
+ // https://learn.microsoft.com/en-us/windows/console/createpseudoconsole
70
+ if windows .Handle (ret ) != windows .S_OK {
69
71
_ = pty .Close ()
70
72
return nil , xerrors .Errorf ("create pseudo console (%d): %w" , int32 (ret ), err )
71
73
}
74
+
72
75
return pty , nil
73
76
}
74
77
@@ -134,39 +137,65 @@ func (p *ptyWindows) Resize(height uint16, width uint16) error {
134
137
Y : int16 (height ),
135
138
X : int16 (width ),
136
139
})))))
137
- if ret != 0 {
140
+ if windows . Handle ( ret ) != windows . S_OK {
138
141
return err
139
142
}
140
143
return nil
141
144
}
142
145
143
- func (p * ptyWindows ) Close () error {
144
- p .closeMutex .Lock ()
145
- defer p .closeMutex .Unlock ()
146
- if p .closed {
147
- return nil
148
- }
149
- p .closed = true
150
-
146
+ // closeConsoleNoLock closes the console handle, and sets it to
147
+ // windows.InvalidHandle. It must be called with p.closeMutex held.
148
+ func (p * ptyWindows ) closeConsoleNoLock () error {
151
149
// if we are running a command in the PTY, the corresponding *windowsProcess
152
150
// may have already closed the PseudoConsole when the command exited, so that
153
151
// output reads can get to EOF. In that case, we don't need to close it
154
152
// again here.
155
153
if p .console != windows .InvalidHandle {
154
+ // ClosePseudoConsole has no return value and typically the syscall
155
+ // returns S_FALSE (a success value). We could ignore the return value
156
+ // and error here but we handle anyway, it just in case.
157
+ //
158
+ // Note that ClosePseudoConsole is a blocking system call and may write
159
+ // a final frame to the output buffer (p.outputWrite), so there must be
160
+ // a consumer (p.outputRead) to ensure we don't block here indefinitely.
161
+ //
162
+ // https://docs.microsoft.com/en-us/windows/console/closepseudoconsole
156
163
ret , _ , err := procClosePseudoConsole .Call (uintptr (p .console ))
157
- if ret < 0 {
158
- return xerrors .Errorf ("close pseudo console: %w" , err )
164
+ if winerrorFailed ( ret ) {
165
+ return xerrors .Errorf ("close pseudo console (%d) : %w" , ret , err )
159
166
}
160
167
p .console = windows .InvalidHandle
161
168
}
162
169
163
- // We always have these files
164
- _ = p .outputRead .Close ()
165
- _ = p .inputWrite .Close ()
166
- // These get closed & unset if we Start() a new process.
170
+ return nil
171
+ }
172
+
173
+ func (p * ptyWindows ) Close () error {
174
+ p .closeMutex .Lock ()
175
+ defer p .closeMutex .Unlock ()
176
+ if p .closed {
177
+ return nil
178
+ }
179
+
180
+ // Close the pseudo console, this will also terminate the process attached
181
+ // to this pty. If it was created via Start(), this also unblocks close of
182
+ // the readers below.
183
+ err := p .closeConsoleNoLock ()
184
+ if err != nil {
185
+ return err
186
+ }
187
+
188
+ // Only set closed after the console has been successfully closed.
189
+ p .closed = true
190
+
191
+ // Close the pipes ensuring that the writer is closed before the respective
192
+ // reader, otherwise closing the reader may block indefinitely. Note that
193
+ // outputWrite and inputRead are unset when we Start() a new process.
167
194
if p .outputWrite != nil {
168
195
_ = p .outputWrite .Close ()
169
196
}
197
+ _ = p .outputRead .Close ()
198
+ _ = p .inputWrite .Close ()
170
199
if p .inputRead != nil {
171
200
_ = p .inputRead .Close ()
172
201
}
@@ -184,15 +213,13 @@ func (p *windowsProcess) waitInternal() {
184
213
// c.f. https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/
185
214
p .pw .closeMutex .Lock ()
186
215
defer p .pw .closeMutex .Unlock ()
187
- if p .pw .console != windows .InvalidHandle {
188
- ret , _ , err := procClosePseudoConsole .Call (uintptr (p .pw .console ))
189
- if ret < 0 && p .cmdErr == nil {
190
- // if we already have an error from the command, prefer that error
191
- // but if the command succeeded and closing the PseudoConsole fails
192
- // then record that error so that we have a chance to see it
193
- p .cmdErr = err
194
- }
195
- p .pw .console = windows .InvalidHandle
216
+
217
+ err := p .pw .closeConsoleNoLock ()
218
+ // if we already have an error from the command, prefer that error
219
+ // but if the command succeeded and closing the PseudoConsole fails
220
+ // then record that error so that we have a chance to see it
221
+ if err != nil && p .cmdErr == nil {
222
+ p .cmdErr = err
196
223
}
197
224
}()
198
225
@@ -225,3 +252,11 @@ func (p *windowsProcess) killOnContext(ctx context.Context) {
225
252
p .Kill ()
226
253
}
227
254
}
255
+
256
+ // winerrorFailed returns true if the syscall failed, this function
257
+ // assumes the return value is a 32-bit integer, like HRESULT.
258
+ //
259
+ // https://learn.microsoft.com/en-us/windows/win32/api/winerror/nf-winerror-failed
260
+ func winerrorFailed (r1 uintptr ) bool {
261
+ return int32 (r1 ) < 0
262
+ }
0 commit comments