wavetermdev/waveterm · error

error writing to pty: %w

Error message

error writing to pty: %w

What it means

After successfully decoding InputData64, HandleInput writes the bytes to the job's controlling PTY (jm.cmdPty). This error wraps any failure from that pty Write, which means the job process's terminal is no longer writable (process exited and pty closed, or an OS-level I/O error on the pty master).

Source

Thrown at pkg/jobmanager/jobcmd.go:196

// TODO set up a single input handler loop + queue so we dont need to hold the lock but still get synchronized in-order execution
func (jm *JobCmd) HandleInput(data wshrpc.CommandJobInputData) error {
	jm.lock.Lock()
	defer jm.lock.Unlock()

	if jm.cmd == nil || jm.cmdPty == nil {
		return fmt.Errorf("no active process")
	}

	if len(data.InputData64) > 0 {
		inputBuf := make([]byte, base64.StdEncoding.DecodedLen(len(data.InputData64)))
		nw, err := base64.StdEncoding.Decode(inputBuf, []byte(data.InputData64))
		if err != nil {
			return fmt.Errorf("error decoding input data: %w", err)
		}
		_, err = jm.cmdPty.Write(inputBuf[:nw])
		if err != nil {
			return fmt.Errorf("error writing to pty: %w", err)
		}
	}

	if data.SigName != "" {
		sig := unixutil.ParseSignal(data.SigName)
		if sig != nil && jm.cmd.Process != nil {
			err := jm.cmd.Process.Signal(sig)
			if err != nil {
				return fmt.Errorf("error sending signal: %w", err)
			}
		}
	}

	if data.TermSize != nil {
		err := jm.setTermSize_withlock(*data.TermSize)
		if err != nil {
			return err
		}

View on GitHub (pinned to a4447c1563)

Solutions

  1. Check the job's exit status before sending input; treat this error as 'job already finished' and stop writing
  2. Retry once after confirming the job process is still alive (jm.Cmd.ProcessState / exit check)
  3. Buffer input client-side and drop pending input when a job-exit event is received
  4. If it happens immediately at start, inspect MakeJobCmd/PTY setup and OS logs for pty allocation failures

Example fix

// before
err := jm.cmdPty.Write(inputBuf[:nw])
// after
if jm.Cmd == nil || jm.Cmd.Process == nil || jm.Cmd.ProcessState != nil {
    return fmt.Errorf("job not running, skipping pty write")
}
_, err := jm.cmdPty.Write(inputBuf[:nw])
Defensive patterns

Strategy: try-catch

Validate before calling

func jobWritable(jm *jobmanager.JobManager) bool {
    return jm != nil && jm.Cmd != nil && jm.Cmd.Process != nil && jm.Cmd.ProcessState == nil
}

Try / catch

if err := jm.HandleInput(data); err != nil {
    if strings.Contains(err.Error(), "error writing to pty") {
        // treat as job-finished: mark job ended, discard queued input
        return
    }
    return err
}

Prevention

When it happens

Trigger: Sending job input after the job process has already exited and the PTY has been closed; writing to a pty whose master fd hit an EIO because the slave side has no reader; a race between job termination and a queued input write.

Common situations: User types into a still-open terminal pane after the shell command finished; a script sends input with a delay while the process already consumed EOF and exited; job killed by a signal moments before input arrives.

Related errors


AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01). Data as JSON: /api/errors/4a76030e90c7af6f. Report an issue: GitHub.