8

In some cases, when delegating work to an external process, it's useful to set a timeout on the command to prevent Emacs from hanging indefinitely.
Unfortunately, the following does not work.

(with-timeout (1 nil)
  (call-process "/usr/bin/bash" nil t nil "-c" "sleep 10"))

The same goes for shell-command.

Is there a way to set a timeout on these synchronous processes?
That is, I want the process to be killed automatically if it doesn't finish within a certain number of seconds. Is that possible?

Malabarba
  • 22,878
  • 6
  • 78
  • 163
  • I've never used `with-timeout`, but I have used `kill-process` and `delete-process`. – lawlist Mar 25 '15 at 01:06
  • Not sure, but maybe accepting process output would help. I.e. it would tell Emacs to take initiative and execute some Elisp code, supposedly waiting for the process to produce some output, perhaps, that would also be a good time to kill the process if it timed out. – wvxvw Mar 25 '15 at 09:57
  • @wvxvw The `call-process` above is setting the output to be printed on the current buffer (I get the same effect if I pass a different output buffer). Is that what you mean? – Malabarba Mar 25 '15 at 12:17
  • Nah, sorry, I confused few things. I meant this: https://www.gnu.org/software/emacs/manual/html_node/elisp/Accepting-Output.html but this is only relevant for the async processes. When I'm home, I'll look into the `call-process` sources, but now I'm beginning to suspect that there isn't a way to kill it on timeout. – wvxvw Mar 25 '15 at 12:52
  • Actually, I tried to look it up on Github: https://github.com/emacs-mirror/emacs/blob/0537943561a37b54467bec19d1b8afbeba8e1e58/src/callproc.c and, no, I don't see there any code that would do something like setting timeout for the process. – wvxvw Mar 25 '15 at 13:04
  • Emacs does not process events in `call-process` it basically just does `while(1) { read_output(); maybe_redisplay(); break_if_done(); }` – Jordon Biondo Mar 25 '15 at 15:38
  • This issue does not only deal with processes though. The user must make sure the code in `with-timeout` provides a way for emacs to process events else it does not work. For instance, this will never timeout `(with-timeout (1) (while t))` – Jordon Biondo Mar 25 '15 at 15:41

2 Answers2

5

While call-process is running, emacs will processing events, with-timeout will not work without this:

The timeout is checked whenever Emacs waits for some kind of external event (such as keyboard input, input from subprocesses, or a certain time); if the program loops without waiting in any way, the timeout will not be detected.

You can still use with-timeout with (semi) synchronous processes.

You will actually use a asynchronous process but will synchrously wait while it is running, Emacs will process events when you run sit-for, which you can run for 0 seconds. You can then use the timeout-forms argument of with-timeout to kill the process if it is still running when the timeout fires.

(with-current-buffer (get-buffer-create "*my-proc-buffer*")
  (let ((proc (start-process "myproc" (current-buffer) "bash" "-c" "sleep 4"))) ;; start an async process
    (with-timeout (2 (kill-process proc)) ;; on timeout, kill the process
      (while (process-live-p proc) ;; while process is running
        (sit-for .05)) ;; let emacs read events and run timmers (and check for timeout)
      (message "finished on time!!")))) ;; this will run only if there is no timeout
Jordon Biondo
  • 12,332
  • 2
  • 41
  • 62
  • I'm having a small problem with this. The output of the proces isn't guaranteed to be in the output buffer after the loop finishes. I'm working around it by calling another `(sit-for 0.2)` after the `while` loop, but that feels unstable. Is there a way to know that the output has been printed on the buffer? – Malabarba Mar 25 '15 at 17:22
  • I'm not sure about that. I would assume though that if you hit the timeout, the process is not done anyway so the output is not relevant. What are you doing such that the output is still needed even after a timeout? You could potentially switch to using a process filter and killing the process only X seconds have passed without output rather than after X seconds no matter what. – Jordon Biondo Mar 25 '15 at 17:40
  • Sorry, I wasn't clear. I don't care about the output when the timeout is hit. I'm saying the output isn't immediately printed after the process finishes *successfully*. – Malabarba Mar 25 '15 at 17:41
  • I think the problem is `(sit-for 0)`, emacs spends too much time letting the process run and not enough listening for events and inserting the text into the buffer. When I change mine to `(sit-for .05)` things work better and all text gets into my buffer. Even for a short process .05 seconds isn't a concerning overhead. – Jordon Biondo Mar 25 '15 at 17:57
  • I'm not too worried about overhead in this situation. The process is called 5-8 times and the entire thing is an interactive operation which already takes at least several seconds, so 1 more second won't make much of a difference. What worries me a little is stability, because I had to use `(sit-for 0.2)` for my local tests to pass, and even then once I pushed the changes the travis tests failed. – Malabarba Mar 25 '15 at 19:37
4

You can achieve this by just adding a GNU timeout invocation to your shell command, which circumvents needing to know any details about Emacs behavior. For example running:

$ timeout 5 sleep 10

Will return in 5 seconds, not 10 (timeout effectively presses Ctrl-C for you).

Joseph Garvin
  • 2,061
  • 8
  • 21
  • Thanks, I may have to go with this option if I can't achieve stability with the other one. However, I would have preferred not to rely on another utility, so that it would also work on Windows. – Malabarba Mar 25 '15 at 19:39