5

I know there are a bunch of similar questions on SO like this one or this one and maybe a couple more, but none of them seem to apply in my particular situation. My lack of understanding on how subprocess.Popen() works doesn't help either.

What i want to achieve is: launch a subprocess (a command line radio player) that also outputs data to the terminal and can also receive input -- wait for a while -- terminate the subprocess -- exit the shell. I am running python 2.7 on OSX 10.9

Case 1. This launches the radio player (but audio only!), terminates the process, exits.

import subprocess
import time

p = subprocess.Popen(['/bin/bash', '-c', 'mplayer http://173.239.76.147:8090'],
                     stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=False,
                     stderr=subprocess.STDOUT)
time.sleep(5)
p.kill()

Case 2. This launches the radio player, outputs information like radio name, song, bitrate, etc and also accepts input. It terminates the subprocess but it never exists the shell and the terminal becomes unusable even after using 'Ctrl-C'.

p = subprocess.Popen(['/bin/bash', '-c', 'mplayer http://173.239.76.147:8090'],
                     shell=False)
time.sleep(5)
p.kill()

Any ideas on how to do it? I was even thinking at the possibility of opening a slave-shell for the subprocess if there is no other choice (of course it is also something that I don't have a clue about). Thanks!

Community
  • 1
  • 1
skamsie
  • 2,614
  • 5
  • 36
  • 48
  • 1
    Note that instead of using `kill()` you should probably use `terminate()`. Applications usually handle `SIGTERM` better. In fact I can confirm you that using `.terminate()` the audio player exits saying `Exiting... (Quit)`, while using `.kill()` I don't get any feedback. However even in this case the terminal is a bit screwed. – Bakuriu Apr 06 '14 at 20:46
  • 2
    Also Why are you using `/bin/bash` to execute the command? You should use: `Popen(['mplayer', 'http://173.239.76.147:8090'])` to launch a simple subprocess. – Bakuriu Apr 06 '14 at 20:47
  • @Bakuriu Yes you are right about using `terminate()` instead of `kill()` and with `/bin/bash` it's a bad habit I guess. However none of your suggestions fix my actual problem unfortunately... – skamsie Apr 06 '14 at 20:50
  • unrelated: *always* feed/consume pipes that are set to `PIPE` otherwise the child process may block forever doing nothing either expecting input or trying to write output. If you want to discard input/output; use `subprocess.DEVNULL` or `DEVNULL=open(os.devnull, 'r+b', 0)` instead. – jfs Apr 07 '14 at 09:59
  • @J.F.Sebastian I'm not sure I understand how this works… I need the output of the subprocess to be displayed not to go to `/dev/null`. I don't know how to make sure that everything is 'flushed' after the process terminates. – skamsie Apr 07 '14 at 10:06
  • @HerrActress: if your actual code uses `p.stdin`, `p.stdout` e.g., by calling `p.communicate()` then it is fine. You could have mentioned it in a code comment. I often see that people copy-paste the code without understanding what it does. Related: [Python subprocess get children's output to file and terminal?](http://stackoverflow.com/q/4984428/4279) – jfs Apr 07 '14 at 10:23

2 Answers2

5

It seems like mplayer uses the curses library and when kill()ing it or terminate()ing it, for some reason, it doesn't clean the library state correctly.

To restore the terminal state you can use the reset command.

Demo:

import subprocess, time

p = subprocess.Popen(['mplayer', 'http://173.239.76.147:8090'])
time.sleep(5)
p.terminate()
p.wait()  # important!

subprocess.Popen(['reset']).wait()

print('Hello, World!')

In principle it should be possible to use stty sane too, but it doesn't work well for me.


As Sebastian points out, there was a missing wait() call in the above code (now added). With this wait() call and using terminate() the terminal doesn't get messed up (and so there shouldn't be any need for reset).

Without the wait() I sometimes do have problems of mixed output between the python process and mplayer.

Also, a solution specific to mplayer, as pointed out by Sebastian, is to send a q to the stdin of mplayer to quit it.

I leave the code that uses reset because it works with any program that uses the curses library, whether it correctly tears down the library or not, and thus it might be useful in other situations where a clean exit isn't possible.

Bakuriu
  • 98,325
  • 22
  • 197
  • 231
  • @Herr Actress: add `p.wait()` after `p.terminate()`: it avoids zombies and it *might* wait for the child process to restore the terminal by itself before exiting. Use `subprocess.check_call([..])` instead of `rc = Popen([..]).wait(); if rc != 0: raise Error`. You could also try `p.send_signal(signal.SIGINT)` instead of `p.terminate()` to see if it would allow to avoid calling `reset`. See also [Stop reading process output in Python without hang?](http://stackoverflow.com/q/4417962/4279) – jfs Apr 07 '14 at 10:05
  • @J.F. Sebastian I have tried with `p.wait()` after `terminate` but the process just hangs forever… Also it is very hard to 'build' a solution from your comments, so please feel free to post it as an answer if you feel there is a more pythonic way than using `reset` – skamsie Apr 07 '14 at 10:07
  • @HerrActress: `p.wait()` doesn't make the child process hang. It just waits for its exit status. It means that you have to use `p.kill()`. – jfs Apr 07 '14 at 10:12
  • 1
    @J.F.Sebastian Right, I believe that I incorrectly deleted that `wait()` sometime when investigating the problem. And indeed, adding it `reset` doesn't seem to be needed calling `terminate()` (while `kill()` still messes up the terminal for me). – Bakuriu Apr 07 '14 at 14:22
2

What i want to achieve is: launch a subprocess (a command line radio player) that also outputs data to the terminal and can also receive input -- wait for a while -- terminate the subprocess -- exit the shell. I am running python 2.7 on OSX 10.9

On my system, mplayer accepts keyboard commands e.g., q to stop playing and quit:

#!/usr/bin/env python
import shlex
import time
from subprocess import Popen, PIPE

cmd = shlex.split("mplayer http://www.swissradio.ch/streams/6034.m3u")
p = Popen(cmd, stdin=PIPE)
time.sleep(5)
p.communicate(b'q')

It starts mplayer tuned to public domain classical; waits 5 seconds; asks mplayer to quit and waits for it to exit. The output is going to terminal (the same place where the python script's output goes).

I've also tried p.kill(), p.terminate(), p.send_signal(signal.SIGINT) (Ctrl + C). p.kill() creates the impression that the process hangs. Possible explanation: p.kill() leaves some pipes open e.g., if stdout=PIPE then your Python script might hang at p.stdout.read() i.e., it kills the parent mplayer process but there might be a child process that holds the pipes open. Nothing hangs with p.terminate(), p.send_signal(signal.SIGINT) -- mplayer exits in an orderly manner. None of the variants I've tried require reset.


how should I go about having both input from Python and keyboard? Do I need two different subprocesses and how to redirect the keyboard input to PIPE?

It would be much simpler just to drop stdin=PIPE and call p.terminate(); p.wait() instead of p.communicate(b'q').

If you want to keep stdin=PIPE then the general principle is: read from sys.stdin, write to p.stdin until timeout happens. Given that mplayer expects one letter commands, you need to be able to read one character at at time from sys.stdin. The write part is easy: p.stdin.write(c) (set bufsize=0 to avoid buffering on Python side. mplayer doesn't buffer its stdin so you don't need to worry about it).

You don't need two different subprocesses. To implement timeout, you could use threading.Timer(5, p.stdin.write, [b'q']).start() or select.select on sys.stdin with timeout.

I guess something using the good old raw_input has nothing to do with it, or?

raw_input() is not suitable for mplayer because it reads the full lines but mplayer expects one character at a time.

Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • Indeed, this is always better than an explicit kill. Note however that the "clean" way to complete this is to add a timeout on clean child process exit and forcibly kill the process if the timeout elapses, which brings us back to the initial question :-) – André Caron Apr 07 '14 at 12:08
  • @AndréCaron: I've added explanation on what happens on `p.kill()`. If we are talking about hypotheticals; there is no "clean" way in general: `kill -9 someprocess_pid` may [fail to kill descendant processes spawned by `someprocess` (new process group, new session, daemon process)](http://stackoverflow.com/q/4789837/4279) or it even may [fail to kill the process itself ("unaccessible nfs")](http://unix.stackexchange.com/q/5642/1321). – jfs Apr 07 '14 at 12:52
  • @J.F.Sebastian +1 This works like a charm. I was wondering what is the `b` in the `p.communicate(b'q')` line? With this method the player does not accept input from the keyboard. Do you know how to fix that? Is it because of `stdin=PIPE`, but then again, without it `p. communicate` will not work, right? – skamsie Apr 07 '14 at 16:03
  • `b''` is a `bytes` literal in Python (2.7 and 3). It makes the code work on both Python 2.7 and Python 3 without any changes. Yes, if `stdin=PIPE` then `mplayer` accepts input from a pipe, not terminal. If you want to accept input from keyboard, remove `stdin=PIPE` and type `q` yourself. If you want *both* send input from Python and from keyboard then read keyboard input in Python and send it to the subprocess explicitly – jfs Apr 07 '14 at 16:25
  • @J.F.Sebastian Thank you very much. I know this has nothing to do with the initial question but how should I go about having both input from Python and keyboard? Do I need two different subprocesses and how to redirect the keyboard input to `PIPE`? I guess something using the good old `raw_input` has nothing to do with it, or? – skamsie Apr 07 '14 at 17:18
  • @J.F.Sebastian: I meant clean for the controlling process, not for the child. – André Caron Apr 10 '14 at 17:24