Project 1: PTY Echo Chamber

Build a PTY bridge that proxies a shell and logs all I/O.

Quick Reference

Attribute Value
Difficulty Advanced
Time Estimate 1 week
Language C (Alternatives: Rust, Go)
Prerequisites File descriptors, fork/exec
Key Topics PTYs, raw mode, terminal control

1. Learning Objectives

By completing this project, you will:

  1. Allocate PTY master/slave pairs correctly.
  2. Create a controlling terminal for a child shell.
  3. Proxy I/O between terminal and PTY safely.
  4. Capture and log raw terminal output.
  5. Handle window resize propagation.

2. Theoretical Foundation

2.1 Core Concepts

  • PTYs: A master/slave pair; the slave looks like a real terminal to the child.
  • Controlling Terminal: The child process needs a controlling terminal to behave normally.
  • Raw Mode: Disables line buffering and echo so you see keystrokes directly.
  • Window Size: SIGWINCH events must update PTY size via ioctl.

2.2 Why This Matters

tmux is essentially a PTY broker. If you can build this bridge, tmux becomes understandable, not magic.

2.3 Historical Context / Background

GNU Screen and tmux were built to keep sessions alive across disconnects. PTY multiplexing is the core trick that makes that possible.

2.4 Common Misconceptions

  • “PTYs are just pipes”: They emulate terminal semantics, not just byte streams.
  • “Raw mode is optional”: Without it, special keys are handled by the terminal, not your program.

3. Project Specification

3.1 What You Will Build

A program pty_echo that:

  • Spawns a shell in a PTY slave
  • Proxies I/O between your terminal and the PTY master
  • Logs all output to a file

3.2 Functional Requirements

  1. PTY allocation: posix_openpt, grantpt, unlockpt.
  2. Child process: fork, setsid, set controlling terminal, exec shell.
  3. I/O proxy: Bidirectional read/write between STDIN and PTY master.
  4. Logging: Save raw output to a log file.
  5. Resize handling: Handle SIGWINCH and resize the PTY.

3.3 Non-Functional Requirements

  • Reliability: No terminal corruption after exit.
  • Usability: Clean restore of terminal modes.
  • Performance: Minimal overhead.

3.4 Example Usage / Output

$ ./pty_echo

3.5 Real World Outcome

You run the program and see a normal shell prompt. Everything you type appears normally, but the raw output is captured:

$ ./pty_echo
$ ls
$ exit

$ head -n 5 pty_log.txt
# includes escape codes and command output

4. Solution Architecture

4.1 High-Level Design

Terminal <-> PTY master <-> PTY slave <-> Shell

4.2 Key Components

Component Responsibility Key Decisions
PTY Setup Allocate master/slave posix_openpt
Child Setup setsid + TIOCSCTTY controlling terminal
Proxy Loop forward I/O select/poll
Logger write output append mode

4.3 Data Structures

struct termios orig;

4.4 Algorithm Overview

Key Algorithm: Proxy loop

  1. Use select/poll on STDIN and PTY master.
  2. If STDIN readable, forward to PTY master.
  3. If PTY readable, write to STDOUT and log.

Complexity Analysis:

  • Time: O(N) for N bytes
  • Space: O(B) for buffer

5. Implementation Guide

5.1 Development Environment Setup

cc -Wall -Wextra -o pty_echo pty_echo.c

5.2 Project Structure

pty-echo/
├── pty_echo.c
└── README.md

5.3 The Core Question You Are Answering

“How can a program sit between a terminal and a shell and make both think nothing changed?”

5.4 Concepts You Must Understand First

  1. PTY master/slave semantics
  2. Controlling terminal and sessions
  3. Raw vs cooked terminal modes

5.5 Questions to Guide Your Design

  1. Where do you store original termios settings?
  2. How do you restore terminal on exit or crash?
  3. How will you handle EOF from the shell?

5.6 Thinking Exercise

Draw a diagram of data flow for a single keypress. Trace from keyboard to shell and back.

5.7 The Interview Questions They Will Ask

  1. What is a PTY and why does it exist?
  2. Why does the child need a controlling terminal?
  3. What happens if you forget to restore terminal state?

5.8 Hints in Layers

Hint 1: Start with PTY allocation and shell spawn.

Hint 2: Add select/poll for bidirectional I/O.

Hint 3: Add SIGWINCH handling and resize.

5.9 Books That Will Help

Topic Book Chapter
PTYs “The Linux Programming Interface” Ch. 64
Terminal I/O “Advanced Programming in the UNIX Environment” Ch. 18

5.10 Implementation Phases

Phase 1: PTY allocation

  • posix_openpt, grantpt, unlockpt

Phase 2: Fork and exec

  • setsid, TIOCSCTTY, exec shell

Phase 3: Proxy loop

  • select/poll, logging, cleanup

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Unit Tests none minimal C program
Integration Tests interactive run and type
Edge Cases resize SIGWINCH

6.2 Critical Test Cases

  1. Terminal restored after exit.
  2. SIGWINCH updates PTY size.
  3. Log file contains output.

7. Common Pitfalls and Debugging

Pitfall Symptom Solution
No raw mode special keys fail set termios
Terminal left broken no echo restore termios
Missing controlling terminal shell behaves oddly setsid + TIOCSCTTY

8. Extensions and Challenges

8.1 Beginner Extensions

  • Add timestamped logging

8.2 Intermediate Extensions

  • Add file playback mode

8.3 Advanced Extensions

  • Add ANSI escape code parsing

9. Real-World Connections

  • tmux/screen internals
  • Terminal recording tools

10. Resources

  • TLPI Ch. 64 (PTYs)
  • man termios

11. Self-Assessment Checklist

  • I can explain PTY master vs slave
  • I can handle raw mode safely

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Shell runs inside PTY and I/O is proxied

Full Completion:

  • Logging + resize handling

Excellence (Going Above and Beyond):

  • Playback or escape parsing