gravitational/teleport · error

a tncon session is already active

Error message

a tncon session is already active

What it means

tncon supports only one active console-input capture session per process. Start() checks a package-level `running` flag under a mutex and refuses to start a second session while one is active, returning this plain error. The running session must be stopped (Stop() / terminal Close()) before a new one can begin.

Source

Thrown at lib/client/tncon/tncon.go:137

}

// IsRunning determines if a tncon session is currently active.
func IsRunning() bool {
	runningMutex.Lock()
	defer runningMutex.Unlock()

	return running
}

// Start begins a new tncon session, capturing raw input events and emitting
// them as events. Only one session may be active at a time, but sessions can
// be stopped
func Start() error {
	runningMutex.Lock()
	defer runningMutex.Unlock()

	if running {
		return fmt.Errorf("a tncon session is already active")
	}

	running = true
	runningQuitHandle = C.CreateEventA(nil, C.TRUE, C.FALSE, nil)

	// Adding a buffer increases the speed of reads by a great amount,
	// since waiting on channel sends is the main chokepoint. Without
	// a sufficient buffer, the individual keystrokes won't be transmitted
	// quickly enough for them to be grouped as a VT sequence by Windows.
	sequenceBuffer = newBufferedChannelPipe(sequenceBufferSize)

	go readInputContinuous(runningQuitHandle)

	return nil
}

// Stop sets the stop event, requesting that the input reader quits. Subscriber
// channels will close shortly after calling, and the subscriber list will be

View on GitHub (pinned to 1283425b60)

Solutions

  1. Ensure every Terminal that called InitRaw is closed (Terminal.Close()) before starting a new one.
  2. Check tncon.IsRunning() before calling Start and skip or wait when a session is already active.
  3. If the previous session is being torn down, wait for it to finish (Stop() then poll IsRunning() until false) before restarting.
  4. Track Terminal instances in your application so lifecycle is explicit: one active raw terminal per process at a time.

Example fix

// before
err := tncon.Start()

// after
if tncon.IsRunning() {
    tncon.Stop()
    for tncon.IsRunning() {
        time.Sleep(10 * time.Millisecond)
    }
}
err := tncon.Start()
Defensive patterns

Strategy: try-catch

Validate before calling

if tncon.IsRunning() {
    return errors.New("cannot start: a tncon session is already active; close the existing Terminal first")
}

Type guard

func canStartTncon() bool {
    return !tncon.IsRunning()
}

Try / catch

err := t.InitRaw(true)
if err != nil && strings.Contains(err.Error(), "a tncon session is already active") {
    tncon.Stop()
    for tncon.IsRunning() {
        time.Sleep(10 * time.Millisecond)
    }
    err = t.InitRaw(true)
}
if err != nil {
    return trace.Wrap(err)
}

Prevention

When it happens

Trigger: Calling tncon.Start() (directly or via Terminal.InitRaw on Windows) while a previous session is still running: calling InitRaw on two Terminal instances without closing the first, forgetting Terminal.Close(), or a prior session whose reader goroutine has not yet observed the quit event.

Common situations: Opening a second interactive SSH session in the same process without closing the first; a prior session crashed or was abandoned without Terminal.Close(), leaving `running` true; rapid reconnect logic that calls Start again before Stop's async cleanup (readInputContinuous) finishes and resets the flag.

Related errors


AI-assisted analysis of gravitational/teleport@1283425b60 (2026-09-02). Data as JSON: /api/errors/ab27d445ea78a554. Report an issue: GitHub.