From c55309e14ad175101d3b5fec842e201a5ff7e214 Mon Sep 17 00:00:00 2001 From: Nuru Date: Sun, 29 Dec 2024 11:48:19 -0800 Subject: [PATCH] Better handling of terminals that do not respond to queries --- docs/ReleaseNotes-v4.md | 53 +++++++++++++++------ rootfs/etc/profile.d/_01-launch-warning.sh | 54 ++++++++++++++++++---- rootfs/etc/profile.d/_07-term-mode.sh | 38 +++++++++++++++ rootfs/etc/profile.d/_10-colors.sh | 9 +++- rootfs/templates/wrapper-body.sh | 5 +- 5 files changed, 134 insertions(+), 25 deletions(-) diff --git a/docs/ReleaseNotes-v4.md b/docs/ReleaseNotes-v4.md index 5b139fd8..df0ddcac 100644 --- a/docs/ReleaseNotes-v4.md +++ b/docs/ReleaseNotes-v4.md @@ -4,17 +4,28 @@ #### Better Shell Management +##### Equal Treatment for Multiple Shells in a Single Container + A much requested feature, Geodesic no longer exits the container when the first shell exits. Instead, the container runs until all shells have exited. This means you can now run multiple shells inside the container, and exit them in any order; you no longer have to keep track of which shell was the first one launched. Unfortunately, this also means that you can no longer detach and reattach to a shell. +A side benefit of this is that previously, if did something like `trap handler EXIT` in your +top-level shell, there was a good chance the handler would not run because the shell will +be killed (SIGKILL, `kill -9`) rather than shut down cleanly. Now, there is a much greater +likelihood that the shells will shut down in an orderly manner and run their exit hooks. + +##### New Capability for Multiple Shells with One Container per Shell + However, Geodesic now supports another much requested feature: launching a new container each time you run Geodesic. This is done by setting the `ONE_SHELL` environment variable to "true" or passing `--one-shell` on the command line. This allows you to run multiple versions of Geodesic, and also allows you to detach from a shell and reattach to it later. +##### External Command to Stop Geodesic + Not a new feature, but one that many people were not aware of: you can kill the running Geodesic container with the command `geodesic stop`. This will stop the container, and it will be automatically removed (assuming you started it with `geodesic`). Now, however, @@ -22,26 +33,41 @@ there is the possibility you will have several running containers. If this is th `geodesic stop` will list the running containers by name. You can then pass the name as an argument to `geodesic stop` and it will stop that one. +##### Cleanup Commands on Shell Exit and Container Exit + Another old feature few people knew about: you can have Geodesic automatically run a command when a shell exits. This was done by creating an executable command named `geodesic_on_exit` and putting it in your `$PATH`. This feature has been enhanced in 2 ways: -1. Now, you can set the name of the command to run when the shell exits via `ON_SHELL_EXIT` - (defaults to `geodesic_on_exit`). Also new: the `ON_SHELL_EXIT` command will have available to it the short ID of the container in which it was running, via the - environment variable `GEODESIC_CONTAINER_EXITING`. +1. Now you can set the name of the command to run when the shell exits via `ON_SHELL_EXIT` + (defaults to `geodesic_on_exit`). Also new: the `ON_SHELL_EXIT` command will have available + to it the short ID and name of the container in which it was running, via the + environment variables `GEODESIC_EXITING_CONTAINER_ID` and `GEODESIC_EXITING_CONTAINER_NAME`, + respectively. 2. You can use the new environment variable `ON_CONTAINER_EXIT` to configure a different - command to run only when the container exits. + command to run only when the container exits. It will also have the container ID and name + available to it via the same environment variables. Be aware that the commands are called on a best-effort basis when the Geodesic launch wrapper exits. If you detach from a shell, the wrapper will run then and call `ON_SHELL_EXIT`. If you reattach to the shell, the wrapper is not involved, so quitting the shell or container will not run the cleanup command. -Alternately, if you quit 2 shells at nearly the same time, the `ON_CONTAINER_EXIT` -command may be called twice. This is because the wrapper does not know which shell -is the last one to exit; it calls the command when the container has stopped -before shell exit processing has finished. +Alternately, if you quit 2 shells at nearly the same time, for example by +running `geodesic stop`, the `ON_CONTAINER_EXIT` command may be called twice. +This is because the wrapper calls the command when the container has stopped +before shell exit processing has finished, and both shells fit the criterion. + +Now that shells normally exit cleanly (pretty much as long as you do not +run `docker kill geodesic`), you may find that you get more reliable behavior +out of + +```bash +trap exit_handler EXIT +``` + +to run on each shell completion. #### Better Configuration Management @@ -202,21 +228,22 @@ both of which are described after this section. These directories are specified as a comma-separated list of directories (or files) relative to the host user's home directory. If items in the list are not present on the host, they will be silently ignored. -- `HOMEDIR_MOUNTS` is a list of directories to mount. It is set by default to `".aws,.config,.emacs.d,.geodesic,.gitconfig,.kube,.ssh,.terraform.d"`. +- `HOMEDIR_MOUNTS` is a list of directories to mount. It is set by default to `".aws,.config,.emacs.d,.geodesic,.kube,.ssh,.terraform.d"`. If you set it to something else, it will replace the default list. Ensure that your Geodesic configuration directory (default is `$HOME/.config/geodesic`) is mounted. - `HOMEDIR_ADDITIONAL_MOUNTS` is a list of additional directories to mount. It is appended to the `HOMEDIR_MOUNTS` list of directories to mount. This allows you to add to the defaults without overriding them. -Note that you can mount files this way, but it will be difficult to know inside of Geodesic if -the files are on the host or private to the container. When you mount directories, the default -Geodesic prompt will tell you whether the current directory is on the host or not. +Note that you can mount files this way, but there are issues with that, especially when mapping file ownership. + Many files that used to be placed directly in the `/conf` directory can now be placed in subdirectories. Many applications now support the `XDG Base Directory Specification`, which specifies that configuration files should be placed in `$XDG_CONFIG_HOME` (defaults to `~/.config/`). This directory is mounted by default. -- `~/.gitconfig` can be moved to `~/.config/git/config`. +- `~/.gitconfig` can be moved to `~/.config/git/config`. If you mount `~/.gitconfig` directly, and have file ownership + mapping enabled, `git config` will not be able to modify the file. Instead, you should mount `~/.config/` and the + `git/config` inside will work as expected. - `~/.bash_profile` can be moved to `~/.bash_profile.d/` and sourced from there. however, we do not recommend this, and we do not mount `~/.bash_profile.d` by default. Instead, we recommend you put scripts you want to run inside Geodesic in `~/.config/geodesic/defaults/preferences.d/` where they will be sourced automatically. If you want to share diff --git a/rootfs/etc/profile.d/_01-launch-warning.sh b/rootfs/etc/profile.d/_01-launch-warning.sh index 317f92ba..ad46665c 100644 --- a/rootfs/etc/profile.d/_01-launch-warning.sh +++ b/rootfs/etc/profile.d/_01-launch-warning.sh @@ -11,11 +11,49 @@ # Specifically, this guards against: # docker run -it cloudposse/geodesic:latest-debian | bash -printf 'printf "\\nIf piping Geodesic output into a shell, do not attach a terminal (-t flag)\\n\\r" >&2; exit 8;' -# In case this output is not being piped into a shell, hide the warning message. -# Use backspaces, because carriage returns may be ignored or translated into newlines. -printf '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' -printf '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' -printf ' ' -printf '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' -printf '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' +# If the warning message is longer than the terminal width, +# on some terminals the message may be truncated. In that case, +# all the erasure characters will not be printed, and the message will not be erased. +# So we take pains to make the printed message appear short on the terminal. +# We do that in part by using backspaces to erase the message one character at a time, +# so the cursor never advances. Then we eval the message with backspaces removed. +# We have to then add some extra characters to erase the eval command. +function warn_if_piped() { + local saved_stty=$(stty -g) + stty -echo -opost + + local mesg cmd xb m fx bx feval beval + mesg='printf "\\n\\rIf piping Geodesic output into a shell, do not attach a terminal (-t flag)\\n\\n\\r" >&2; exit 8; ' + cmd="printf \"m='\$m'; eval \\\${m//\$'\\b'/}\"" + + xb=0 + m="" + for i in $(seq 1 ${#mesg}); do + m+="${mesg:$i-1:1}" + [[ "${mesg:$i-1:1}" == '\' ]] && xb=$((xb + 1)) || m+=$'\b' + done + + fx="" + bx="" + for i in $(seq 1 $xb); do + bx+=$'\b' + fx+=" " + done + m+=$bx + m+=$fx + m+=$bx + + # Cover the eval command itself. Tough to compute the exact number of backspaces needed, + # due to escapes and non-printing characters. So we just add a few extra, because + # extra backspaces are ignored. + feval=$(printf "%*s" 18) + beval="${feval// /$'\b'}" + + eval "$cmd" + echo -n "$beval$feval$beval" + + stty "$saved_stty" +} + +warn_if_piped +unset -f warn_if_piped diff --git a/rootfs/etc/profile.d/_07-term-mode.sh b/rootfs/etc/profile.d/_07-term-mode.sh index c398c4ce..d6f28f2a 100644 --- a/rootfs/etc/profile.d/_07-term-mode.sh +++ b/rootfs/etc/profile.d/_07-term-mode.sh @@ -14,12 +14,49 @@ # # At some point we may introduce other methods to determine the terminal's color scheme. +# First, at startup, let's try an OSC query. If we get no response, we will assume light mode +# and disable further queries. + +function _verify_terminal_queries_are_supported() { + if tty -s && [[ -n "$(tput setaf 1 2>/dev/null)" ]]; then + local saved_state=$(stty -g) + [[ $GEODESIC_TRACE =~ "terminal" ]] && echo "$(tput setaf 1)* TERMINAL TRACE: Checking if terminal responds to color queries...$(tput sgr0)" >&2 + stty -echo + echo -ne '\e]10;?\a\e]11;?\a' >/dev/tty + # If 2 seconds is not enough at startup, then the terminal is either non-responsive or too slow. + IFS=: read -s -t 2 -d $'\a' x fg_rgb + exit_code=$? + [[ $exit_code -gt 128 ]] || exit_code=0 + IFS=: read -s -t 0.5 -d $'\a' x bg_rgb + ((exit_code += $?)) + stty "$saved_state" + if [[ $exit_code -gt 128 ]] || [[ -z $fg_rgb ]] || [[ -z $bg_rgb ]]; then + if [[ $GEODESIC_TRACE =~ "terminal" ]]; then + echo "$(tput setaf 1)* TERMINAL TRACE: Terminal did not respond to OSC 10 and 11 queries. Disabling color mode detection.$(tput sgr0)" >&2 + fi + export GEODESIC_TERM_COLOR_AUTO=unsupported + fi + fi +} + +_verify_terminal_queries_are_supported + # Normally this function produces no output, but with -b, it outputs "true" or "false", # with -bb it outputs "true", "false", or "unknown". (Otherwise, unknown assume light mode.) # With -m it outputs "dark" or "light", with -mm it outputs "dark", "light", or "unknown". # and always returns true. With -l it outputs integer luminance values for foreground # and background colors. With -ll it outputs labels on the luminance values as well. function _is_term_dark_mode() { + [[ ${GEODESIC_TERM_COLOR_AUTO} == "unsupported" ]] && case "$1" in + -b) echo "false" ;; + -bb) echo "unknown" ;; + -m) echo "light" ;; + -mm) echo "unknown" ;; + -l) echo "0 1000000000" ;; + -ll) echo "Foreground luminance: 0, Background luminance: 1000000000" ;; + *) return 1 ;; + esac && return 0 + local x fg_rgb bg_rgb fg_lum bg_lum exit_code # Do not try to auto-detect if we are not in a terminal @@ -35,6 +72,7 @@ function _is_term_dark_mode() { # Timeout of 2 was not enough when waking for sleep. # The second read should be part of the first response, should not need much time at all regardless. IFS=: read -s -t 1 -d $'\a' x fg_rgb + exit_code=$? if [[ $exit_code -gt 128 ]] || [[ -z $fg_rgb ]] && [[ ${GEODESIC_TERM_COLOR_SIGNAL} == "true" ]]; then IFS=: read -s -t 30 -d $'\a' x fg_rgb exit_code=$? diff --git a/rootfs/etc/profile.d/_10-colors.sh b/rootfs/etc/profile.d/_10-colors.sh index ccaa122d..0525b210 100755 --- a/rootfs/etc/profile.d/_10-colors.sh +++ b/rootfs/etc/profile.d/_10-colors.sh @@ -36,7 +36,12 @@ function update-terminal-color-mode() { [[ "$quiet" == "true" ]] || echo "No terminal detected." >&2 elif [[ -z "$(tput op 2>/dev/null)" ]]; then [[ "$quiet" == "true" ]] || echo "Terminal does not appear to support color." >&2 + elif [[ ${GEODESIC_TERM_COLOR_AUTO} == "unsupported" ]]; then + [[ "$quiet" == "true" ]] || echo "Terminal does not support color mode detection." >&2 + else + [[ "$quiet" == "true" ]] || echo "Terminal did not respond to color queries." >&2 fi + # "light" is historical default for unknown terminals. new_mode="light" fi @@ -312,8 +317,8 @@ function _update-terminal-color-mode-sigwinch() { unset GEODESIC_TERM_COLOR_SIGNAL } -if _is_color_term; then - # We install the trap handler whether GEODESIC_TERM_COLOR_AUTO is set or not, +if [[ ${GEODESIC_TERM_COLOR_AUTO} != "unsupported" ]] && _is_color_term; then + # We install the trap handler whether GEODESIC_TERM_COLOR_AUTO is set to "disabled" or "false", # because we will not be able to detect the change in that variable if # it started out disabled and then someone enables it. diff --git a/rootfs/templates/wrapper-body.sh b/rootfs/templates/wrapper-body.sh index b8c069aa..174978cc 100755 --- a/rootfs/templates/wrapper-body.sh +++ b/rootfs/templates/wrapper-body.sh @@ -1,5 +1,5 @@ # Default directory mounts for the user's home directory -homedir_default_mounts=".aws,.config,.emacs.d,.geodesic,.gitconfig,.kube,.ssh,.terraform.d" +homedir_default_mounts=".aws,.config,.emacs.d,.geodesic,.kube,.ssh,.terraform.d" function require_installed() { if ! command -v $1 >/dev/null 2>&1; then @@ -184,7 +184,8 @@ function _on_shell_exit() { } function _on_container_exit() { - export GEODESIC_CONTAINER_EXITING="${CONTAINER_ID:0:12}" + export GEODESIC_EXITING_CONTAINER_ID="${CONTAINER_ID:0:12}" + export GEODESIC_EXITING_CONTAINER_NAME="${DOCKER_NAME}" _on_shell_exit [ -n "${ON_CONTAINER_EXIT}" ] && command -v "${ON_CONTAINER_EXIT}" >/dev/null && "${ON_CONTAINER_EXIT}" }