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.

  • 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 Nov 5 '15 at 15:33
up vote 22 down vote accepted

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!

  • Note that the python script requires root permissions to access the terminal. – martinkunev Aug 16 at 22:44

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.

Probably the simplest way is to use the reptyr command to switch the remote tty to the local (new) tty:

ssh user@remote
sudo apt install reptyr
ps auxw | grep -i vim
reptyr PID

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

If you get an error:

Unable to attach to pid 12345: Permission denied

Then 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.

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

  • 1
    Vim doesn't automatically write unsaved changes, no matter which signals were sent. – muru Sep 15 '17 at 5:22

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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