View previous topic :: View next topic |
Author |
Message |
pjp Administrator
Joined: 16 Apr 2002 Posts: 20485
|
Posted: Mon Jan 29, 2024 5:28 am Post subject: How do you save / restore tmux sessions? |
|
|
My solution has grown over time, first with a simple way to either start a new session, or load an existing one.
Then I started to use multiple sessions, and I haven't completely integrated the start / single session manager and multi-session management solutions. My previous "save session" script would iterate over a session's windows and output something like this to a "session file": Code: | neww -t0 -n logs -c "/var/log"
neww -t1 -n dict -c "/usr/share/dict"
neww -t2 -n mounts -c "/mnt" | I'd then have to manually fix the first line, because "neww" can't create the session.
My current test script seems to have eliminated the manual work, but I'm wondering how others have solved it. I'm aware of a plugin, but it isn't in ::gentoo.
Output of my current test script: Code: | SESSION_NAME="Example"
new-session -d -s "${SESSION_NAME}" -c "/var/log"
rename-window -t "${SESSION_NAME}":0 "logs"
neww -t1 -n dict -c "/usr/share/dict"
neww -t2 -n mounts -c "/mnt" | The test script, which still needs some clean up: Code: | #!/bin/sh
SESSION_NAME=$(tmux display-message -p "#{session_name}")
for w in $(tmux list-windows |cut -d: -f1) ; do
if [ ${w} -eq 0 ] ; then
echo "SESSION_NAME=\"${SESSION_NAME}\""
newsession=$(echo "new-session -d -s \"\${SESSION_NAME}\" -c")
pane_id=$(tmux display-message -p -t ${SESSION_NAME}:${w} "#{pane_pid}")
wcwd=$(lsof -d cwd -aFn -p ${pane_id} |awk -F"/" '/^n/ { print "\"" substr($0,2,length($0)) "\""}')
# Output new-session line
echo "${newsession} ${wcwd}"
# Output rename-window line
echo $(echo "rename-window -t \"\${SESSION_NAME}\":${w} \"logs\"")
continue
fi
wdata=$(tmux display-message -p -t ${SESSION_NAME}:${w} "neww -t#{window_index} -n #{window_name} -c;#{pane_pid}")
winfo=${wdata%;*}
wcwd=$(lsof -d cwd -aFn -p ${wdata##*;} |awk -F"/" '/^n/ { print "\"" substr($0,2,length($0)) "\""}')
# echo "${winfo} ${wcwd}" >> "${SESSION_FILE_TS}"
echo "${winfo} ${wcwd}"
done | At this point it's a bit ugly, but I don't have any ideas for a cleaner solution that I want to undertake. All I've thought of is trying a simple config file so that session names, window names and window paths aren't hard coded, but that's quite a bit of work for what seems like a minor gain. _________________ Quis separabit? Quo animo? |
|
Back to top |
|
|
grknight Retired Dev
Joined: 20 Feb 2015 Posts: 1921
|
|
Back to top |
|
|
Hu Administrator
Joined: 06 Mar 2007 Posts: 22660
|
Posted: Mon Jan 29, 2024 4:14 pm Post subject: |
|
|
Do you specifically need Bourne compatibility, or would Bash be acceptable? If GNU Bash is acceptable, there are some simplifications possible.
On principal, I think all new scripts should start with set -eu or even set -euo pipefail, so that errors are diagnosed early and clearly, rather than manifesting as weirdly incomplete output or wrong commands.
pjp wrote: | Code: | SESSION_NAME=$(tmux display-message -p "#{session_name}") |
| This could be single-quoted instead of double-quoted, since you don't need the shell to expand anything here.
pjp wrote: | Code: | for w in $(tmux list-windows |cut -d: -f1) ; do |
| I think you could use tmux list-windows -F '#I' instead of invoking cut.
Since you don't need to do anything after the done, you could write this as: Code: | tmux list-windows -F '#I' | while read w; do | This would allow the loop to start immediately, without collecting all the tmux output first.
This output only seems to capture the active session. For me, I have multiple xterm windows open, each connected to a shared tmux, and each showing different groups of windows. Your script as shown can only save one xterm's worth of state.
pjp wrote: | Code: | if [ ${w} -eq 0 ] ; then |
| This does not work reliably. If the window numbered 0 has been closed or renumbered elsewhere, then $w -eq 0 is false for all windows, and your first-run special case never triggers. I think if you need to special-case this, then you need a dedicated shell variable telling you that this session is new. pjp wrote: | Code: | echo "SESSION_NAME=\"${SESSION_NAME}\"" |
| If GNU Bash is permitted, this could be done as printf 'SESSION_NAME=%q\n' "$SESSION_NAME", to delegate quoting to the shell. That would enable proper handling of weird session names, like DROP TABLE STUDENTS"; Bobby. Compare: Code: |
$ (SESSION_NAME='DROP TABLE STUDENTS"; Bobby'; echo "SESSION_NAME=\"${SESSION_NAME}\"")
SESSION_NAME="DROP TABLE STUDENTS"; Bobby"
$ (SESSION_NAME='DROP TABLE STUDENTS"; Bobby'; printf 'SESSION_NAME=%q\n' "${SESSION_NAME}")
SESSION_NAME=DROP\ TABLE\ STUDENTS\"\;\ Bobby
| Note the unescaped semicolon and unbalanced quotes in the echo form. pjp wrote: | Code: | newsession=$(echo "new-session -d -s \"\${SESSION_NAME}\" -c") |
| Why do you need $(echo X) here instead of just X? pjp wrote: | Code: | pane_id=$(tmux display-message -p -t ${SESSION_NAME}:${w} "#{pane_pid}") |
| Although it would add a bit of complication, you can capture the pane_pid during list-windows with a custom format, which would save going back to tmux for each window individually.
Note though that both forms misbehave when a window has more than one pane. In both forms, only the pid of the active pane is provided. The inactive pane is not mentioned, and so will not be saved. pjp wrote: | Code: | wcwd=$(lsof -d cwd -aFn -p ${pane_id} |awk -F"/" '/^n/ { print "\"" substr($0,2,length($0)) "\""}') |
| This can cause a lot of junk output from lsof if there are mounts that cannot be accessed. Additionally, it looks like readlink "/proc/$pane_id/cwd" gives the same result with much less work.
Similar comments apply to the non-first-run path. |
|
Back to top |
|
|
pjp Administrator
Joined: 16 Apr 2002 Posts: 20485
|
Posted: Thu Feb 01, 2024 5:31 am Post subject: |
|
|
I think that's the one I'd seen mentioned somewhere, but as far as I can tell, it isn't in ::gentoo (not by way of searching for 'tmux' or 'resu').
Hu wrote: | Do you specifically need Bourne compatibility, or would Bash be acceptable? If GNU Bash is acceptable, there are some simplifications possible. | It's not specifically needed, but I generally prefer to avoid bash-isms. "Vendor lock-in" bothers me, regardless of the vendor. There was another thread where you made a bash suggestion that was worth it. I can't recall the details at the moment.
Hu wrote: | Code: | SESSION_NAME=$(tmux display-message -p "#{session_name}") | This could be single-quoted instead of double-quoted, since you don't need the shell to expand anything here. | I've replaced double quotes with single quotes, and that was done out of habit for shell expansion.
Hu wrote: | Code: | tmux list-windows -F '#I' | while read w; do | This would allow the loop to start immediately, without collecting all the tmux output first. | That does work, and it seems unlikely I would have randomly found that in the man page. Although performance is never likely to be an issue with this script, I like that it is a simpler solution. The benefit of piping output to while / read sounds vaguely familiar, but I can't say as I've ever considered that detail.
Hu wrote: | This output only seems to capture the active session. For me, I have multiple xterm windows open, each connected to a shared tmux, and each showing different groups of windows. Your script as shown can only save one xterm's worth of state. | That is accurate. My original need was only one xterm with one tmux session of multiple windows. Now I'm using multiple tmux sessions, each with multiple windows. Once saving for a single session works, then I'll integrate multiple sessions.
Hu wrote: | Code: | if [ ${w} -eq 0 ] ; then | This does not work reliably. If the window numbered 0 has been closed or renumbered elsewhere, then $w -eq 0 is false for all windows, and your first-run special case never triggers. I think if you need to special-case this, then you need a dedicated shell variable telling you that this session is new. | I didn't like it when I wrote it, but nothing else came to mind. For the closed window situation, that could be worth failing until the issue is resolved. Merging from a previous saved state seems complicated, and possibly incorrect. A renumbered window seems intentional, so presumably what had been at 0 is no longer wanted in that position. Manual intervention seems the best approach. I don't think shifting remaining windows down toward 0 would be a good approach. In the near term, "if you break it, you fix it" seems the most practical approach.
Hu wrote: | If GNU Bash is permitted, this could be done as printf 'SESSION_NAME=%q\n' "$SESSION_NAME", to delegate quoting to the shell. That would enable proper handling of weird session names, like DROP TABLE STUDENTS"; Bobby. Compare: | Does /usr/bin/printf not address the problem? As I read the output, the single quotes make for an ugly session name, which might require tmux to reject the name. Otherwise that could justify bash. Code: | $ (SESSION_NAME='DROP TABLE STUDENTS"; Bobby'; /usr/bin/printf 'SESSION_NAME=%q\n' "${SESSION_NAME}")
SESSION_NAME='DROP TABLE STUDENTS"; Bobby' |
Hu wrote: | Why do you need $(echo X) here instead of just X? | It isn't needed. I believe I originally copied the other similar line for 'neww' and expected it would look similar. I hadn't noticed echo had become unnecessary.
Hu wrote: | pane_id=$(tmux display-message -p -t ${SESSION_NAME}:${w} "#{pane_pid}")[/code] Although it would add a bit of complication, you can capture the pane_pid during list-windows with a custom format, which would save going back to tmux for each window individually. | I'm often torn between readability versus reducing the number of external calls. I don't seem to retain much about parameter expansion between uses, including the name, which means finding notes or documentation always takes extra time. In this case, I chose getting it to work. I'll probably use parameter expansion.
I presume you mean something like this: Code: | $ tmux list-windows -F '#I #{pane_pid}' | while read window_index pane_pid
@> do
@> echo "Window Index: ${window_index} Pane PID: ${pane_pid}"
@> done | I don't often use while loops, but that does seem nicer.
Hu wrote: | Note though that both forms misbehave when a window has more than one pane. In both forms, only the pid of the active pane is provided. The inactive pane is not mentioned, and so will not be saved. | Looking through the variable name list in the FORMATS section, it isn't readily apparent how that might be addressed. I'm only using one pane per window, so that might be a future improvement.
Hu wrote: | This can cause a lot of junk output from lsof if there are mounts that cannot be accessed. Additionally, it looks like readlink "/proc/$pane_id/cwd" gives the same result with much less work. | I absolutely did NOT like the lsof solution. I obviously don't browse around /proc enough. #{pane_current_path} might also work.
Hu wrote: | Similar comments apply to the non-first-run path. | I think you are referring to subsequent executions of the script after the first run?
Thanks for the comments! I hope to have an improved version this weekend. _________________ Quis separabit? Quo animo? |
|
Back to top |
|
|
Zucca Moderator
Joined: 14 Jun 2007 Posts: 3706 Location: Rasi, Finland
|
Posted: Thu Feb 01, 2024 8:40 am Post subject: |
|
|
O.O Thank you.
I think this saved me for reinventing the wheel. _________________ ..: Zucca :..
My gentoo installs: | init=/sbin/openrc-init
-systemd -logind -elogind seatd |
Quote: | I am NaN! I am a man! |
|
|
Back to top |
|
|
Hu Administrator
Joined: 06 Mar 2007 Posts: 22660
|
Posted: Thu Feb 01, 2024 5:34 pm Post subject: |
|
|
pjp wrote: | Hu wrote: | Code: | if [ ${w} -eq 0 ] ; then | This does not work reliably. ... | I didn't like it when I wrote it, but nothing else came to mind. |
Code: | first_in_session=1
tmux list-windows -F '#I' | while read w; do
if [[ "$first_in_session" = 1 ]]; then
first_in_session=0
newsession=$(...) | This way, the first pass always triggers that logic, and no later pass ever triggers it. pjp wrote: | For the closed window situation, that could be worth failing until the issue is resolved. | It will not fail at save time, as written. It will just create an incorrect session file, which will do the wrong thing when loaded. pjp wrote: | A renumbered window seems intentional, so presumably what had been at 0 is no longer wanted in that position. | It could be as simple as having run exit in window 0, and never creating a replacement window 0 before saving the session. Deliberately not restoring window 0 is fine, but as written, if you have no window 0, then your restore script has no new-session, which seems unlikely to be what you want. pjp wrote: | Manual intervention seems the best approach. I don't think shifting remaining windows down toward 0 would be a good approach. In the near term, "if you break it, you fix it" seems the most practical approach. | You could force the remaining windows to reappear with their proper numbers. The script seems most of the way there on that point, though I have not tried running it. pjp wrote: | Does /usr/bin/printf not address the problem? | It appears it does. I considered only the shell built-in printf, and found that busybox sh did not accept printf '%q\n', so I planned that an invocation of /bin/sh's printf may or may not understand %q, depending on whether sh is GNU bash, Debian's dash, or Busybox sh. Thus, before using the builtin printf with %q, I want to be sure the shell is GNU bash. pjp wrote: | As I read the output, the single quotes make for an ugly session name, which might require tmux to reject the name. Otherwise that could justify bash. | When I wrote the suggestion to use %q, I was thinking that the output was meant to be evaluated by bash, not by tmux. I agree, if tmux is the direct consumer, then you need to be more careful about how you quote the value. pjp wrote: | I presume you mean something like this: Code: | $ tmux list-windows -F '#I #{pane_pid}' | while read window_index pane_pid
@> do
@> echo "Window Index: ${window_index} Pane PID: ${pane_pid}"
@> done | I don't often use while loops, but that does seem nicer. | Yes, that is what I intended. pjp wrote: | Hu wrote: | Note though that both forms misbehave when a window has more than one pane. ... | Looking through the variable name list in the FORMATS section, it isn't readily apparent how that might be addressed. I'm only using one pane per window, so that might be a future improvement. | I agree, I do not see a ready fix for this, so I settled for warning that it is a problem as written, so at least it is a known issue, rather than an unknown one. pjp wrote: | Hu wrote: | Similar comments apply to the non-first-run path. | I think you are referring to subsequent executions of the script after the first run? | I meant that both the (window == 0) case and the else block (for the (window != 0) case) had similar commands, and that my comments about the window==0 case applied to the equivalent commands in the window!=0 case. I felt restating them would just waste the reader's time rereading the same remarks with different line numbers. I have found that some authors, when presented with review comments, will address exactly the review comments on the original lines, and not check whether the code they change as a result of the review was duplicated elsewhere, and therefore they do not fix the duplicated copies. I try to compensate for this by including at least a brief reminder that duplicate copies exist and need attention. |
|
Back to top |
|
|
pjp Administrator
Joined: 16 Apr 2002 Posts: 20485
|
Posted: Thu Feb 15, 2024 1:44 am Post subject: |
|
|
Thanks for the feedback Hu. Very helpful, as always.
Here's the current iteration that I had mostly finished by the time of my last post. I still need to integrate it with the script I use to start tmux. I haven't corrected everything, and I forgot about selecting the active window for some sessions. For now that is still manual. But, it's an improvement: example-save-session.sh: | #!/bin/sh
SESSION_FILE_TS=$(/bin/date "+%Y-%m-%dT%H%M%SZ")
SESSION_NAME=$(tmux display-message -p '#{session_name}')
SESSION_FILE="${SESSION_NAME}_${SESSION_FILE_TS}"
format='#{session_name} #I #{window_name} #{pane_pid} #{pane_current_path}'
tmux list-windows -F "${format}" |
while read s_name w_index w_name p_pid p_curr_path; do
# Handle creating a new session
if [ ${w_index} -eq 0 ] ; then
/usr/bin/printf "SESSION_NAME='%q'\n" "${s_name}" >> "${SESSION_FILE}"
# Output new-session line
/usr/bin/printf "new-session -d -s '%q' -c '%q'\n" "${s_name}" "${p_curr_path}" >> "${SESSION_FILE}"
# Output rename-window line
/usr/bin/printf "rename-window -t '%q':%q '%q'\n" "${s_name}" "${w_index}" "${w_name}" >> "${SESSION_FILE}"
continue
fi
# Handle the default condition of creating a new window.
# Output neww line
/usr/bin/printf "neww -t '%q':'%q' -n '%q' -c '%q'\n" "${s_name}" "${w_index}" "${w_name}" "${p_curr_path}" >> "${SESSION_FILE}"
done | Result: Code: | SESSION_NAME='Example'
new-session -d -s 'Example' -c '/tmp/example'
rename-window -t 'Example':0 'Zero'
neww -t 'Example':'1' -n 'logs' -c '/var/log' | It is loaded using: tmux source-file <file name>
And I just now noticed that I should have removed the 'continue' after rewriting that section.
Handling the "window 0 doesn't exist" condition and optionally selecting an active window introduces some complications that make me wonder if this should be considered "beyond a shell script." Python or something else might make it easier to modify an existing saved session file or not create one at all if there are no changes. _________________ Quis separabit? Quo animo? |
|
Back to top |
|
|
Hu Administrator
Joined: 06 Mar 2007 Posts: 22660
|
Posted: Thu Feb 15, 2024 3:58 pm Post subject: |
|
|
Your latest iteration does not clear the file before you begin, though the use of a high precision timestamp makes it unlikely you will get a collision.
You now invoke /usr/bin/printf 3 times on the window 0 path, and another time on the general window path. For the small maximum count of windows, this is probably fine, but it seems unnecessary.
Some possible improvements:- At the start of the script, detect whether printf (builtin) supports %q. If yes, set a shell variable to the internal version:
If no, set it to the program: Code: | printf=/usr/bin/printf | Then, in all uses, invoke $printf. This way, you get the shell internal version when it works, and fall back on the external version if your shell cannot provide a sufficient printf.Combine the three uses of printf into one use, on the window 0 path.Since every step in the loop now writes to the same place, you could move the >> "${SESSION_FILE}" down to after done instead of processing it on each call.
I did not realize this was meant to edit existing sessions. I thought you would always dump an active tmux session, and completely replace a prior session file with the results of the new dump. |
|
Back to top |
|
|
|
|
You cannot post new topics in this forum You cannot reply to topics in this forum You cannot edit your posts in this forum You cannot delete your posts in this forum You cannot vote in polls in this forum
|
|