Project 1: PTY Echo Chamber

Build a PTY proxy that runs a shell, forwards I/O in both directions, and logs raw terminal bytes for later replay.

Quick Reference

Attribute Value
Difficulty Level 3: Advanced
Time Estimate 1 week
Main Programming Language C (Alternatives: Rust, Go, Python)
Alternative Programming Languages Rust, Go, Python
Coolness Level Level 4: Hardcore Tech Flex
Business Potential 1: The “Resume Gold”
Prerequisites Unix file descriptors, fork/exec basics, C structs and pointers, basic signal handling
Key Topics PTYs, termios modes, job control, byte logging, SIGWINCH propagation

1. Learning Objectives

By completing this project, you will:

  1. Build a working implementation of pty echo chamber and verify it with deterministic outputs.
  2. Explain the underlying Unix and terminal primitives involved in the project.
  3. Diagnose common failure modes with logs and targeted tests.
  4. Extend the project with performance and usability improvements.

2. All Theory Needed (Per-Concept Breakdown)

PTY Interposition Pipeline (PTY + termios + job control)

  • Fundamentals PTY Interposition Pipeline (PTY + termios + job control) is the core contract that makes the project behave like a real terminal tool. It sits at the boundary between raw bytes and structured state, so you must treat it as both a protocol and a data model. The goal of the fundamentals is to understand what assumptions the system makes about ordering, buffering, and ownership, and how those assumptions surface as user-visible behavior. Key terms include: PTY master/slave, controlling terminal, process groups, raw mode, SIGWINCH. In practice, the fastest way to gain intuition is to trace a single input through the pipeline and note where it can be delayed, reordered, or transformed. That exercise reveals why PTY Interposition Pipeline (PTY + termios + job control) needs explicit invariants and why even small mistakes can cascade into broken rendering or stuck input.

  • Deep Dive into the concept A deep understanding of PTY Interposition Pipeline (PTY + termios + job control) requires thinking in terms of state transitions and invariants. You are not just implementing functions; you are enforcing a contract between producers and consumers of bytes, and that contract persists across time. Most failures in this area are caused by violating ordering guarantees, dropping state updates, or misunderstanding how the operating system delivers events. This concept is built from the following pillars: PTY master/slave, controlling terminal, process groups, raw mode, SIGWINCH. A reliable implementation follows a deterministic flow: Allocate PTY master, derive slave path. -> fork(); child calls setsid() and opens slave as controlling terminal. -> Child dup2()s slave onto stdin/out/err and execs shell. -> Parent sets client terminal to raw mode and starts forwarding bytes. -> Resize events are propagated to PTY via ioctl(TIOCSWINSZ).. From a systems perspective, the tricky part is coordinating concurrency without introducing races. Even in a single-threaded loop, multiple events can arrive in the same tick, so you need deterministic ordering. This is why many implementations keep a strict sequence: read, update state, compute diff, render. Another subtlety is error handling and recovery. A robust design treats errors as part of the normal control flow: EOF is expected, partial reads are expected, and transient failures must be retried or gracefully handled. The deep dive should also cover how to observe the system, because without logs and trace points, you cannot reason about correctness. When you design the project, treat each key term as a source of constraints. For example, if a term implies buffering, decide the buffer size and how overflow is handled. If a term implies state, decide how that state is initialized, updated, and reset. Finally, validate your assumptions with deterministic fixtures so you can reproduce bugs. From a systems perspective, the tricky part is coordinating concurrency without introducing races. Even in a single-threaded loop, multiple events can arrive in the same tick, so you need deterministic ordering. This is why many implementations keep a strict sequence: read, update state, compute diff, render. Another subtlety is error handling and recovery. A robust design treats errors as part of the normal control flow: EOF is expected, partial reads are expected, and transient failures must be retried or gracefully handled. The deep dive should also cover how to observe the system, because without logs and trace points, you cannot reason about correctness. From a systems perspective, the tricky part is coordinating concurrency without introducing races. Even in a single-threaded loop, multiple events can arrive in the same tick, so you need deterministic ordering. This is why many implementations keep a strict sequence: read, update state, compute diff, render. Another subtlety is error handling and recovery. A robust design treats errors as part of the normal control flow: EOF is expected, partial reads are expected, and transient failures must be retried or gracefully handled. The deep dive should also cover how to observe the system, because without logs and trace points, you cannot reason about correctness. From a systems perspective, the tricky part is coordinating concurrency without introducing races. Even in a single-threaded loop, multiple events can arrive in the same tick, so you need deterministic ordering. This is why many implementations keep a strict sequence: read, update state, compute diff, render. Another subtlety is error handling and recovery. A robust design treats errors as part of the normal control flow: EOF is expected, partial reads are expected, and transient failures must be retried or gracefully handled. The deep dive should also cover how to observe the system, because without logs and trace points, you cannot reason about correctness.

  • How this fit on projects This concept is the backbone of the project because it defines how data and control flow move through the system.

  • Definitions & key terms

    • PTY master/slave -> a paired pseudo-terminal device; the master is controlled by the multiplexer and the slave appears as a real terminal to the child process
    • controlling terminal -> the terminal associated with a session that delivers job-control signals and foreground input
    • process groups -> a set of related processes that receive terminal signals together
    • raw mode -> terminal mode that disables line editing and delivers input byte-by-byte
    • SIGWINCH -> signal delivered when the terminal window size changes
  • Mental model diagram (ASCII)

