31

Let's say I have a X11 server that's hanging, keeping me from saving the work from the XTerm Vim session that X11 server controls. (Not GVim, just regular Vim-in-XTerm.)

Is there a way that I could (from a different terminal) tell the running Vim process to "save all & exit" from the command line? By sending a signal, or through some other means?

I know about Vim swap files, and that I could just kill Vim and recover from the swap. I am asking if there is a "cleaner" way.

1
  • 6
    If those Vim sessions were started with a server enabled (like gvim does by default), then you could use Vim's client-server functionality to do so. Another option might be to use reptyr to force the Vim programs to a new terminal, say the TTYs, and close them then.
    – muru
    Commented Nov 5, 2015 at 15:33

5 Answers 5

30

Having recently run into this problem (via another way: Vim running on a remote server, and I'd forgotten screen), I decided to hunt for a way.

The first idea was to look up the file descriptors used by Vim and try writing to it. Vim's fds point to the psedoterminal opened by the terminal emulator, naturally enough:

$ ls -l /proc/$(pgrep -n vim)/fd/
total 0
lrwx------ 1 muru muru 64 Nov 17 01:25 0 -> /dev/pts/14
lrwx------ 1 muru muru 64 Nov 17 01:25 1 -> /dev/pts/14
lrwx------ 1 muru muru 64 Nov 17 01:25 2 -> /dev/pts/14
lrwx------ 1 muru muru 64 Nov 17 01:25 3 -> socket:[99564312]

However, my initial few attempts failed:

echo '^[:wq^M' > /proc/$(pgrep -n vim)/fd/0
echo ':wq^M' > /proc/$(pgrep -n vim)/fd/0
echo ':wq^M' > /proc/$(pgrep -n vim)/fd/0
echo '^C' > /proc/$(pgrep -n vim)/fd/0
printf "%s" '^[:wqa!^M' > /proc/$(pgrep -n vim)/fd/0

The ^[ and ^M were obtained by CtrlVEsc and CtrlVEnter, respectively.

They all resulted in the characters showing up on the terminal (I was testing this out locally, before applying it to the remote session). Googling around, I found this SO post, using Python to write to the pseudoterminal device:

#!/usr/bin/python

import sys,os,fcntl,termios
if len(sys.argv) != 3:
   sys.stderr.write("usage: ttyexec.py tty command\n")
   sys.exit(1)
fd = os.open("/dev/" + sys.argv[1], os.O_RDWR)
cmd=sys.argv[2]
for i in range(len(cmd)):
   fcntl.ioctl(fd, termios.TIOCSTI, cmd[i])
fcntl.ioctl(fd, termios.TIOCSTI, '\n')
os.close(fd)

And trying it out on an interactive python shell worked:

$ sudo python3
Python 3.5.0 (default, Sep 20 2015, 11:28:25) 
[GCC 5.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os, fcntl, termios
>>> fd = os.open('/dev/pts/14', os.O_RDWR)
>>> a = '\033:wqa!\n'
>>> for i in a: fcntl.ioctl(fd, termios.TIOCSTI, i);
... 
b'\x1b'
b':'
b'w'
b'q'
b'a'
b'!'
b'\n'
>>> 

Done!

1
  • 2
    Note that the python script requires root permissions to access the terminal. Commented Aug 16, 2018 at 22:44
14

Install the reptyr command using the system's package manager, such as:

sudo apt install reptyr
pacman -Sy reptyr

Then use the reptyr command to switch the remote tty to the local (new) tty, as follows:

ssh user@remote-hostname
ps auxw | grep -i vim
reptyr PID

Where PID is the process ID from the ps command output.

Upon the error:

Unable to attach to pid 12345: Permission denied

Change the "ptrace scope" to 0:

sudo su -
echo 0 > /proc/sys/kernel/yama/ptrace_scope

Once the vim session has been swapped from the old session to the new one, save and quit as usual. Note that you may have to press Enter to refresh the console.

2
  • 4
    reptyr is absolutely the best tool for this kind of jobs... it's like a plunger for Linux :D
    – Avio
    Commented Feb 20, 2020 at 14:27
  • Seriously.... This should be the accepted answer. Very simple command that allows the terminal owning the process to be swapped to the current terminal. Simple and solid solution.
    – labradort
    Commented Jun 28, 2022 at 15:02
13

You can send commands to vim externally if you're running...

Vim servers

For example, doing:

vim --servername vim

will cause vim to launch a server with the name "vim". Call it twice and the new server will be called "vim1", call it thrice and it will be "vim2", etc. You might want to make an alias of that command.

You can know what server a particular instance is named by looking at the window title. When you see:

[No Name] + - VIM3

the server name is case-insensitively "VIM3" ("vim3" would refer to the same instance.). Note that if you see:

[No Name] + - VIM

that doesn't necessarily mean that it's got a server named "VIM". You can make sure the server exists by listing the server names with:

vim --serverlist

Still, the question only arises for "VIM", specifically. If you see "GVIM" or some other name with a number appended to it, then it does mean it's a server.

How to use client

Now, onto your question, you can save all and quit a given vim instance by doing, e.g.:

vim --servername vim2 --remote-send $'\e:wqa\n'

We use the escape to return to normal mode in case you're in insert or command mode. You can do something other than :wqa, but that seems the most appropriate to me because it will leave the swapfiles of buffers that could not be saved (because they're new and don't have a filename, etc.).

If you want to do so for all instances like in your case here, you can just loop through the server list like so:

for instance in $(vim --serverlist); do
  vim --servername $instance --remote-send $'\e:wqa\n'
done

If for some reason you don't like --remote-send, you can instead use --remote-expr which has the advantage that it will cause the client to output the result or the error that it might have caused, like so:

$ vim --servername vim2 --remote-expr 'execute("wqa")'

E141: No file name for buffer 1

Note that using Vim's server functionality requires that Vim was built with the +clientserver option.

2

What if you gracefully kill Vim?

kill -s 15 -p [PID for Vim]

kill -s (signal) 15 is called SIGTERM wich tells that process to gracefully shut itself down.

To get the PID (Process ID) of Vim use:
ps ax | grep vim

2
  • 1
    Vim doesn't automatically write unsaved changes, no matter which signals were sent.
    – muru
    Commented Sep 15, 2017 at 5:22
  • @muru Is there a way to accomplish just that?
    – tejasvi
    Commented Feb 9, 2021 at 12:23
1

I don't know whether it will work but you could try to install an autocmd listening to SigUSR1:

augroup save_and_exit | au!
    au SigUSR1 * wall | qa!
augroup END

Then, from another shell, you could run:

$ kill -s USR1 1234

Where 1234 is the pid of the Vim process. From Vim, you can get the latter via the getpid() function.

This requires the patch 8.2.0952 to get the SigUSR1 event.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.