#!/usr/bin/env bash
# Real demos for P002 "Every Linux Command You Actually Need". Runs on render01 (Docker).
# One Ubuntu 24.04 machine (a container, hostname "ubuntu") with a normal user, ana.
# Every command runs as ana in a real terminal (a pty), in the folder the prompt shows,
# and its real output goes to demo_output.json; terminal scenes show only that.
# Every number in video.json must match a value here. Re-run = new numbers = update the script.
set -euo pipefail
cd "$(dirname "$0")"
BOX=ce-p002
cleanup() { docker rm -f $BOX >/dev/null 2>&1 || true; }
cleanup
trap cleanup EXIT
W=/tmp/ce-p002
rm -rf $W && mkdir -p $W/img

# The machine: stock ubuntu:24.04 plus sudo and procps (ps, top, free), like a desktop install.
# Package lists are removed again, as on a fresh image: apt needs `apt update` first.
cat > $W/img/Dockerfile <<'EOF'
FROM ubuntu:24.04
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sudo procps >/dev/null \
 && rm -rf /var/lib/apt/lists/* \
 && userdel -r ubuntu 2>/dev/null; useradd -m -s /bin/bash ana \
 && echo 'ana ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ana
EOF
docker build -q -t ce-p002-ubuntu $W/img >/dev/null
docker run -d --name $BOX --hostname ubuntu ce-p002-ubuntu sleep infinity >/dev/null

python3 - "$BOX" "$W" <<'PY'
import json, random, re, subprocess, sys, time
BOX, W = sys.argv[1:3]
HOME = "/home/ana"
steps = {}

def sh(cmd, check=True):
    r = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    if check and r.returncode:
        raise SystemExit(f"FAILED: {cmd}\n{r.stdout}\n{r.stderr}")
    return r.stdout

def clean(text):
    text = re.sub(r"\x1b7.*?\x1b8", "", text, flags=re.S)   # apt's progress bar: drawn at the bottom between cursor save/restore
    text = re.sub(r"\x1b\[[0-9;?]*[A-Za-z]|\x1b[()][A-Z0-9]", "", text)   # colours, cursor codes
    # a terminal ends lines with \r\n; a progress bar redraws its line after a bare \r: keep the last draw
    lines = [l.rstrip("\r").split("\r")[-1] for l in text.split("\n")]
    return "\n".join(l.rstrip() for l in lines).strip("\n")

import fcntl, os, pty, select, struct, termios
READY = re.compile(rb"__READY__:(.*?):__ ")
shell = {"fd": None, "where": "~"}

def start_shell():
    """One interactive bash login for ana in a real terminal (64 columns), like opening a terminal window."""
    pid, fd = pty.fork()
    if pid == 0:
        fcntl.ioctl(0, termios.TIOCSWINSZ, struct.pack("HHHH", 40, 64, 0, 0))
        os.execvp("docker", ["docker", "exec", "-it", "-u", "ana", "-w", HOME, "-e", f"HOME={HOME}",
                             "-e", "TERM=xterm", BOX, "bash", "-i"])
    shell["fd"] = fd
    # Our marker prompt lets us see where each output ends and which folder bash is in (\w).
    # Echo off: the screen shows the command once, from our record, with the prompt ana would see.
    os.write(fd, b"stty -echo; PS1='__REA''DY__:\\w:__ '\n")   # split, so the echoed line can't match
    read_until_ready()

def read_until_ready(timeout=300):
    buf, end = b"", time.time() + timeout
    while time.time() < end:
        if select.select([shell["fd"]], [], [], 0.5)[0]:
            buf += os.read(shell["fd"], 65536)
            m = None
            for m in READY.finditer(buf):
                pass
            if m:
                return buf[:m.start()], m.group(1).decode()
    raise SystemExit(f"timed out; last output: {buf[-400:]!r}")

def run(key, cmd):
    """Type cmd into ana's terminal; record it with the prompt ana sees and its real output."""
    shown = f"ana@ubuntu:{shell['where']}$ {cmd}"
    os.write(shell["fd"], cmd.encode() + b"\n")
    raw, shell["where"] = read_until_ready()
    out = clean(raw.decode("utf-8", "replace"))
    steps[key] = {"cmd": shown, "out": out}
    print(f"== {key}\n{shown}\n{out}\n", flush=True)

def as_ana(cmd):
    subprocess.run(["docker", "exec", "-u", "ana", "-w", HOME, BOX, "bash", "-c", cmd], check=True)

# ---------------------------------------------------------------- ana's home folder
as_ana("mkdir -p Documents Pictures Videos projects/website")
as_ana("printf 'Meeting at 10:00\\nBack up the website\\nRenew the domain name\\n' > notes.txt")
as_ana("printf 'Q3 report\\nSales were up 4%%.\\n' > Documents/report.txt")
as_ana("printf 'month,spent\\nJuly,820\\nAugust,760\\n' > Documents/budget.csv")
as_ana("for i in 1 2 3; do fallocate -l 12M Pictures/beach$i.jpg; done; fallocate -l 420M Videos/holiday.mp4")
as_ana("printf '<h1>Hello</h1>\\n' > projects/website/index.html; printf 'h1 { color: navy; }\\n' > projects/website/style.css")
as_ana("printf '#!/bin/bash\\ntar -czf ~/backup.tar.gz -C ~ Documents\\necho \"Backed up Documents to backup.tar.gz\"\\n' > backup.sh; chmod 644 backup.sh")

# A web app's log: one line per event, a few kinds of error (fixed seed: same file every run)
rnd = random.Random(2)
errors = ["ERROR payment service timed out", "ERROR disk full: could not write upload",
          "ERROR database connection refused"]
lines, t = [], 9 * 3600
for i in range(480):
    t += rnd.randint(1, 9)
    stamp = f"2026-10-23 {t // 3600:02d}:{t // 60 % 60:02d}:{t % 60:02d}"
    p = rnd.random()
    msg = (rnd.choice(errors) if p < 0.05 else "WARN  slow request: 2.1 s" if p < 0.08
           else f"INFO  GET /{rnd.choice(['', 'shop', 'cart', 'login'])} 200 in {rnd.randint(4, 60)} ms")
    lines.append(f"{stamp} {msg}")
open(f"{W}/app.log", "w").write("\n".join(lines) + "\n")
sh(f"docker cp {W}/app.log {BOX}:{HOME}/app.log && docker exec {BOX} sh -c 'chown ana:ana {HOME}/app.log; chmod 644 {HOME}/app.log'")
start_shell()

# ---------------------------------------------------------------- hook: one line answers a real question
run("hook", "du -sh * | sort -rh | head -n 3")

# ---------------------------------------------------------------- 1. where am I
run("pwd", "pwd")
run("ls", "ls")
run("ls_l", "ls -l")
run("ls_a", "ls -a")
run("anatomy", "ls -l Documents")
run("cd_web", "cd projects/website")
run("ls_web", "ls")
run("cd_up", "cd ..")
run("pwd_up", "pwd")
run("cd_home", "cd")
run("pwd_home", "pwd")

# ---------------------------------------------------------------- 2. files and folders
run("mkdir", "mkdir backups")
run("touch", "touch todo.txt")
run("ls_new", "ls")
run("cp", "cp notes.txt backups/")
run("ls_backups", "ls backups")
run("mv", "mv todo.txt tasks.txt")
run("ls_mv", "ls")
run("rm", "rm tasks.txt")
run("rm_dir", "rm backups")
run("rm_r", "rm -r backups")
run("ls_rm", "ls")

# ---------------------------------------------------------------- 3. reading files
run("cat", "cat notes.txt")
run("wc", "wc -l app.log")
run("head", "head -n 3 app.log")
run("tail", "tail -n 3 app.log")

# ---------------------------------------------------------------- 4. searching
run("grep", 'grep "disk full" app.log')
run("grep_c", "grep -c ERROR app.log")
run("find_txt", 'find . -name "*.txt"')
run("find_big", "find . -size +100M")

# ---------------------------------------------------------------- 5. pipes and redirection
run("pipe_count", "grep ERROR app.log | wc -l")
run("pipe_head", "grep ERROR app.log | tail -n 2")
run("du", "du -sh *")
run("du_sort", "du -sh * | sort -rh")
run("redirect", "grep ERROR app.log > errors.txt")
run("redirect_wc", "wc -l errors.txt")
run("append", 'echo "Call the bank" >> notes.txt')
run("append_cat", "cat notes.txt")

# ---------------------------------------------------------------- 6. permissions and sudo
run("perm_ls", "ls -l backup.sh")
run("perm_denied", "./backup.sh")
run("chmod", "chmod +x backup.sh")
run("perm_ls2", "ls -l backup.sh")
run("perm_run", "./backup.sh")
run("whoami", "whoami")
run("shadow", "cat /etc/shadow")
run("sudo_whoami", "sudo whoami")

# ---------------------------------------------------------------- 7. processes
sh(f"docker exec -d -u ana {BOX} bash -c 'yes > /dev/null'")       # a program stuck using a whole CPU
time.sleep(4)
run("ps", "ps aux --sort=-%cpu | head -n 3")
pid = sh(f"docker exec {BOX} pgrep -x yes").split()[0]
run("kill", f"kill {pid}")
time.sleep(1)
run("ps_after", "ps aux --sort=-%cpu | head -n 3")

# ---------------------------------------------------------------- 8. disk and memory
run("df", "df -h /")
run("free", "free -h")

# ---------------------------------------------------------------- 9. packages and the network
run("curl_missing", "curl -I https://example.com")
run("apt_fail", "sudo apt install -y curl")
run("apt_update", "sudo apt update")
run("apt_install", "sudo apt install -y curl iputils-ping")
run("curl", "curl -I https://example.com")
run("ping", "ping -c 3 1.1.1.1")

# ---------------------------------------------------------------- 10. help
run("help", "ls --help | head -n 4")
run("man_missing", "man ls")
run("history", "history | tail -n 5")

steps["version"] = {"cmd": "versions", "out": sh(
    f"docker exec {BOX} sh -c '. /etc/os-release; echo $PRETTY_NAME; uname -r; bash --version | head -1; "
    f"ls --version | head -1'; date -u +%Y-%m-%dT%H:%MZ").strip()}
json.dump(steps, open("demo_output.json", "w"), indent=1)
print("saved demo_output.json")
PY