[Input] -> [PTY Interposition Pipeline (PTY + termios + job control)] -> [State] -> [Output]
  • How it works (step-by-step, with invariants and failure modes)

    1. Allocate PTY master, derive slave path.
    2. fork(); child calls setsid() and opens slave as controlling terminal.
    3. Child dup2()s slave onto stdin/out/err and execs shell.
    4. Parent sets client terminal to raw mode and starts forwarding bytes.
    5. Resize events are propagated to PTY via ioctl(TIOCSWINSZ).
  • Minimal concrete example

    // open PTY and launch shell
int master = posix_openpt(O_RDWR | O_NOCTTY);
grantpt(master); unlockpt(master);
char *slave = ptsname(master);
if (fork() == 0) {
    setsid();
    int sfd = open(slave, O_RDWR);
    ioctl(sfd, TIOCSCTTY, 0);
    dup2(sfd, 0); dup2(sfd, 1); dup2(sfd, 2);
    execlp("/bin/sh", "sh", NULL);
    _exit(1);
}
  • Common misconceptions

    • “A PTY is just a pipe” -> it is a terminal device with line discipline and window size.
    • “Raw mode is optional” -> without raw mode, prefix keys and control sequences break.
    • “SIGWINCH is cosmetic” -> resizing changes program layout and cursor math.
  • Check-your-understanding questions

    • Why must the child call setsid() before opening the PTY slave?
    • What changes when you disable ICANON and ECHO in termios?
    • How does SIGWINCH reach applications running in the pane?
  • Check-your-understanding answers

    • setsid() creates a new session so the PTY can become the controlling terminal.
    • Input becomes byte-at-a-time and is no longer echoed by the kernel.
    • tmux updates PTY window size and the kernel delivers SIGWINCH to the foreground group.
  • Real-world applications

    • tmux and screen
    • expect/automation tools
    • interactive debuggers
  • Where you’ll apply it

  • References

    • The Linux Programming Interface - Ch. 64 (PTYs)
    • Advanced Programming in the UNIX Environment - Ch. 18 (termios)
    • man 7 pty
  • Key insights PTY Interposition Pipeline (PTY + termios + job control) works best when you treat it as a stateful contract with explicit invariants.

  • Summary You now have a concrete mental model for PTY Interposition Pipeline (PTY + termios + job control) and can explain how it affects correctness and usability.

  • Homework/Exercises to practice the concept

    • Write a PTY wrapper that runs /bin/cat and logs bytes.
    • Toggle raw mode on stdin and observe how Ctrl-C behaves.
    • Resize your terminal and verify TIOCGWINSZ changes.
  • Solutions to the homework/exercises

    • Use posix_openpt/grantpt/unlockpt and forward bytes via poll.
    • Disable ISIG and ECHO, then re-enable and compare behavior.
    • Call ioctl(TIOCGWINSZ) before and after resizing.

3. Project Specification

3.1 What You Will Build

A CLI tool that allocates a PTY, launches a shell on the slave, forwards bytes between the user terminal and the PTY master, and logs the raw stream to disk in a deterministic format.

3.2 Functional Requirements

  1. Requirement 1: Spawn a child shell attached to a PTY slave and keep it as the foreground job in that PTY.
  2. Requirement 2: Forward input from the user terminal to the PTY master and output from the PTY master back to stdout.
  3. Requirement 3: Log all bytes in both directions with timestamps and direction tags.
  4. Requirement 4: Handle terminal resize (SIGWINCH) and propagate window size to the PTY.
  5. Requirement 5: Cleanly exit when the child shell exits and return its exit status.
  6. Requirement 6: Support a configurable log path and log rotation by size.

3.3 Non-Functional Requirements

  • Performance: Avoid blocking I/O; batch writes when possible.
  • Reliability: Handle partial reads/writes and cleanly recover from disconnects.
  • Usability: Provide clear CLI errors, deterministic output, and helpful logs.

