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
1009
1010
self.last_enabled = None
1010
1011
self.expires = None
1012
log.debug("Creating client %r", self.name)
1013
log.debug(" Key ID: %s", self.key_id)
1014
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)
1015
1016
self.created = settings.get("created",
1016
1017
datetime.datetime.utcnow())
1055
1057
if not getattr(self, "enabled", False):
1058
log.info("Disabling client %s", self.name)
1060
logger.info("Disabling client %s", self.name)
1059
1061
if getattr(self, "disable_initiator_tag", None) is not None:
1060
1062
GLib.source_remove(self.disable_initiator_tag)
1061
1063
self.disable_initiator_tag = None
1073
1075
def __del__(self):
1076
def init_checker(self, randomize_start=False):
1077
# Schedule a new checker to be started a randomly selected
1078
# time (a fraction of 'interval') from now. This spreads out
1079
# 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.
1081
1081
if self.checker_initiator_tag is not None:
1082
1082
GLib.source_remove(self.checker_initiator_tag)
1083
interval_milliseconds = int(self.interval.total_seconds()
1086
delay_milliseconds = random.randrange(
1087
interval_milliseconds + 1)
1089
delay_milliseconds = interval_milliseconds
1090
1083
self.checker_initiator_tag = GLib.timeout_add(
1091
delay_milliseconds, self.start_checker, randomize_start)
1092
delay = datetime.timedelta(0, 0, 0, delay_milliseconds)
1093
# A checker might take up to an 'interval' of time, so we can
1094
# expire at the soonest one interval after a checker was
1095
# started. Since the initial checker is delayed, the expire
1096
# time might have to be extended.
1097
now = datetime.datetime.utcnow()
1098
self.expires = now + delay + self.interval
1099
# Schedule a disable() at expire time
1084
random.randrange(int(self.interval.total_seconds() * 1000
1087
# Schedule a disable() when 'timeout' has passed
1100
1088
if self.disable_initiator_tag is not None:
1101
1089
GLib.source_remove(self.disable_initiator_tag)
1102
1090
self.disable_initiator_tag = GLib.timeout_add(
1103
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()
1106
1095
def checker_callback(self, source, condition, connection,
1118
1107
self.last_checker_status = returncode
1119
1108
self.last_checker_signal = None
1120
1109
if self.last_checker_status == 0:
1121
log.info("Checker for %(name)s succeeded", vars(self))
1110
logger.info("Checker for %(name)s succeeded",
1122
1112
self.checked_ok()
1124
log.info("Checker for %(name)s failed", vars(self))
1114
logger.info("Checker for %(name)s failed", vars(self))
1126
1116
self.last_checker_status = -1
1127
1117
self.last_checker_signal = -returncode
1128
log.warning("Checker for %(name)s crashed?", vars(self))
1118
logger.warning("Checker for %(name)s crashed?",
1131
1122
def checked_ok(self):
1178
1169
command = self.checker_command % escaped_attrs
1179
1170
except TypeError as error:
1180
log.error('Could not format string "%s"',
1181
self.checker_command, exc_info=error)
1171
logger.error('Could not format string "%s"',
1172
self.checker_command,
1182
1174
return True # Try again later
1183
1175
self.current_checker_command = command
1184
log.info("Starting checker %r for %s", command, self.name)
1176
logger.info("Starting checker %r for %s", command,
1185
1178
# We don't need to redirect stdout and stderr, since
1186
1179
# in normal mode, that is already done by daemon(),
1187
1180
# and in debug mode we don't want to. (Stdin is
1206
1199
GLib.IOChannel.unix_new(pipe[0].fileno()),
1207
1200
GLib.PRIORITY_DEFAULT, GLib.IO_IN,
1208
1201
self.checker_callback, pipe[0], command)
1209
if start_was_randomized:
1210
# We were started after a random delay; Schedule a new
1211
# checker to be started an 'interval' from now, and every
1212
# interval from then on.
1213
now = datetime.datetime.utcnow()
1214
self.checker_initiator_tag = GLib.timeout_add(
1215
int(self.interval.total_seconds() * 1000),
1217
self.expires = max(self.expires, now + self.interval)
1218
# Don't start a new checker again after same random delay
1220
1202
# Re-run this periodically if run by GLib.timeout_add
1356
1338
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1357
1339
out_signature="s",
1358
path_keyword="object_path",
1359
connection_keyword="connection")
1340
path_keyword='object_path',
1341
connection_keyword='connection')
1360
1342
def Introspect(self, object_path, connection):
1361
1343
"""Overloading of standard D-Bus method.
1516
1498
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1517
1499
out_signature="s",
1518
path_keyword="object_path",
1519
connection_keyword="connection")
1500
path_keyword='object_path',
1501
connection_keyword='connection')
1520
1502
def Introspect(self, object_path, connection):
1521
1503
"""Overloading of standard D-Bus method.
1618
1600
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1619
1601
out_signature="s",
1620
path_keyword="object_path",
1621
connection_keyword="connection")
1602
path_keyword='object_path',
1603
connection_keyword='connection')
1622
1604
def Introspect(self, object_path, connection):
1623
1605
"""Overloading of standard D-Bus method.
2290
2272
class ProxyClient:
2291
2273
def __init__(self, child_pipe, key_id, fpr, address):
2292
2274
self._pipe = child_pipe
2293
self._pipe.send(("init", key_id, fpr, address))
2275
self._pipe.send(('init', key_id, fpr, address))
2294
2276
if not self._pipe.recv():
2295
2277
raise KeyError(key_id or fpr)
2297
2279
def __getattribute__(self, name):
2299
2281
return super(ProxyClient, self).__getattribute__(name)
2300
self._pipe.send(("getattr", name))
2282
self._pipe.send(('getattr', name))
2301
2283
data = self._pipe.recv()
2302
if data[0] == "data":
2284
if data[0] == 'data':
2304
if data[0] == "function":
2286
if data[0] == 'function':
2306
2288
def func(*args, **kwargs):
2307
self._pipe.send(("funcall", name, args, kwargs))
2289
self._pipe.send(('funcall', name, args, kwargs))
2308
2290
return self._pipe.recv()[1]
2312
2294
def __setattr__(self, name, value):
2314
2296
return super(ProxyClient, self).__setattr__(name, value)
2315
self._pipe.send(("setattr", name, value))
2297
self._pipe.send(('setattr', name, value))
2318
2300
class ClientHandler(socketserver.BaseRequestHandler, object):
2324
2306
def handle(self):
2325
2307
with contextlib.closing(self.server.child_pipe) as child_pipe:
2326
log.info("TCP connection from: %s",
2327
str(self.client_address))
2328
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())
2330
2313
session = gnutls.ClientSession(self.request)
2332
# priority = ":".join(("NONE", "+VERS-TLS1.1",
2315
# priority = ':'.join(("NONE", "+VERS-TLS1.1",
2333
2316
# "+AES-256-CBC", "+SHA1",
2334
2317
# "+COMP-NULL", "+CTYPE-OPENPGP",
2343
2326
# Start communication using the Mandos protocol
2344
2327
# Get protocol number
2345
2328
line = self.request.makefile().readline()
2346
log.debug("Protocol version: %r", line)
2329
logger.debug("Protocol version: %r", line)
2348
2331
if int(line.strip().split()[0]) > 1:
2349
2332
raise RuntimeError(line)
2350
2333
except (ValueError, IndexError, RuntimeError) as error:
2351
log.error("Unknown protocol version: %s", error)
2334
logger.error("Unknown protocol version: %s", error)
2354
2337
# Start GnuTLS connection
2356
2339
session.handshake()
2357
2340
except gnutls.Error as error:
2358
log.warning("Handshake failed: %s", error)
2341
logger.warning("Handshake failed: %s", error)
2359
2342
# Do not run session.bye() here: the session is not
2360
2343
# established. Just abandon the request.
2362
log.debug("Handshake succeeded")
2345
logger.debug("Handshake succeeded")
2364
2347
approval_required = False
2381
2364
fpr = self.fingerprint(
2382
2365
self.peer_certificate(session))
2383
2366
except (TypeError, gnutls.Error) as error:
2384
log.warning("Bad certificate: %s", error)
2367
logger.warning("Bad certificate: %s", error)
2386
log.debug("Fingerprint: %s", fpr)
2369
logger.debug("Fingerprint: %s", fpr)
2389
2372
client = ProxyClient(child_pipe, key_id, fpr,
2408
2392
# We are approved or approval is disabled
2410
2394
elif client.approved is None:
2411
log.info("Client %s needs approval",
2395
logger.info("Client %s needs approval",
2413
2397
if self.server.use_dbus:
2414
2398
# Emit D-Bus signal
2415
2399
client.NeedApproval(
2416
2400
client.approval_delay.total_seconds()
2417
2401
* 1000, client.approved_by_default)
2419
log.warning("Client %s was not approved",
2403
logger.warning("Client %s was not approved",
2421
2405
if self.server.use_dbus:
2422
2406
# Emit D-Bus signal
2423
2407
client.Rejected("Denied")
2447
2431
session.send(client.secret)
2448
2432
except gnutls.Error as error:
2449
log.warning("gnutls send failed", exc_info=error)
2433
logger.warning("gnutls send failed",
2452
log.info("Sending secret to %s", client.name)
2437
logger.info("Sending secret to %s", client.name)
2453
2438
# bump the timeout using extended_timeout
2454
2439
client.bump_timeout(client.extended_timeout)
2455
2440
if self.server.use_dbus:
2478
2464
valid_cert_types = frozenset((gnutls.CRT_OPENPGP,))
2479
2465
# If not a valid certificate type...
2480
2466
if cert_type not in valid_cert_types:
2481
log.info("Cert type %r not in %r", cert_type,
2467
logger.info("Cert type %r not in %r", cert_type,
2483
2469
# ...return invalid data
2485
2471
list_size = ctypes.c_uint(1)
2669
2655
(self.interface + "\0").encode("utf-8"))
2670
2656
except socket.error as error:
2671
2657
if error.errno == errno.EPERM:
2672
log.error("No permission to bind to interface %s",
2658
logger.error("No permission to bind to"
2659
" interface %s", self.interface)
2674
2660
elif error.errno == errno.ENOPROTOOPT:
2675
log.error("SO_BINDTODEVICE not available; cannot"
2676
" bind to interface %s", self.interface)
2661
logger.error("SO_BINDTODEVICE not available;"
2662
" cannot bind to interface %s",
2677
2664
elif error.errno == errno.ENODEV:
2678
log.error("Interface %s does not exist, cannot"
2679
" bind", self.interface)
2665
logger.error("Interface %s does not exist,"
2666
" cannot bind", self.interface)
2682
2669
# Only bind(2) the socket if we really need to.
2780
log.info("Client not found for key ID: %s, address:"
2781
" %s", key_id or fpr, address)
2767
logger.info("Client not found for key ID: %s, address"
2768
": %s", key_id or fpr, address)
2782
2769
if self.use_dbus:
2783
2770
# Emit D-Bus signal
2784
2771
mandos_dbus_service.ClientNotFound(key_id or fpr,
2797
2784
# remove the old hook in favor of the new above hook on
2800
if command == "funcall":
2787
if command == 'funcall':
2801
2788
funcname = request[1]
2802
2789
args = request[2]
2803
2790
kwargs = request[3]
2805
parent_pipe.send(("data", getattr(client_object,
2792
parent_pipe.send(('data', getattr(client_object,
2806
2793
funcname)(*args,
2809
if command == "getattr":
2796
if command == 'getattr':
2810
2797
attrname = request[1]
2811
2798
if isinstance(client_object.__getattribute__(attrname),
2812
2799
collections.abc.Callable):
2813
parent_pipe.send(("function", ))
2800
parent_pipe.send(('function', ))
2815
2802
parent_pipe.send((
2816
"data", client_object.__getattribute__(attrname)))
2803
'data', client_object.__getattribute__(attrname)))
2818
if command == "setattr":
2805
if command == 'setattr':
2819
2806
attrname = request[1]
2820
2807
value = request[2]
2821
2808
setattr(client_object, attrname, value)
2927
2914
def string_to_delta(interval):
2928
2915
"""Parse a string and return a datetime.timedelta
2930
>>> string_to_delta("7d") == datetime.timedelta(7)
2932
>>> string_to_delta("60s") == datetime.timedelta(0, 60)
2934
>>> string_to_delta("60m") == datetime.timedelta(0, 3600)
2936
>>> string_to_delta("24h") == datetime.timedelta(1)
2938
>>> string_to_delta("1w") == datetime.timedelta(7)
2940
>>> 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)
3179
3166
pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
3180
3167
except IOError as e:
3181
log.error("Could not open file %r", pidfilename,
3168
logger.error("Could not open file %r", pidfilename,
3184
3171
for name, group in (("_mandos", "_mandos"),
3185
3172
("mandos", "mandos"),
3199
log.debug("Did setuid/setgid to %s:%s", uid, gid)
3187
logger.debug("Did setuid/setgid to {}:{}".format(uid,
3200
3189
except OSError as error:
3201
log.warning("Failed to setuid/setgid to %s:%s: %s", uid, gid,
3202
os.strerror(error.errno))
3190
logger.warning("Failed to setuid/setgid to {}:{}: {}"
3191
.format(uid, gid, os.strerror(error.errno)))
3203
3192
if error.errno != errno.EPERM:
3339
3329
os.remove(stored_state_path)
3340
3330
except IOError as e:
3341
3331
if e.errno == errno.ENOENT:
3342
log.warning("Could not load persistent state:"
3343
" %s", os.strerror(e.errno))
3332
logger.warning("Could not load persistent state:"
3333
" {}".format(os.strerror(e.errno)))
3345
log.critical("Could not load persistent state:",
3335
logger.critical("Could not load persistent state:",
3348
3338
except EOFError as e:
3349
log.warning("Could not load persistent state: EOFError:",
3339
logger.warning("Could not load persistent state: "
3352
3343
with PGPEngine() as pgp:
3353
3344
for client_name, client in clients_data.items():
3380
3371
if client["enabled"]:
3381
3372
if datetime.datetime.utcnow() >= client["expires"]:
3382
3373
if not client["last_checked_ok"]:
3383
log.warning("disabling client %s - Client"
3384
" never performed a successful"
3385
" checker", client_name)
3375
"disabling client {} - Client never "
3376
"performed a successful checker".format(
3386
3378
client["enabled"] = False
3387
3379
elif client["last_checker_status"] != 0:
3388
log.warning("disabling client %s - Client"
3389
" last checker failed with error"
3390
" code %s", client_name,
3391
client["last_checker_status"])
3381
"disabling client {} - Client last"
3382
" checker failed with error code"
3385
client["last_checker_status"]))
3392
3386
client["enabled"] = False
3394
3388
client["expires"] = (
3395
3389
datetime.datetime.utcnow()
3396
3390
+ client["timeout"])
3397
log.debug("Last checker succeeded, keeping %s"
3398
" enabled", client_name)
3391
logger.debug("Last checker succeeded,"
3392
" keeping {} enabled".format(
3400
3395
client["secret"] = pgp.decrypt(
3401
3396
client["encrypted_secret"],
3402
3397
client_settings[client_name]["secret"])
3403
3398
except PGPError:
3404
3399
# If decryption fails, we use secret from new settings
3405
log.debug("Failed to decrypt %s old secret",
3400
logger.debug("Failed to decrypt {} old secret".format(
3407
3402
client["secret"] = (client_settings[client_name]
3605
3600
except NameError:
3607
3602
if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
3608
log.warning("Could not save persistent state: %s",
3609
os.strerror(e.errno))
3603
logger.warning("Could not save persistent state: {}"
3604
.format(os.strerror(e.errno)))
3611
log.warning("Could not save persistent state:",
3606
logger.warning("Could not save persistent state:",
3615
3610
# Delete all clients, and settings from config
3642
3637
service.port = tcp_server.socket.getsockname()[1]
3644
log.info("Now listening on address %r, port %d, flowinfo %d,"
3645
" 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())
3647
log.info("Now listening on address %r, port %d",
3648
*tcp_server.socket.getsockname())
3643
logger.info("Now listening on address %r, port %d",
3644
*tcp_server.socket.getsockname())
3650
3646
# service.interface = tcp_server.socket.getsockname()[3]
3666
3662
lambda *args, **kwargs: (tcp_server.handle_request
3667
3663
(*args[2:], **kwargs) or True))
3669
log.debug("Starting main loop")
3665
logger.debug("Starting main loop")
3670
3666
main_loop.run()
3671
3667
except AvahiError as error:
3672
log.critical("Avahi Error", exc_info=error)
3668
logger.critical("Avahi Error", exc_info=error)
3675
3671
except KeyboardInterrupt:
3677
3673
print("", file=sys.stderr)
3678
log.debug("Server received KeyboardInterrupt")
3679
log.debug("Server exiting")
3674
logger.debug("Server received KeyboardInterrupt")
3675
logger.debug("Server exiting")
3680
3676
# Must run before the D-Bus bus name gets deregistered
3684
def parse_test_args():
3685
# type: () -> argparse.Namespace
3680
def should_only_run_tests():
3686
3681
parser = argparse.ArgumentParser(add_help=False)
3687
parser.add_argument("--check", action="store_true")
3688
parser.add_argument("--prefix", )
3682
parser.add_argument("--check", action='store_true')
3689
3683
args, unknown_args = parser.parse_known_args()
3691
# Remove test options from sys.argv
3684
run_tests = args.check
3686
# Remove --check argument from sys.argv
3692
3687
sys.argv[1:] = unknown_args
3695
3690
# Add all tests from doctest strings
3696
3691
def load_tests(loader, tests, none):
3698
3693
tests.addTests(doctest.DocTestSuite())
3701
if __name__ == "__main__":
3702
options = parse_test_args()
3696
if __name__ == '__main__':
3705
extra_test_prefix = options.prefix
3706
if extra_test_prefix is not None:
3707
if not (unittest.main(argv=[""], exit=False)
3708
.result.wasSuccessful()):
3710
class ExtraTestLoader(unittest.TestLoader):
3711
testMethodPrefix = extra_test_prefix
3712
# Call using ./scriptname --test [--verbose]
3713
unittest.main(argv=[""], testLoader=ExtraTestLoader())
3715
unittest.main(argv=[""])
3698
if should_only_run_tests():
3699
# Call using ./mandos --check [--verbose]
3719
3704
logging.shutdown()
3723
# (lambda (&optional extra)
3724
# (if (not (funcall run-tests-in-test-buffer default-directory
3726
# (funcall show-test-buffer-in-test-window)
3727
# (funcall remove-test-window)
3728
# (if extra (message "Extra tests run successfully!"))))
3729
# run-tests-in-test-buffer:
3730
# (lambda (dir &optional extra)
3731
# (with-current-buffer (get-buffer-create "*Test*")
3732
# (setq buffer-read-only nil
3733
# default-directory dir)
3735
# (compilation-mode))
3736
# (let ((process-result
3737
# (let ((inhibit-read-only t))
3738
# (process-file-shell-command
3739
# (funcall get-command-line extra) nil "*Test*"))))
3740
# (and (numberp process-result)
3741
# (= process-result 0))))
3743
# (lambda (&optional extra)
3744
# (let ((quoted-script
3745
# (shell-quote-argument (funcall get-script-name))))
3747
# (concat "%s --check" (if extra " --prefix=atest" ""))
3751
# (if (fboundp 'file-local-name)
3752
# (file-local-name (buffer-file-name))
3753
# (or (file-remote-p (buffer-file-name) 'localname)
3754
# (buffer-file-name))))
3755
# remove-test-window:
3757
# (let ((test-window (get-buffer-window "*Test*")))
3758
# (if test-window (delete-window test-window))))
3759
# show-test-buffer-in-test-window:
3761
# (when (not (get-buffer-window-list "*Test*"))
3762
# (setq next-error-last-buffer (get-buffer "*Test*"))
3763
# (let* ((side (if (>= (window-width) 146) 'right 'bottom))
3764
# (display-buffer-overriding-action
3765
# `((display-buffer-in-side-window) (side . ,side)
3766
# (window-height . fit-window-to-buffer)
3767
# (window-width . fit-window-to-buffer))))
3768
# (display-buffer "*Test*"))))
3771
# (let* ((run-extra-tests (lambda () (interactive)
3772
# (funcall run-tests t)))
3773
# (inner-keymap `(keymap (116 . ,run-extra-tests))) ; t
3774
# (outer-keymap `(keymap (3 . ,inner-keymap)))) ; C-c
3775
# (setq minor-mode-overriding-map-alist
3776
# (cons `(run-tests . ,outer-keymap)
3777
# minor-mode-overriding-map-alist)))
3778
# (add-hook 'after-save-hook run-tests 90 t))