diff --git a/bin/board b/bin/board index 6fdc44f..0a89fb1 100755 --- a/bin/board +++ b/bin/board @@ -16,6 +16,8 @@ renumbers the rest. Run it twice and nothing is duplicated. import os import shlex +import subprocess +import time import sys HERE = os.path.dirname(os.path.abspath(__file__)) @@ -28,7 +30,8 @@ BIN = os.path.join(os.path.dirname(HERE), "bin", "harness") def parse(argv): - opts = {"folder": os.getcwd(), "replace": "--replace" in argv} + opts = {"folder": os.getcwd(), "replace": "--replace" in argv, + "reset": "--reset" in argv} for i, a in enumerate(argv): if a == "--folder" and i + 1 < len(argv): opts["folder"] = argv[i + 1] @@ -76,6 +79,22 @@ def main(): h.say("board: %s (session %s, folder %s)" % (cfg["paperclip"]["project"], session, folder)) h.say("watching: %s" % task) + if opts["reset"]: + # Empty the session first, then build. --replace alone can run out of + # workspace slots on a half-built session and leave pages missing. + h.say("reset: closing every pane in %s" % session) + for w in tuios.windows(session): + subprocess.run(["tuios", "close-window", w["window_id"], "-s", session], + capture_output=True, text=True, timeout=30) + for _ in range(30): + if not tuios.windows(session): + break + time.sleep(0.5) + for w in tuios.workspaces(session): + subprocess.run(["tuios", "set-workspace-name", str(w["workspace"]), "", + "-s", session], capture_output=True, text=True, timeout=20) + h.say("reset: %s" % ("empty" if not tuios.windows(session) else "panes remain, building anyway")) + if opts["replace"]: for name, _, _ in page_list: if tuios.close_pane(name, session): diff --git a/bin/office b/bin/office index fbdce62..d34ba77 100755 --- a/bin/office +++ b/bin/office @@ -24,6 +24,7 @@ producing a second copy of the same project. import json import os import shlex +import socket import subprocess import sys @@ -167,6 +168,41 @@ exit 0 +HOSTS = { + "nixos-desktop": "192.168.20.13", ".13": "192.168.20.13", "13": "192.168.20.13", + "sam-4screen-desktop": "192.168.20.27", "sam-4screen-desktop-1": "192.168.20.27", + ".27": "192.168.20.27", "27": "192.168.20.27", + "sam-thinkpad": "192.168.20.51", ".51": "192.168.20.51", "51": "192.168.20.51", +} + + +def local_addresses(): + """The addresses and names that mean this machine.""" + names = {socket.gethostname(), socket.gethostname().split(".")[0]} + try: + out = subprocess.run(["ip", "-4", "-o", "addr", "show", "scope", "global"], + capture_output=True, text=True, timeout=10).stdout + for line in out.splitlines(): + parts = line.split() + if len(parts) >= 4: + names.add(parts[3].split("/")[0]) + except Exception: + pass + return names + + +def resolve_host(name): + """A host name, or the address ssh can actually reach for it.""" + if not name: + return "" + return HOSTS.get(name, name) + + +def is_this_machine(name): + addr = resolve_host(name) + return addr in local_addresses() + + def run_on_remote(host, folder, opts): """Adopt a folder that lives on another machine, from where you are sitting. @@ -175,7 +211,13 @@ def run_on_remote(host, folder, opts): over ssh on that machine, and only the last step — showing the session — is the client's own business. """ - here = os.path.dirname(os.path.abspath(__file__)) + addr = resolve_host(host) + probe = subprocess.run(["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=8", addr, "true"], + capture_output=True, text=True, timeout=30) + if probe.returncode != 0: + raise h.Refused(3, "cannot log in to %s (%s). Set up a key first: ssh-copy-id %s" + % (host, addr, addr)) + here = os.path.dirname(os.path.realpath(__file__)) inner = [os.path.join(here, "office"), folder, "--no-switch"] for flag, value in (("--project", opts["project"]), ("--session", opts["session"]), ("--new", opts["new"])): if value: @@ -184,7 +226,7 @@ def run_on_remote(host, folder, opts): inner.append("--dry") cmd = " ".join(shlex.quote(a) for a in inner) h.say("on %s: %s" % (host, cmd)) - out = subprocess.run(["ssh", "-o", "StrictHostKeyChecking=accept-new", host, cmd], + out = subprocess.run(["ssh", "-o", "StrictHostKeyChecking=accept-new", addr, cmd], capture_output=True, text=True, timeout=600) sys.stdout.write(out.stdout) if out.returncode != 0: @@ -201,6 +243,10 @@ def main(): raise h.Refused(2, "no folder at %s" % folder) host = opts["host"] or pj.this_machine() + if opts["host"] and not is_this_machine(opts["host"]): + return run_on_remote(opts["host"], folder, opts) + if opts["host"]: + h.say("host: %s (this machine)" % opts["host"]) # An adopted project already knows its session. Only a new one needs a # default, and a default that names a session nobody made is worse than an @@ -218,7 +264,11 @@ def main(): # the verb for exactly this; asking the person to run `tuios new` first was # the step everyone forgot. if opts["remote"]: - return run_on_remote(opts["remote"], folder, opts) + if is_this_machine(opts["remote"]): + h.say("host: %s (this machine)" % opts["remote"]) + opts["remote"] = "" + else: + return run_on_remote(opts["remote"], folder, opts) if opts["dry"]: h.say("would create a Paperclip project %r for %s" % (opts["new"] or opts["project"], folder)) diff --git a/docs/DOCMAP.md b/docs/DOCMAP.md index 6ebed58..7551a61 100644 --- a/docs/DOCMAP.md +++ b/docs/DOCMAP.md @@ -24,6 +24,7 @@ it is a pointer and should be deleted or replaced. | `docs/UI-LAYOUT.md` | this project | what each page must show, where each fact comes from, and the three candidate layouts | | `docs/PLAN-INTERFACE.md` | this project | the plan: Paperclip object → screen, what exists, what is missing, and in what order | | `docs/SESSIONS.md` | this project | the two kinds of session, where Pi's conversations are, and the plan for the junk TUIOS sessions | +| `docs/STARTUP.md` | this project | starting a project: the three commands, the rule about machine and session, and the rebuild levels | | `docs/TEST-PLAN.md` | this project | the test plan: every step, every key, every expected result | | `docs/CLEAN-TESTS.md` | this project | the two clean tests: a new project from nothing, and adopting an existing folder | | `docs/RUNS-AND-MERGE.md` | this project | what a task, a run and a merge are, with the measured run counts | diff --git a/docs/STARTUP.md b/docs/STARTUP.md new file mode 100644 index 0000000..3456375 --- /dev/null +++ b/docs/STARTUP.md @@ -0,0 +1,68 @@ +# Starting a project: the three commands and the rule + +Replaces the previous project's four commands. There are three now, and one rule that removes +most of the confusion. + +## The rule + +> **The folder and the session live on the same machine.** +> If you are typing inside a session, on the machine that owns the folder, you type +> `bin/office ` and nothing else. + +`--host` exists for exactly one case: the folder is on a different machine from the shell you are +typing in. It is stated once; after that the project file remembers it. + +## The three commands + +| What you want | Command | +|---|---| +| adopt a folder | `bin/office --project ""` | +| adopt a folder Paperclip has never seen | `bin/office --new ""` | +| adopt a folder that lives on another machine | `bin/office --project "" --host nixos-desktop` | +| repair or rebuild the panes | `bin/board`, `bin/board --replace`, `bin/board --reset` | +| close everything and start again | `bin/project-reset ` | + +Nothing else is needed. In particular you do **not** create the session by hand any more: the +previous design needed `tuios new --host …` because `office` could not create a session. This one +can, through the daemon's own verb, so the second command is gone. + +## What `bin/office` does, in order + +1. Reads the folder. If another machine is named, checks first whether that name is *this* + machine, and runs locally if it is. (The previous version once tried to `ssh` to itself.) +2. Resolves a host name to an address ssh can reach: `nixos-desktop`/`.13` → `192.168.20.13`, + `sam-4screen-desktop`/`.27` → `192.168.20.27`, `sam-thinkpad`/`.51` → `192.168.20.51`. +3. If it really is another machine: tests the login with `ssh -o BatchMode=yes
true`, + then runs the same command there, and prints how to attach. +4. On the machine that owns the folder: creates the Paperclip project if `--new` was given, or + adopts the existing one; writes `.harness/harness.toml`, `check.sh`, `NOTES.md` — never + overwriting anything that exists. +5. Creates the session if it is missing, on that machine's daemon. +6. Builds the eight panes, in order, each pane claimed by workspace **name**. + +The last step it cannot do is show you the session. A program may not move your client: the +daemon refuses `switch-session` from a pane. Switch with TUIOS's own session switcher, or +`tuios attach --host `. + +## The three rebuild levels + +| Command | What it does | +|---|---| +| `bin/board` | creates what is missing, closes nothing | +| `bin/board --replace` | closes this project's panes, then builds all eight again | +| `bin/board --reset` | closes **every** pane in the session, waits for none to remain, clears the workspace names, then builds | + +`--reset` exists because `--replace` on a half-built session can run out of workspace slots and +leave pages missing. + +Inside a pane, `R` in `Repair>repair` rebuilds everything; `B` only adds what is missing. + +## Traps this design keeps from the previous one + +- A pane's command must carry the project, or a rebuild shows the wrong one. +- Compare a host name against this machine's own names and addresses before ssh, or you log in to + yourself. +- Never store a workspace number; TUIOS deletes an emptied workspace and renumbers the rest. +- A pane that never exits keeps a workspace alive. `Shell>shell` is that pane here. +- Scripts resolve their own directory with `realpath`, so a symlinked wrapper still finds its + siblings.