3.4 Example Usage / Output

    $ ./pty_echo --log /tmp/pty.log --shell /bin/bash
[pty] master=/dev/pts/3 slave=/dev/pts/4
[pty] child shell pid=4123

$ printf "hi"
hi

$ exit
[pty] child exited status=0
[pty] log bytes=9
[exit code: 0]

$ ./pty_echo --log /root/pty.log
[error] cannot open log path: permission denied
[exit code: 2]

3.5 Data Formats / Schemas / Protocols

    Log record format (text for readability):
TIMESTAMP_NS DIRECTION LEN BYTES_HEX

Example:
1735689600123456789 OUT 5 48 65 6c 6c 6f
1735689600123460000 IN 1 0a

3.6 Edge Cases

  • Child exits immediately (exec failure).
  • Partial writes to PTY master or stdout.
  • Client terminal is too small (< 10x4).
  • Log file path is unwritable or on a full disk.
  • SIGWINCH arrives during a write.

3.7 Real World Outcome

This section defines a deterministic, repeatable outcome. Use fixed inputs and set TZ=UTC where time appears.

3.7.1 How to Run (Copy/Paste)

make
./pty_echo --log /tmp/pty.log --shell /bin/bash

3.7.2 Golden Path Demo (Deterministic)

The “success” demo below is a fixed scenario with a known outcome. It should always match.

3.7.3 If CLI: provide an exact terminal transcript

    $ ./pty_echo --log /tmp/pty.log --shell /bin/bash
[pty] master=/dev/pts/3 slave=/dev/pts/4
[pty] child shell pid=4123

$ printf "hi"
hi

$ exit
[pty] child exited status=0
[pty] log bytes=9
[exit code: 0]

Failure Demo (Deterministic)

    $ ./pty_echo --log /root/pty.log
[error] cannot open log path: permission denied
[exit code: 2]

3.7.8 If TUI

At least one ASCII layout for the UI:

    +------------------------------+
    | PTY Echo Chamber           |
    | [content area]               |
    | [status / hints]             |
    +------------------------------+

4. Solution Architecture

4.1 High-Level Design

    +-----------+     +-----------+     +-----------+
    |  Client   | <-> |  Server   | <-> |  PTYs     |
    +-----------+     +-----------+     +-----------+

4.2 Key Components

| Component | Responsibility | Key Decisions | |-----------|----------------|---------------| | PTY manager | Allocates PTY pair, configures slave, and tracks window size. | Use posix_openpt + grantpt/unlockpt for portability. | | I/O forwarder | Moves bytes between stdin/stdout and PTY master. | Use non-blocking fds with poll to avoid stalls. | | Logger | Serializes byte stream to deterministic log records. | Log both directions with monotonic timestamps. |

4.4 Data Structures (No Full Code)

    struct LogRecord {
    uint64_t ts_ns;   // monotonic timestamp
    uint8_t  dir;     // 0 = IN, 1 = OUT
    uint32_t len;     // byte count
    uint8_t  data[4096];
};

4.4 Algorithm Overview

Key Algorithm: Bidirectional PTY forwarding loop

  1. Set stdin and PTY master to non-blocking.
  2. poll() on stdin and PTY master for readability.
  3. When stdin readable, read bytes, log as IN, write to PTY master.
  4. When PTY master readable, read bytes, log as OUT, write to stdout.
  5. On SIGWINCH, update PTY window size via ioctl(TIOCSWINSZ).

Complexity Analysis:

  • Time O(n) in bytes forwarded; Space O(1) per buffer.

5. Implementation Guide

5.1 Development Environment Setup

    cc --version
make --version
tmux -V

5.2 Project Structure

    pty-echo/
|-- src/
|   |-- main.c
|   |-- pty.c
|   |-- termios.c
|   `-- log.c
|-- include/
|   |-- pty.h
|   `-- log.h
|-- tests/
|   `-- test_log.c
`-- Makefile

5.3 The Core Question You’re Answering

“How can a program sit between a shell and a terminal without either one knowing?”

5.4 Concepts You Must Understand First

  1. PTYs
    • Why it matters and how it impacts correctness.
  2. termios modes
    • Why it matters and how it impacts correctness.
  3. job control
    • Why it matters and how it impacts correctness.
  4. byte logging
    • Why it matters and how it impacts correctness.
  5. SIGWINCH propagation
    • Why it matters and how it impacts correctness.

5.5 Questions to Guide Your Design

  • How will you handle partial reads/writes on PTY and stdout?
  • What log format allows deterministic replay?
  • How will you propagate SIGWINCH to the PTY without races?

    5.6 Thinking Exercise

Trace a single keypress from the user terminal to the child process, listing each syscall and buffer it passes through.

