I am working with the cwiid library, which is a library written in C, but used in python. The library allows me to use a Wiimote to control some motors on a robot. The code is running as a daemon on an embedded device without a monitor, keyboard, or mouse.
When I try to initialize the object:
import cwiid
while True:
try:
wm = cwiid.Wiimote()
except RuntimeError:
# RuntimeError exception thrown if no Wiimote is trying to connect
# Wait a second
time.sleep(1)
# Try again
continue
99% of the time, everything works, but once in a while, the library gets into some sort of weird state where the call to cwiid.Wiimote()
results in the library writing "Socket connect error (control channel)" to stderr, and python throwing an exception. When this happens, every subsequent call to cwiid.Wiimote()
results in the same thing being written to stderr, and the same exception being thrown until I reboot the device.
What I want to do is detect this problem, and have python reboot the device automatically.
The type of exception the cwiid library throws if it's in a weird state is also RuntimeError
, which is no different than a connection timeout exception (which is very common), so I can't seem to differentiate it that way. What I want to do is read stderr right after running cwiid.Wiimote()
to see if the message "Socket connect error (control channel)" appears, and if so, reboot.
So far, I can redirect stderr to prevent the message from showing up by using some os.dup()
and os.dup2()
methods, but that doesn't appear to help me read stderr.
Most of the examples online deal with reading stderr if you're running something with subprocess, which doesn't apply in this case.
How could I go about reading stderr to detect the message being written to it?
I think what I'm looking for is something like:
while True:
try:
r, w = os.pipe()
os.dup2(sys.stderr.fileno(), r)
wm = cwiid.Wiimote()
except RuntimeError:
# RuntimeError exception thrown if no Wiimote is trying to connect
if ('Socket connect error (control channel)' in os.read(r, 100)):
# Reboot
# Wait a second
time.sleep(1)
# Try again
continue
This doesn't seem to work the way I think it should though.
Under the hood, subprocess
uses anonymous pipes in addition to dups to redirect subprocess output. To get a process to read its own stderr, you need to do this manually. It involves getting an anonymous pipe, redirecting the standard error to the pipe's input, running the stderr-writing action in question, reading the output from the other end of the pipe, and cleaning everything back up. It's all pretty fiddly, but I think I got it right in the code below.
The following wrapper for your cwiid.Wiimote
call will return a tuple consisting of the result returned by the function call (None
in case of RuntimeError
) and stderr output generated, if any. See the tests
function for example of how it's supposed to work under various conditions. I took a stab at adapting your example loop but don't quite understand what's supposed to happen when the cwiid.Wiimote
call succeeds. In your example code, you just immediately re-loop.
Edit: Oops! Fixed a bug in example_loop()
where Wiimote
was called instead of passed as an argument.
import time
import os
import fcntl
def capture_runtime_stderr(action):
"""Handle runtime errors and capture stderr"""
(r,w) = os.pipe()
fcntl.fcntl(w, fcntl.F_SETFL, os.O_NONBLOCK)
saved_stderr = os.dup(2)
os.dup2(w, 2)
try:
result = action()
except RuntimeError:
result = None
finally:
os.close(w)
os.dup2(saved_stderr, 2)
with os.fdopen(r) as o:
output = o.read()
return (result, output)
## some tests
def return_value():
return 5
def return_value_with_stderr():
os.system("echo >&2 some output")
return 10
def runtime_error():
os.system("echo >&2 runtime error occurred")
raise RuntimeError()
def tests():
print(capture_runtime_stderr(return_value))
print(capture_runtime_stderr(return_value_with_stderr))
print(capture_runtime_stderr(runtime_error))
os.system("echo >&2 never fear, stderr is back to normal")
## possible code for your loop
def example_loop():
while True:
(wm, output) = capture_runtime_stderr(cmiid.Wiimote)
if wm == None:
if "Socket connect error" in output:
raise RuntimeError("library borked, time to reboot")
time.sleep(1)
continue
## do something with wm??