diff --git a/blog/content/posts/ssh-to-idle-screen-window.md b/blog/content/posts/ssh-to-idle-screen-window.md new file mode 100644 index 0000000..bc0f2c0 --- /dev/null +++ b/blog/content/posts/ssh-to-idle-screen-window.md @@ -0,0 +1,103 @@ +--- +title: "SSH to Idle Screen Window" +date: 2022-07-19T15:42:31-07:00 +tags: + - homelab + +--- +I've [written before]({{< ref "/posts/auto-screen" >}}) about setting up my ssh config so that I'll automatically join an existing screen session when ssh-ing to certain hosts, by setting `RemoteCommand screen -D -RR -p +` + +However, this has a couple of issues: +* It will _always_ create a new window within the session, even if an idle window exists. More often than not, I find myself immediately killing the new window and switching to an existing one. +* It doesn't restrict the rejoin to a named session - in my current usage, I typically only have a single `screen` session open at once, but that could change! + +This didn't seem like too hard of a problem to solve - in pseudo-code, the algorithm should look like: + +``` +if !(named session exists): + create and join named session +else + for window in named session: + if window is idle: + join window + else: + create and join new window in named session +end if +``` + +Translated into shell code, that looks like: + +``` +Host host_nickname + HostName hostname.avril + RequestTTY force + RemoteCommand sessionName=main; sessionSocket=$(ls /run/screen/S-pi | grep -E "^[[:digit:]]+\.$sessionName$"); if [ -z "$sessionSocket" ]; then screen -S "$sessionName"; else idleWindows=$(pgrep -P $(echo "$sessionSocket" | cut -d "." -f1) | xargs -I {} sh -c "echo -n '{}:'; pgrep -P {} | tr '\n' ':'; echo" | grep -E -v ':[[:digit:]]' | sed -n 's/:$//p'); if [ -z "$idleWindows" ]; then screen -D -RR -S "$sessionName" -p +; else screen -D -RR -S "$sessionName" -p $(tr '\0' '\n' ` contains sockets for each session started by ``, with names of the form `.`. If we wanted, we could declare `sessionUser=pi` to make this configurable, but I think it's very unlikely I'll be interested in sessions started by other users. + +## Create the session if it doesn't exist + +`if [ -z "$sessionSocket" ]; then screen -S "$sessionName"` - that is, "_if the variable_ `$sessionSocket` _is empty (i.e. there was nothing in_ `/run/screen/S-pi/` _matching the sessionName - no such named session is running), then create a session with that name_" + +That's the simple case. Moving into the `else` branch, where the session already exists and we need to join it, gets a bit more complicated. + +## Find idle windows + +`idleWindows=$(pgrep -P $(echo "$sessionSocket" | cut -d "." -f1) | xargs -I {} sh -c "echo -n '{}:'; pgrep -P {} | tr '\n' ':'; echo" | grep -E -v ':[[:digit:]]' | sed -n 's/:$//p')`[^1] [^2], broken down, is: + +* `echo "$sessionSocket" | cut -d "." -f1` gets the processId of the session +* `pgrep -P ` finds all processes that are children of that session - that is, the processes of the windows. + * This _works_, but I have the feeling that I'm misunderstanding or glossing over some complexity of `screen`'s operation (or, indeed, Unix process design) by relying on this fact. Note that [this SO answer](https://unix.stackexchange.com/a/556640/30828) recommends finding windows-in-a-screen by examining the output of `sudo w`, but it wasn't clear how to map from that output to the window numbers, nor how to tell whether an entry in `sudo w` corresponds with a `screen` window or not. In particular, `pts/` entries seem to appear for regular `ssh` connections, for screen session parents, and for each window, so that's pretty unclear. If you understand how to parse `sudo w` to identify screens-and-windows, please do let me know! +* `pgrep -P | xargs -I {} sh -c "echo -n '{}:'; pgrep -P {} | tr '\n' ':'; echo"` - for each window, list the child processIds of that window, colon-separated, on the same line. For instance, if there are windows with PID `001` and `002`, and window `001` has child process `003`, the output would look like: +``` +001:003: +002: +``` +* `pgrep -P | | grep -E -v ':[[:digit:]]' | sed -n 's/:$//p` filters the preceding output to any lines that _don't_ (`-v`) contain "_a colon followed by a digit_" (that is - it filters to only the lines referring to a window with no children; an idle window), then `sed` removes the trailing colons from each line so we recover the processIds of the idle windows. + * The grep here could have equivalently been `grep -E -v ':[^$]`, to search for "_any lines that don't contain a colon not at the end of the line_" => "_all lines whose only colon is at the end of the line_". + +After all that, we're left with a variable named `$idleWindows` which is a newline-delimited list of the processIds of idle windows of the `screen` session. + +We're nearly done! + +## Open screen to appropriate window + +`if [ -z "$idleWindows" ]; then screen -D -RR -p +; else screen -D -RR -p $(tr '\0' '\n' /environ`. I imagine that has a few nefarious purposes for black-/grey-hat applications... +* `pgrep` - here I've been, using `ps aux | grep` like some kind of _chump_! +* Hopefully, this has now cemented in my brain the `if [ -z "$variable" ]` syntax for checking emptiness - but I doubt it... +* `echo $$` to find the process of the current process + +And a few things I have yet to learn: +* I'm still a bit puzzled about why `perl` appears to behave differently when called in an `ssh` command than when called on the machine itself. When called on the machine, referencing capture groups uses the `$1` syntax that I'm used to; but, when `perl` is used in a command in `ssh`, I needed to use `\1` instead (apparently, [there is no standard across engines to insert capture groups into replacements](https://www.rexegg.com/regex-capture.html#replaceg10) - and note that that article lists Perl as using both). I even checked that the `perl --version` in both contexts was the same. This is a real puzzler! Because of this, you'll notice I haven't used `perl` here even though it could have replaced and consolidated a few commands. +* As I called out in [an earlier section]({{< ref "#find-idle-sections" >}}), I don't yet know how I could have parsed the output of `sudo w` or information from `netstat` to extract the information I was looking for here. I'm also a little stumped about why [this questioner](https://unix.stackexchange.com/questions/556594/how-do-i-find-what-process-is-running-in-a-particular-gnu-screen-window) found `screen -Q` commands to be so slow, but they [appeared to take negligible time for me](https://unix.stackexchange.com/a/556640/30828). + +[^1]: I had to use `xargs sh -c ""` here, rather than the more normal `xargs `, because the command contained semi-colons - without the `sh -c` wrapper, only the first part of `` would have been passed to `xargs`, and the following parts would have been executed independently, outside the `xargs` context. +[^2]: While testing this, I ran into a problem - I was testing by running `ssh ` from the command line, and I needed to enclose the command in quotes to prevent the `ssh` command only reading up until the first semi-colon and then stopping. However, I'd already used both double-quotes (to wrap the `sh -c` command) and single-quotes (within the `sh -c` command) - how could I combine both of those _and_ wrap them in a string identifier? Luckily, [this SO answer](https://stackoverflow.com/a/25941629/1040915) provided a solution - by wrapping a string in `$'...'`, you can use both double-quotes and escaped-single-quotes within it. This also required that the slashes in `\0` and `\n` be escaped, too - so it ended up looking like `\'\\0\'`. Not recommended - but good to know when necessary! +[^3]: There's another `fi` at the end of the full command to close the first `if` which created the session if it didn't exist. \ No newline at end of file diff --git a/blog/static/img/Why-would-you-do-this.png b/blog/static/img/Why-would-you-do-this.png new file mode 100644 index 0000000..b468b7b Binary files /dev/null and b/blog/static/img/Why-would-you-do-this.png differ