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 beView on GitHub (pinned to 1283425b60)
Solutions
- Ensure every Terminal that called InitRaw is closed (Terminal.Close()) before starting a new one.
- Check tncon.IsRunning() before calling Start and skip or wait when a session is already active.
- If the previous session is being torn down, wait for it to finish (Stop() then poll IsRunning() until false) before restarting.
- 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
- Always pair Terminal.InitRaw with Terminal.Close (use defer).
- Maintain one active raw terminal per process; track instances explicitly.
- Use tncon.IsRunning() as a guard before Start.
- After Stop(), allow the async reader cleanup to complete (poll IsRunning()) before restarting.
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
- failed to retrieve stdout mode: %w
- failed to set stdout mode: %w
- failed to set stdin mode: %w
- unexpected nil response from GetAssertion
- unexpected nil response from MakeCredential
AI-assisted analysis of gravitational/teleport@1283425b60 (2026-09-02).
Data as JSON: /api/errors/ab27d445ea78a554.
Report an issue: GitHub.