5.7 The Interview Questions They’ll Ask

  • Why do terminal programs break if you forget to set raw mode?
  • What is a controlling terminal and who owns it?
  • How does job control interact with foreground process groups?

    5.8 Hints in Layers

  • Start with a simple poll loop that just echoes bytes.
  • Log input and output separately to confirm direction.
  • Add SIGWINCH handling after I/O forwarding works.

5.9 Books That Will Help

| Topic | Book | Chapter | |——-|——|———| | PTYs | The Linux Programming Interface | Ch. 64 | | Termios | Advanced Programming in the UNIX Environment | Ch. 18 | | Job control | The Linux Programming Interface | Ch. 34 |

5.10 Implementation Phases

Phase 1: Foundation (1 week)

Goals:

  • Establish the core data structures and loop.
  • Prove basic I/O or rendering works.

Tasks:

  1. Implement the core structs and minimal main loop.
  2. Add logging for key events and errors.

Checkpoint: You can run the tool and see deterministic output.

Phase 2: Core Functionality (1 week)

Goals:

  • Implement the main requirements and pass basic tests.
  • Integrate with OS primitives.

Tasks:

  1. Implement remaining functional requirements.
  2. Add error handling and deterministic test fixtures.

Checkpoint: All functional requirements are met for the golden path.

Phase 3: Polish & Edge Cases (1 week)

Goals:

  • Handle edge cases and improve UX.
  • Optimize rendering or I/O.

Tasks:

  1. Add edge-case handling and exit codes.
  2. Improve logs and documentation.

Checkpoint: Failure demos behave exactly as specified.

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
I/O model blocking vs non-blocking non-blocking avoids stalls in multiplexed loops
Logging text vs binary text for v1 easier to inspect and debug

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Unit Tests Validate components parser, buffer, protocol
Integration Tests Validate interactions end-to-end CLI flow
Edge Case Tests Handle boundary conditions resize, invalid input

6.2 Critical Test Cases

  1. Forwarding preserves byte order for a known input stream.
  2. Log output matches recorded bytes from /bin/echo.
  3. Resize triggers TIOCSWINSZ and child observes new size.

    6.3 Test Data

text Input: "printf \033[31mRED\033[0m"; Expect log to include ESC[31m and ESC[0m sequences.


7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |———|———|———-| | Proxy exits on Ctrl-C | SIGINT hits parent instead of child | Put terminal in raw mode or forward signals explicitly. | | Frozen output | Blocking read on PTY master | Use non-blocking I/O with poll. | | Wrong window size | Forgot to propagate SIGWINCH | Handle SIGWINCH and update PTY size. |

7.2 Debugging Strategies

  • Use strace/dtruss to confirm read/write directions.
  • Log bytes as hex to diagnose escape sequences.

    7.3 Performance Traps

  • Writing logs synchronously without buffering can stall the PTY.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add a –shell flag to choose the child program.
  • Support a –max-log-size with rotation.

    8.2 Intermediate Extensions

  • Implement replay that renders the log to the terminal.
  • Add JSON log format alongside text logs.

    8.3 Advanced Extensions

  • Implement compression for large logs.
  • Add timestamp-driven replay with speed control.

9. Real-World Connections

9.1 Industry Applications

  • Terminal multiplexers
  • Remote shell gateways
  • Session recording tools
  • tmux: terminal multiplexer
  • script(1): session recorder

    9.3 Interview Relevance

  • Event loops, terminal I/O, and state machines are common interview topics.

10. Resources

10.1 Essential Reading

  • The Linux Programming Interface by Michael Kerrisk - Ch. 64
  • Advanced Programming in the UNIX Environment by W. Richard Stevens - Ch. 18

    10.2 Video Resources

  • Terminal I/O and PTYs overview (conference talk or lecture).

    10.3 Tools & Documentation

  • stty (inspect terminal modes): stty (inspect terminal modes)
  • script (record I/O): script (record I/O)
  • Project 2: ANSI Escape Sequence Renderer - Extends these ideas

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain the core concept without notes
  • I can explain how input becomes output in this tool
  • I can explain the main failure modes

11.2 Implementation

  • All functional requirements are met
  • All test cases pass
  • Code is clean and well-documented
  • Edge cases are handled

11.3 Growth

  • I can identify one thing I’d do differently next time
  • I’ve documented lessons learned
  • I can explain this project in a job interview

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Tool runs and passes the golden-path demo
  • Deterministic output matches expected snapshot
  • Failure demo returns the correct exit code

Full Completion:

  • All minimum criteria plus:
  • Edge cases handled and tested
  • Documentation covers usage and troubleshooting

Excellence (Going Above & Beyond):

  • Add at least one advanced extension
  • Provide a performance profile and improvement notes