395
394
logger.error(bad_states[state] + ": %r", error)
397
396
elif state == avahi.SERVER_RUNNING:
400
except dbus.exceptions.DBusException as error:
401
if (error.get_dbus_name()
402
== "org.freedesktop.Avahi.CollisionError"):
403
logger.info("Local Zeroconf service name"
405
return self.rename(remove=False)
407
logger.critical("D-Bus Exception", exc_info=error)
411
399
if error is None:
412
400
logger.debug("Unknown state: %r", state)
435
423
.format(self.name)))
438
def call_pipe(connection, # : multiprocessing.Connection
439
func, *args, **kwargs):
440
"""This function is meant to be called by multiprocessing.Process
442
This function runs func(*args, **kwargs), and writes the resulting
443
return value on the provided multiprocessing.Connection.
445
connection.send(func(*args, **kwargs))
448
427
class Client(object):
449
428
"""A representation of a client host served by this server.
476
455
last_checker_status: integer between 0 and 255 reflecting exit
477
456
status of last checker. -1 reflects crashed
478
457
checker, -2 means no checker completed yet.
479
last_checker_signal: The signal which killed the last checker, if
480
last_checker_status is -1
481
458
last_enabled: datetime.datetime(); (UTC) or None
482
459
name: string; from the config file, used in log messages and
483
460
D-Bus identifiers
657
634
# Also start a new checker *right now*.
658
635
self.start_checker()
660
def checker_callback(self, source, condition, connection,
637
def checker_callback(self, pid, condition, command):
662
638
"""The checker has completed, so take appropriate actions."""
663
639
self.checker_callback_tag = None
664
640
self.checker = None
665
# Read return code from connection (see call_pipe)
666
returncode = connection.recv()
670
self.last_checker_status = returncode
671
self.last_checker_signal = None
641
if os.WIFEXITED(condition):
642
self.last_checker_status = os.WEXITSTATUS(condition)
672
643
if self.last_checker_status == 0:
673
644
logger.info("Checker for %(name)s succeeded",
677
648
logger.info("Checker for %(name)s failed", vars(self))
679
650
self.last_checker_status = -1
680
self.last_checker_signal = -returncode
681
651
logger.warning("Checker for %(name)s crashed?",
685
654
def checked_ok(self):
686
655
"""Assert that the client has been seen, alive and well."""
687
656
self.last_checked_ok = datetime.datetime.utcnow()
688
657
self.last_checker_status = 0
689
self.last_checker_signal = None
690
658
self.bump_timeout()
692
660
def bump_timeout(self, timeout=None):
718
686
# than 'timeout' for the client to be disabled, which is as it
721
if self.checker is not None and not self.checker.is_alive():
722
logger.warning("Checker was not alive; joining")
689
# If a checker exists, make sure it is not a zombie
691
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
692
except AttributeError:
694
except OSError as error:
695
if error.errno != errno.ECHILD:
699
logger.warning("Checker was a zombie")
700
gobject.source_remove(self.checker_callback_tag)
701
self.checker_callback(pid, status,
702
self.current_checker_command)
725
703
# Start a new checker if needed
726
704
if self.checker is None:
727
705
# Escape attributes for the shell
737
715
return True # Try again later
738
716
self.current_checker_command = command
739
logger.info("Starting checker %r for %s", command,
741
# We don't need to redirect stdout and stderr, since
742
# in normal mode, that is already done by daemon(),
743
# and in debug mode we don't want to. (Stdin is
744
# always replaced by /dev/null.)
745
# The exception is when not debugging but nevertheless
746
# running in the foreground; use the previously
748
popen_args = { "close_fds": True,
751
if (not self.server_settings["debug"]
752
and self.server_settings["foreground"]):
753
popen_args.update({"stdout": wnull,
755
pipe = multiprocessing.Pipe(duplex = False)
756
self.checker = multiprocessing.Process(
758
args = (pipe[1], subprocess.call, command),
761
self.checker_callback_tag = gobject.io_add_watch(
762
pipe[0].fileno(), gobject.IO_IN,
763
self.checker_callback, pipe[0], command)
718
logger.info("Starting checker %r for %s", command,
720
# We don't need to redirect stdout and stderr, since
721
# in normal mode, that is already done by daemon(),
722
# and in debug mode we don't want to. (Stdin is
723
# always replaced by /dev/null.)
724
# The exception is when not debugging but nevertheless
725
# running in the foreground; use the previously
728
if (not self.server_settings["debug"]
729
and self.server_settings["foreground"]):
730
popen_args.update({"stdout": wnull,
732
self.checker = subprocess.Popen(command,
737
except OSError as error:
738
logger.error("Failed to start subprocess",
741
self.checker_callback_tag = gobject.child_watch_add(
742
self.checker.pid, self.checker_callback, data=command)
743
# The checker may have completed before the gobject
744
# watch was added. Check for this.
746
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
747
except OSError as error:
748
if error.errno == errno.ECHILD:
749
# This should never happen
750
logger.error("Child process vanished",
755
gobject.source_remove(self.checker_callback_tag)
756
self.checker_callback(pid, status, command)
764
757
# Re-run this periodically if run by gobject.timeout_add
1110
1110
interface_names.add(alt_interface)
1111
1111
# Is this a D-Bus signal?
1112
1112
if getattr(attribute, "_dbus_is_signal", False):
1113
if sys.version_info.major == 2:
1114
# Extract the original non-method undecorated
1115
# function by black magic
1116
nonmethod_func = (dict(
1117
zip(attribute.func_code.co_freevars,
1118
attribute.__closure__))
1119
["func"].cell_contents)
1121
nonmethod_func = attribute
1113
# Extract the original non-method undecorated
1114
# function by black magic
1115
nonmethod_func = (dict(
1116
zip(attribute.func_code.co_freevars,
1117
attribute.__closure__))
1118
["func"].cell_contents)
1122
1119
# Create a new, but exactly alike, function
1123
1120
# object, and decorate it to be a new D-Bus signal
1124
1121
# with the alternate D-Bus interface name
1125
if sys.version_info.major == 2:
1126
new_function = types.FunctionType(
1127
nonmethod_func.func_code,
1128
nonmethod_func.func_globals,
1129
nonmethod_func.func_name,
1130
nonmethod_func.func_defaults,
1131
nonmethod_func.func_closure)
1133
new_function = types.FunctionType(
1134
nonmethod_func.__code__,
1135
nonmethod_func.__globals__,
1136
nonmethod_func.__name__,
1137
nonmethod_func.__defaults__,
1138
nonmethod_func.__closure__)
1139
1122
new_function = (dbus.service.signal(
1141
attribute._dbus_signature)(new_function))
1123
alt_interface, attribute._dbus_signature)
1124
(types.FunctionType(
1125
nonmethod_func.func_code,
1126
nonmethod_func.func_globals,
1127
nonmethod_func.func_name,
1128
nonmethod_func.func_defaults,
1129
nonmethod_func.func_closure)))
1142
1130
# Copy annotations, if any
1144
1132
new_function._dbus_annotations = dict(
1367
1355
DBusObjectWithProperties.__del__(self, *args, **kwargs)
1368
1356
Client.__del__(self, *args, **kwargs)
1370
def checker_callback(self, source, condition,
1371
connection, command, *args, **kwargs):
1372
ret = Client.checker_callback(self, source, condition,
1373
connection, command, *args,
1375
exitstatus = self.last_checker_status
1358
def checker_callback(self, pid, condition, command,
1360
self.checker_callback_tag = None
1362
if os.WIFEXITED(condition):
1363
exitstatus = os.WEXITSTATUS(condition)
1377
1364
# Emit D-Bus signal
1378
1365
self.CheckerCompleted(dbus.Int16(exitstatus),
1366
dbus.Int64(condition),
1380
1367
dbus.String(command))
1382
1369
# Emit D-Bus signal
1383
1370
self.CheckerCompleted(dbus.Int16(-1),
1385
self.last_checker_signal),
1371
dbus.Int64(condition),
1386
1372
dbus.String(command))
1374
return Client.checker_callback(self, pid, condition, command,
1389
1377
def start_checker(self, *args, **kwargs):
1390
1378
old_checker_pid = getattr(self.checker, "pid", None)
2194
2181
# avoid excessive use of external libraries.
2196
2183
# New type for defining tokens, syntax, and semantics all-in-one
2184
Token = collections.namedtuple("Token",
2185
("regexp", # To match token; if
2186
# "value" is not None,
2187
# must have a "group"
2189
"value", # datetime.timedelta or
2191
"followers")) # Tokens valid after
2197
2193
Token = collections.namedtuple("Token", (
2198
2194
"regexp", # To match token; if "value" is not None, must have
2199
2195
# a "group" containing digits