189
189
facility=logging.handlers.SysLogHandler.LOG_DAEMON,
190
190
address="/dev/log"))
191
191
syslogger.setFormatter(logging.Formatter
192
("Mandos [%(process)d]: %(levelname)s:"
194
log.addHandler(syslogger)
192
('Mandos [%(process)d]: %(levelname)s:'
194
logger.addHandler(syslogger)
197
197
console = logging.StreamHandler()
198
console.setFormatter(logging.Formatter("%(asctime)s %(name)s"
202
log.addHandler(console)
198
console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
202
logger.addHandler(console)
203
logger.setLevel(level)
206
206
class PGPError(Exception):
224
224
except OSError as e:
225
225
if e.errno != errno.ENOENT:
227
self.gnupgargs = ["--batch",
228
"--homedir", self.tempdir,
227
self.gnupgargs = ['--batch',
228
'--homedir', self.tempdir,
231
231
# Only GPG version 1 has the --no-use-agent option.
232
232
if self.gpg == b"gpg" or self.gpg.endswith(b"/gpg"):
233
233
self.gnupgargs.append("--no-use-agent")
351
351
interface: integer; avahi.IF_UNSPEC or an interface index.
352
352
Used to optionally bind to the specified interface.
353
name: string; Example: "Mandos"
354
type: string; Example: "_mandos._tcp".
353
name: string; Example: 'Mandos'
354
type: string; Example: '_mandos._tcp'.
355
355
See <https://www.iana.org/assignments/service-names-port-numbers>
356
356
port: integer; what port to announce
357
357
TXT: list of strings; TXT record for the service
394
394
def rename(self, remove=True):
395
395
"""Derived from the Avahi example code"""
396
396
if self.rename_count >= self.max_renames:
397
log.critical("No suitable Zeroconf service name found"
398
" after %i retries, exiting.",
397
logger.critical("No suitable Zeroconf service name found"
398
" after %i retries, exiting.",
400
400
raise AvahiServiceError("Too many renames")
402
402
self.server.GetAlternativeServiceName(self.name))
403
403
self.rename_count += 1
404
log.info("Changing Zeroconf service name to %r ...",
404
logger.info("Changing Zeroconf service name to %r ...",
410
410
except dbus.exceptions.DBusException as error:
411
411
if (error.get_dbus_name()
412
412
== "org.freedesktop.Avahi.CollisionError"):
413
log.info("Local Zeroconf service name collision.")
413
logger.info("Local Zeroconf service name collision.")
414
414
return self.rename(remove=False)
416
log.critical("D-Bus Exception", exc_info=error)
416
logger.critical("D-Bus Exception", exc_info=error)
435
435
avahi.DBUS_INTERFACE_ENTRY_GROUP)
436
436
self.entry_group_state_changed_match = (
437
437
self.group.connect_to_signal(
438
"StateChanged", self.entry_group_state_changed))
439
log.debug("Adding Zeroconf service '%s' of type '%s' ...",
440
self.name, self.type)
438
'StateChanged', self.entry_group_state_changed))
439
logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
440
self.name, self.type)
441
441
self.group.AddService(
451
451
def entry_group_state_changed(self, state, error):
452
452
"""Derived from the Avahi example code"""
453
log.debug("Avahi entry group state change: %i", state)
453
logger.debug("Avahi entry group state change: %i", state)
455
455
if state == avahi.ENTRY_GROUP_ESTABLISHED:
456
log.debug("Zeroconf service established.")
456
logger.debug("Zeroconf service established.")
457
457
elif state == avahi.ENTRY_GROUP_COLLISION:
458
log.info("Zeroconf service name collision.")
458
logger.info("Zeroconf service name collision.")
460
460
elif state == avahi.ENTRY_GROUP_FAILURE:
461
log.critical("Avahi: Error in group state changed %s",
461
logger.critical("Avahi: Error in group state changed %s",
463
463
raise AvahiGroupError("State changed: {!s}".format(error))
465
465
def cleanup(self):
476
476
def server_state_changed(self, state, error=None):
477
477
"""Derived from the Avahi example code"""
478
log.debug("Avahi server state change: %i", state)
478
logger.debug("Avahi server state change: %i", state)
480
480
avahi.SERVER_INVALID: "Zeroconf server invalid",
481
481
avahi.SERVER_REGISTERING: None,
495
495
except dbus.exceptions.DBusException as error:
496
496
if (error.get_dbus_name()
497
497
== "org.freedesktop.Avahi.CollisionError"):
498
log.info("Local Zeroconf service name collision.")
498
logger.info("Local Zeroconf service name"
499
500
return self.rename(remove=False)
501
log.critical("D-Bus Exception", exc_info=error)
502
logger.critical("D-Bus Exception", exc_info=error)
505
506
if error is None:
506
log.debug("Unknown state: %r", state)
507
logger.debug("Unknown state: %r", state)
508
log.debug("Unknown state: %r: %r", state, error)
509
logger.debug("Unknown state: %r: %r", state, error)
510
511
def activate(self):
511
512
"""Derived from the Avahi example code"""
685
686
def _retry_on_error(result, func, arguments,
686
687
_error_code=_error_code):
687
688
"""A function to retry on some errors, suitable
688
for the "errcheck" attribute on ctypes functions"""
689
for the 'errcheck' attribute on ctypes functions"""
689
690
while result < gnutls.E_SUCCESS:
690
691
if result not in (gnutls.E_INTERRUPTED, gnutls.E_AGAIN):
691
692
return _error_code(result)
875
876
"""A representation of a client host served by this server.
878
approved: bool(); None if not yet approved/disapproved
879
approved: bool(); 'None' if not yet approved/disapproved
879
880
approval_delay: datetime.timedelta(); Time to wait for approval
880
881
approval_duration: datetime.timedelta(); Duration of one approval
881
882
checker: multiprocessing.Process(); a running checker process used
882
to see if the client lives. None if no process is
883
to see if the client lives. 'None' if no process is
884
885
checker_callback_tag: a GLib event source tag, or None
885
886
checker_command: string; External command which is run to check
961
962
# key_id() and fingerprint() functions
962
963
client["key_id"] = (section.get("key_id", "").upper()
963
964
.replace(" ", ""))
964
client["fingerprint"] = (section.get("fingerprint",
965
client["fingerprint"] = (section["fingerprint"].upper()
966
966
.replace(" ", ""))
967
if not (client["key_id"] or client["fingerprint"]):
968
log.error("Skipping client %s without key_id or"
969
" fingerprint", client_name)
970
del settings[client_name]
972
967
if "secret" in section:
973
968
client["secret"] = codecs.decode(section["secret"]
974
969
.encode("utf-8"),
1015
1010
self.last_enabled = None
1016
1011
self.expires = None
1018
log.debug("Creating client %r", self.name)
1019
log.debug(" Key ID: %s", self.key_id)
1020
log.debug(" Fingerprint: %s", self.fingerprint)
1013
logger.debug("Creating client %r", self.name)
1014
logger.debug(" Key ID: %s", self.key_id)
1015
logger.debug(" Fingerprint: %s", self.fingerprint)
1021
1016
self.created = settings.get("created",
1022
1017
datetime.datetime.utcnow())
1061
1057
if not getattr(self, "enabled", False):
1064
log.info("Disabling client %s", self.name)
1060
logger.info("Disabling client %s", self.name)
1065
1061
if getattr(self, "disable_initiator_tag", None) is not None:
1066
1062
GLib.source_remove(self.disable_initiator_tag)
1067
1063
self.disable_initiator_tag = None
1079
1075
def __del__(self):
1082
def init_checker(self, randomize_start=False):
1083
# Schedule a new checker to be started a randomly selected
1084
# time (a fraction of 'interval') from now. This spreads out
1085
# the startup of checkers over time when the server is
1078
def init_checker(self):
1079
# Schedule a new checker to be started an 'interval' from now,
1080
# and every interval from then on.
1087
1081
if self.checker_initiator_tag is not None:
1088
1082
GLib.source_remove(self.checker_initiator_tag)
1089
interval_milliseconds = int(self.interval.total_seconds()
1092
delay_milliseconds = random.randrange(
1093
interval_milliseconds + 1)
1095
delay_milliseconds = interval_milliseconds
1096
1083
self.checker_initiator_tag = GLib.timeout_add(
1097
delay_milliseconds, self.start_checker, randomize_start)
1098
delay = datetime.timedelta(0, 0, 0, delay_milliseconds)
1099
# A checker might take up to an 'interval' of time, so we can
1100
# expire at the soonest one interval after a checker was
1101
# started. Since the initial checker is delayed, the expire
1102
# time might have to be extended.
1103
now = datetime.datetime.utcnow()
1104
self.expires = now + delay + self.interval
1105
# Schedule a disable() at expire time
1084
random.randrange(int(self.interval.total_seconds() * 1000
1087
# Schedule a disable() when 'timeout' has passed
1106
1088
if self.disable_initiator_tag is not None:
1107
1089
GLib.source_remove(self.disable_initiator_tag)
1108
1090
self.disable_initiator_tag = GLib.timeout_add(
1109
int((self.expires - now).total_seconds() * 1000),
1091
int(self.timeout.total_seconds() * 1000), self.disable)
1092
# Also start a new checker *right now*.
1093
self.start_checker()
1112
1095
def checker_callback(self, source, condition, connection,
1124
1107
self.last_checker_status = returncode
1125
1108
self.last_checker_signal = None
1126
1109
if self.last_checker_status == 0:
1127
log.info("Checker for %(name)s succeeded", vars(self))
1110
logger.info("Checker for %(name)s succeeded",
1128
1112
self.checked_ok()
1130
log.info("Checker for %(name)s failed", vars(self))
1114
logger.info("Checker for %(name)s failed", vars(self))
1132
1116
self.last_checker_status = -1
1133
1117
self.last_checker_signal = -returncode
1134
log.warning("Checker for %(name)s crashed?", vars(self))
1118
logger.warning("Checker for %(name)s crashed?",
1137
1122
def checked_ok(self):
1184
1169
command = self.checker_command % escaped_attrs
1185
1170
except TypeError as error:
1186
log.error('Could not format string "%s"',
1187
self.checker_command, exc_info=error)
1171
logger.error('Could not format string "%s"',
1172
self.checker_command,
1188
1174
return True # Try again later
1189
1175
self.current_checker_command = command
1190
log.info("Starting checker %r for %s", command, self.name)
1176
logger.info("Starting checker %r for %s", command,
1191
1178
# We don't need to redirect stdout and stderr, since
1192
1179
# in normal mode, that is already done by daemon(),
1193
1180
# and in debug mode we don't want to. (Stdin is
1212
1199
GLib.IOChannel.unix_new(pipe[0].fileno()),
1213
1200
GLib.PRIORITY_DEFAULT, GLib.IO_IN,
1214
1201
self.checker_callback, pipe[0], command)
1215
if start_was_randomized:
1216
# We were started after a random delay; Schedule a new
1217
# checker to be started an 'interval' from now, and every
1218
# interval from then on.
1219
now = datetime.datetime.utcnow()
1220
self.checker_initiator_tag = GLib.timeout_add(
1221
int(self.interval.total_seconds() * 1000),
1223
self.expires = max(self.expires, now + self.interval)
1224
# Don't start a new checker again after same random delay
1226
1202
# Re-run this periodically if run by GLib.timeout_add
1362
1338
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1363
1339
out_signature="s",
1364
path_keyword="object_path",
1365
connection_keyword="connection")
1340
path_keyword='object_path',
1341
connection_keyword='connection')
1366
1342
def Introspect(self, object_path, connection):
1367
1343
"""Overloading of standard D-Bus method.
1522
1498
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1523
1499
out_signature="s",
1524
path_keyword="object_path",
1525
connection_keyword="connection")
1500
path_keyword='object_path',
1501
connection_keyword='connection')
1526
1502
def Introspect(self, object_path, connection):
1527
1503
"""Overloading of standard D-Bus method.
1624
1600
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1625
1601
out_signature="s",
1626
path_keyword="object_path",
1627
connection_keyword="connection")
1602
path_keyword='object_path',
1603
connection_keyword='connection')
1628
1604
def Introspect(self, object_path, connection):
1629
1605
"""Overloading of standard D-Bus method.
2296
2272
class ProxyClient:
2297
2273
def __init__(self, child_pipe, key_id, fpr, address):
2298
2274
self._pipe = child_pipe
2299
self._pipe.send(("init", key_id, fpr, address))
2275
self._pipe.send(('init', key_id, fpr, address))
2300
2276
if not self._pipe.recv():
2301
2277
raise KeyError(key_id or fpr)
2303
2279
def __getattribute__(self, name):
2305
2281
return super(ProxyClient, self).__getattribute__(name)
2306
self._pipe.send(("getattr", name))
2282
self._pipe.send(('getattr', name))
2307
2283
data = self._pipe.recv()
2308
if data[0] == "data":
2284
if data[0] == 'data':
2310
if data[0] == "function":
2286
if data[0] == 'function':
2312
2288
def func(*args, **kwargs):
2313
self._pipe.send(("funcall", name, args, kwargs))
2289
self._pipe.send(('funcall', name, args, kwargs))
2314
2290
return self._pipe.recv()[1]
2318
2294
def __setattr__(self, name, value):
2320
2296
return super(ProxyClient, self).__setattr__(name, value)
2321
self._pipe.send(("setattr", name, value))
2297
self._pipe.send(('setattr', name, value))
2324
2300
class ClientHandler(socketserver.BaseRequestHandler, object):
2330
2306
def handle(self):
2331
2307
with contextlib.closing(self.server.child_pipe) as child_pipe:
2332
log.info("TCP connection from: %s",
2333
str(self.client_address))
2334
log.debug("Pipe FD: %d", self.server.child_pipe.fileno())
2308
logger.info("TCP connection from: %s",
2309
str(self.client_address))
2310
logger.debug("Pipe FD: %d",
2311
self.server.child_pipe.fileno())
2336
2313
session = gnutls.ClientSession(self.request)
2338
# priority = ":".join(("NONE", "+VERS-TLS1.1",
2315
# priority = ':'.join(("NONE", "+VERS-TLS1.1",
2339
2316
# "+AES-256-CBC", "+SHA1",
2340
2317
# "+COMP-NULL", "+CTYPE-OPENPGP",
2349
2326
# Start communication using the Mandos protocol
2350
2327
# Get protocol number
2351
2328
line = self.request.makefile().readline()
2352
log.debug("Protocol version: %r", line)
2329
logger.debug("Protocol version: %r", line)
2354
2331
if int(line.strip().split()[0]) > 1:
2355
2332
raise RuntimeError(line)
2356
2333
except (ValueError, IndexError, RuntimeError) as error:
2357
log.error("Unknown protocol version: %s", error)
2334
logger.error("Unknown protocol version: %s", error)
2360
2337
# Start GnuTLS connection
2362
2339
session.handshake()
2363
2340
except gnutls.Error as error:
2364
log.warning("Handshake failed: %s", error)
2341
logger.warning("Handshake failed: %s", error)
2365
2342
# Do not run session.bye() here: the session is not
2366
2343
# established. Just abandon the request.
2368
log.debug("Handshake succeeded")
2345
logger.debug("Handshake succeeded")
2370
2347
approval_required = False
2387
2364
fpr = self.fingerprint(
2388
2365
self.peer_certificate(session))
2389
2366
except (TypeError, gnutls.Error) as error:
2390
log.warning("Bad certificate: %s", error)
2367
logger.warning("Bad certificate: %s", error)
2392
log.debug("Fingerprint: %s", fpr)
2369
logger.debug("Fingerprint: %s", fpr)
2395
2372
client = ProxyClient(child_pipe, key_id, fpr,
2414
2392
# We are approved or approval is disabled
2416
2394
elif client.approved is None:
2417
log.info("Client %s needs approval",
2395
logger.info("Client %s needs approval",
2419
2397
if self.server.use_dbus:
2420
2398
# Emit D-Bus signal
2421
2399
client.NeedApproval(
2422
2400
client.approval_delay.total_seconds()
2423
2401
* 1000, client.approved_by_default)
2425
log.warning("Client %s was not approved",
2403
logger.warning("Client %s was not approved",
2427
2405
if self.server.use_dbus:
2428
2406
# Emit D-Bus signal
2429
2407
client.Rejected("Denied")
2453
2431
session.send(client.secret)
2454
2432
except gnutls.Error as error:
2455
log.warning("gnutls send failed", exc_info=error)
2433
logger.warning("gnutls send failed",
2458
log.info("Sending secret to %s", client.name)
2437
logger.info("Sending secret to %s", client.name)
2459
2438
# bump the timeout using extended_timeout
2460
2439
client.bump_timeout(client.extended_timeout)
2461
2440
if self.server.use_dbus:
2484
2464
valid_cert_types = frozenset((gnutls.CRT_OPENPGP,))
2485
2465
# If not a valid certificate type...
2486
2466
if cert_type not in valid_cert_types:
2487
log.info("Cert type %r not in %r", cert_type,
2467
logger.info("Cert type %r not in %r", cert_type,
2489
2469
# ...return invalid data
2491
2471
list_size = ctypes.c_uint(1)
2675
2655
(self.interface + "\0").encode("utf-8"))
2676
2656
except socket.error as error:
2677
2657
if error.errno == errno.EPERM:
2678
log.error("No permission to bind to interface %s",
2658
logger.error("No permission to bind to"
2659
" interface %s", self.interface)
2680
2660
elif error.errno == errno.ENOPROTOOPT:
2681
log.error("SO_BINDTODEVICE not available; cannot"
2682
" bind to interface %s", self.interface)
2661
logger.error("SO_BINDTODEVICE not available;"
2662
" cannot bind to interface %s",
2683
2664
elif error.errno == errno.ENODEV:
2684
log.error("Interface %s does not exist, cannot"
2685
" bind", self.interface)
2665
logger.error("Interface %s does not exist,"
2666
" cannot bind", self.interface)
2688
2669
# Only bind(2) the socket if we really need to.
2786
log.info("Client not found for key ID: %s, address:"
2787
" %s", key_id or fpr, address)
2767
logger.info("Client not found for key ID: %s, address"
2768
": %s", key_id or fpr, address)
2788
2769
if self.use_dbus:
2789
2770
# Emit D-Bus signal
2790
2771
mandos_dbus_service.ClientNotFound(key_id or fpr,
2803
2784
# remove the old hook in favor of the new above hook on
2806
if command == "funcall":
2787
if command == 'funcall':
2807
2788
funcname = request[1]
2808
2789
args = request[2]
2809
2790
kwargs = request[3]
2811
parent_pipe.send(("data", getattr(client_object,
2792
parent_pipe.send(('data', getattr(client_object,
2812
2793
funcname)(*args,
2815
if command == "getattr":
2796
if command == 'getattr':
2816
2797
attrname = request[1]
2817
2798
if isinstance(client_object.__getattribute__(attrname),
2818
2799
collections.abc.Callable):
2819
parent_pipe.send(("function", ))
2800
parent_pipe.send(('function', ))
2821
2802
parent_pipe.send((
2822
"data", client_object.__getattribute__(attrname)))
2803
'data', client_object.__getattribute__(attrname)))
2824
if command == "setattr":
2805
if command == 'setattr':
2825
2806
attrname = request[1]
2826
2807
value = request[2]
2827
2808
setattr(client_object, attrname, value)
2933
2914
def string_to_delta(interval):
2934
2915
"""Parse a string and return a datetime.timedelta
2936
>>> string_to_delta("7d") == datetime.timedelta(7)
2938
>>> string_to_delta("60s") == datetime.timedelta(0, 60)
2940
>>> string_to_delta("60m") == datetime.timedelta(0, 3600)
2942
>>> string_to_delta("24h") == datetime.timedelta(1)
2944
>>> string_to_delta("1w") == datetime.timedelta(7)
2946
>>> string_to_delta("5m 30s") == datetime.timedelta(0, 330)
2917
>>> string_to_delta('7d') == datetime.timedelta(7)
2919
>>> string_to_delta('60s') == datetime.timedelta(0, 60)
2921
>>> string_to_delta('60m') == datetime.timedelta(0, 3600)
2923
>>> string_to_delta('24h') == datetime.timedelta(1)
2925
>>> string_to_delta('1w') == datetime.timedelta(7)
2927
>>> string_to_delta('5m 30s') == datetime.timedelta(0, 330)
3185
3166
pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
3186
3167
except IOError as e:
3187
log.error("Could not open file %r", pidfilename,
3168
logger.error("Could not open file %r", pidfilename,
3190
3171
for name, group in (("_mandos", "_mandos"),
3191
3172
("mandos", "mandos"),
3205
log.debug("Did setuid/setgid to %s:%s", uid, gid)
3187
logger.debug("Did setuid/setgid to {}:{}".format(uid,
3206
3189
except OSError as error:
3207
log.warning("Failed to setuid/setgid to %s:%s: %s", uid, gid,
3208
os.strerror(error.errno))
3190
logger.warning("Failed to setuid/setgid to {}:{}: {}"
3191
.format(uid, gid, os.strerror(error.errno)))
3209
3192
if error.errno != errno.EPERM:
3345
3329
os.remove(stored_state_path)
3346
3330
except IOError as e:
3347
3331
if e.errno == errno.ENOENT:
3348
log.warning("Could not load persistent state:"
3349
" %s", os.strerror(e.errno))
3332
logger.warning("Could not load persistent state:"
3333
" {}".format(os.strerror(e.errno)))
3351
log.critical("Could not load persistent state:",
3335
logger.critical("Could not load persistent state:",
3354
3338
except EOFError as e:
3355
log.warning("Could not load persistent state: EOFError:",
3339
logger.warning("Could not load persistent state: "
3358
3343
with PGPEngine() as pgp:
3359
3344
for client_name, client in clients_data.items():
3386
3371
if client["enabled"]:
3387
3372
if datetime.datetime.utcnow() >= client["expires"]:
3388
3373
if not client["last_checked_ok"]:
3389
log.warning("disabling client %s - Client"
3390
" never performed a successful"
3391
" checker", client_name)
3375
"disabling client {} - Client never "
3376
"performed a successful checker".format(
3392
3378
client["enabled"] = False
3393
3379
elif client["last_checker_status"] != 0:
3394
log.warning("disabling client %s - Client"
3395
" last checker failed with error"
3396
" code %s", client_name,
3397
client["last_checker_status"])
3381
"disabling client {} - Client last"
3382
" checker failed with error code"
3385
client["last_checker_status"]))
3398
3386
client["enabled"] = False
3400
3388
client["expires"] = (
3401
3389
datetime.datetime.utcnow()
3402
3390
+ client["timeout"])
3403
log.debug("Last checker succeeded, keeping %s"
3404
" enabled", client_name)
3391
logger.debug("Last checker succeeded,"
3392
" keeping {} enabled".format(
3406
3395
client["secret"] = pgp.decrypt(
3407
3396
client["encrypted_secret"],
3408
3397
client_settings[client_name]["secret"])
3409
3398
except PGPError:
3410
3399
# If decryption fails, we use secret from new settings
3411
log.debug("Failed to decrypt %s old secret",
3400
logger.debug("Failed to decrypt {} old secret".format(
3413
3402
client["secret"] = (client_settings[client_name]
3611
3600
except NameError:
3613
3602
if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
3614
log.warning("Could not save persistent state: %s",
3615
os.strerror(e.errno))
3603
logger.warning("Could not save persistent state: {}"
3604
.format(os.strerror(e.errno)))
3617
log.warning("Could not save persistent state:",
3606
logger.warning("Could not save persistent state:",
3621
3610
# Delete all clients, and settings from config
3648
3637
service.port = tcp_server.socket.getsockname()[1]
3650
log.info("Now listening on address %r, port %d, flowinfo %d,"
3651
" scope_id %d", *tcp_server.socket.getsockname())
3639
logger.info("Now listening on address %r, port %d,"
3640
" flowinfo %d, scope_id %d",
3641
*tcp_server.socket.getsockname())
3653
log.info("Now listening on address %r, port %d",
3654
*tcp_server.socket.getsockname())
3643
logger.info("Now listening on address %r, port %d",
3644
*tcp_server.socket.getsockname())
3656
3646
# service.interface = tcp_server.socket.getsockname()[3]
3672
3662
lambda *args, **kwargs: (tcp_server.handle_request
3673
3663
(*args[2:], **kwargs) or True))
3675
log.debug("Starting main loop")
3665
logger.debug("Starting main loop")
3676
3666
main_loop.run()
3677
3667
except AvahiError as error:
3678
log.critical("Avahi Error", exc_info=error)
3668
logger.critical("Avahi Error", exc_info=error)
3681
3671
except KeyboardInterrupt:
3683
3673
print("", file=sys.stderr)
3684
log.debug("Server received KeyboardInterrupt")
3685
log.debug("Server exiting")
3674
logger.debug("Server received KeyboardInterrupt")
3675
logger.debug("Server exiting")
3686
3676
# Must run before the D-Bus bus name gets deregistered
3690
def parse_test_args():
3691
# type: () -> argparse.Namespace
3680
def should_only_run_tests():
3692
3681
parser = argparse.ArgumentParser(add_help=False)
3693
parser.add_argument("--check", action="store_true")
3694
parser.add_argument("--prefix", )
3682
parser.add_argument("--check", action='store_true')
3695
3683
args, unknown_args = parser.parse_known_args()
3697
# Remove test options from sys.argv
3684
run_tests = args.check
3686
# Remove --check argument from sys.argv
3698
3687
sys.argv[1:] = unknown_args
3701
3690
# Add all tests from doctest strings
3702
3691
def load_tests(loader, tests, none):
3704
3693
tests.addTests(doctest.DocTestSuite())
3707
if __name__ == "__main__":
3708
options = parse_test_args()
3696
if __name__ == '__main__':
3711
extra_test_prefix = options.prefix
3712
if extra_test_prefix is not None:
3713
if not (unittest.main(argv=[""], exit=False)
3714
.result.wasSuccessful()):
3716
class ExtraTestLoader(unittest.TestLoader):
3717
testMethodPrefix = extra_test_prefix
3718
# Call using ./scriptname --test [--verbose]
3719
unittest.main(argv=[""], testLoader=ExtraTestLoader())
3721
unittest.main(argv=[""])
3698
if should_only_run_tests():
3699
# Call using ./mandos --check [--verbose]
3725
3704
logging.shutdown()
3729
# (lambda (&optional extra)
3730
# (if (not (funcall run-tests-in-test-buffer default-directory
3732
# (funcall show-test-buffer-in-test-window)
3733
# (funcall remove-test-window)
3734
# (if extra (message "Extra tests run successfully!"))))
3735
# run-tests-in-test-buffer:
3736
# (lambda (dir &optional extra)
3737
# (with-current-buffer (get-buffer-create "*Test*")
3738
# (setq buffer-read-only nil
3739
# default-directory dir)
3741
# (compilation-mode))
3742
# (let ((process-result
3743
# (let ((inhibit-read-only t))
3744
# (process-file-shell-command
3745
# (funcall get-command-line extra) nil "*Test*"))))
3746
# (and (numberp process-result)
3747
# (= process-result 0))))
3749
# (lambda (&optional extra)
3750
# (let ((quoted-script
3751
# (shell-quote-argument (funcall get-script-name))))
3753
# (concat "%s --check" (if extra " --prefix=atest" ""))
3757
# (if (fboundp 'file-local-name)
3758
# (file-local-name (buffer-file-name))
3759
# (or (file-remote-p (buffer-file-name) 'localname)
3760
# (buffer-file-name))))
3761
# remove-test-window:
3763
# (let ((test-window (get-buffer-window "*Test*")))
3764
# (if test-window (delete-window test-window))))
3765
# show-test-buffer-in-test-window:
3767
# (when (not (get-buffer-window-list "*Test*"))
3768
# (setq next-error-last-buffer (get-buffer "*Test*"))
3769
# (let* ((side (if (>= (window-width) 146) 'right 'bottom))
3770
# (display-buffer-overriding-action
3771
# `((display-buffer-in-side-window) (side . ,side)
3772
# (window-height . fit-window-to-buffer)
3773
# (window-width . fit-window-to-buffer))))
3774
# (display-buffer "*Test*"))))
3777
# (let* ((run-extra-tests (lambda () (interactive)
3778
# (funcall run-tests t)))
3779
# (inner-keymap `(keymap (116 . ,run-extra-tests))) ; t
3780
# (outer-keymap `(keymap (3 . ,inner-keymap)))) ; C-c
3781
# (setq minor-mode-overriding-map-alist
3782
# (cons `(run-tests . ,outer-keymap)
3783
# minor-mode-overriding-map-alist)))
3784
# (add-hook 'after-save-hook run-tests 90 t))