Commits

Ronald Oussoren committed edf0449

Various updates

* Finish the command line tool

* Provide more information in the documentation

Comments (0)

Files changed (6)

 asl_open = aslclient
 asl_new = aslmsg
 
+
 def ASL_FILTER_MASK(level):
     return 1 << level
 
+
 def ASL_FILTER_MASK_UPTO(level):
-    return (1 << (level+1)) - 1
+    return (1 << (level + 1)) - 1
 
 LEVEL2STRING = {
     ASL_LEVEL_EMERG: ASL_STRING_EMERG,
+from __future__ import absolute_import, print_function
 import argparse
+import os
+import sys
+import collections
+
+if sys.version_info[0] == 2:
+    # Use string.Formatter on Python 2 because str doesn't
+    # have a 'format_map' method yet.
+    from string import Formatter
 
 import asl
-import os
 
 
-LEVELS=list(sorted(asl.STRING2LEVEL, key=lambda l: asl.STRING2LEVEL[l]))
+LEVELS = list(sorted(asl.STRING2LEVEL, key=lambda l: asl.STRING2LEVEL[l]))
 
 parser = argparse.ArgumentParser(description="ASL command-line interface", prog=__package__)
 parser.add_argument('--version', action='version', version='%(prog)s ' + asl.__version__)
 )
 parser_consolelog.add_argument('message', help='The message', metavar='MESSAGE', nargs='+')
 
+parser_sendlog = sub.add_parser('sendlog', help='Write log message')
+parser_sendlog.set_defaults(action='sendlog')
+parser_sendlog.add_argument(
+    '-k', nargs=2, action='append',
+    dest='keys', metavar=('KEY', 'VALUE'),
+    help='Add a key and value to the message')
+
+parser_query = sub.add_parser('query', help='Perform ASL query')
+parser_query.set_defaults(action='query')
+parser_query.add_argument(
+    '-f', '--format',
+    action='store', dest='format', default=None,
+    metavar='FMT', help='Format string for showing the record')
+parser_query.add_argument(
+    '-C', action='store_true',
+    dest='show_console', help='include Console.app log entries')
+parser_query.add_argument(
+    '-k', action='append', dest='query',
+    nargs=3, metavar=('KEY', 'OP', 'VALUE'),
+    help='add query element')
+parser_query.add_argument(
+    '-e', action='append', dest='exists',
+    metavar='KEY', help='query that key exists')
+
 
 def do_consolelog(options):
     ident = options.ident
 
     cli.log(msg, asl.STRING2LEVEL[level], message)
 
+
+def do_sendlog(opts):
+    if not opts.keys:
+        print("No message arguments specified", file=sys.stderr)
+        sys.exit(1)
+
+    cli = asl.aslclient(ident=None, facility='user', options=0)
+    msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+    for k, v in opts.keys:
+        msg[k] = v
+
+    cli.send(msg)
+
+
+OP_MAP = {
+    'eq': asl.ASL_QUERY_OP_EQUAL,
+    'ne': asl.ASL_QUERY_OP_NOT_EQUAL,
+    'gt': asl.ASL_QUERY_OP_GREATER,
+    'ge': asl.ASL_QUERY_OP_GREATER_EQUAL,
+    'le': asl.ASL_QUERY_OP_LESS_EQUAL,
+    'lt': asl.ASL_QUERY_OP_LESS,
+    'match': asl.ASL_QUERY_OP_REGEX,
+    'Ceq': asl.ASL_QUERY_OP_EQUAL | asl.ASL_QUERY_OP_CASEFOLD,
+    'Cne': asl.ASL_QUERY_OP_NOT_EQUAL | asl.ASL_QUERY_OP_CASEFOLD,
+    'Cgt': asl.ASL_QUERY_OP_GREATER | asl.ASL_QUERY_OP_CASEFOLD,
+    'Cge': asl.ASL_QUERY_OP_GREATER_EQUAL | asl.ASL_QUERY_OP_CASEFOLD,
+    'Cle': asl.ASL_QUERY_OP_LESS_EQUAL | asl.ASL_QUERY_OP_CASEFOLD,
+    'Clt': asl.ASL_QUERY_OP_LESS | asl.ASL_QUERY_OP_CASEFOLD,
+    'Cmatch': asl.ASL_QUERY_OP_REGEX | asl.ASL_QUERY_OP_CASEFOLD,
+    '==': asl.ASL_QUERY_OP_EQUAL | asl.ASL_QUERY_OP_NUMERIC,
+    '!=': asl.ASL_QUERY_OP_NOT_EQUAL | asl.ASL_QUERY_OP_NUMERIC,
+    '>': asl.ASL_QUERY_OP_GREATER | asl.ASL_QUERY_OP_NUMERIC,
+    '>=': asl.ASL_QUERY_OP_GREATER_EQUAL | asl.ASL_QUERY_OP_NUMERIC,
+    '<=': asl.ASL_QUERY_OP_LESS_EQUAL | asl.ASL_QUERY_OP_NUMERIC,
+    '<': asl.ASL_QUERY_OP_LESS | asl.ASL_QUERY_OP_NUMERIC,
+    'startswith': asl.ASL_QUERY_OP_EQUAL | asl.ASL_QUERY_OP_PREFIX,
+    'endswith': asl.ASL_QUERY_OP_EQUAL | asl.ASL_QUERY_OP_SUFFIX,
+    'contains': asl.ASL_QUERY_OP_EQUAL | asl.ASL_QUERY_OP_SUBSTRING,
+    'Cstartswith': asl.ASL_QUERY_OP_EQUAL | asl.ASL_QUERY_OP_PREFIX | asl.ASL_QUERY_OP_CASEFOLD,
+    'Cendswith': asl.ASL_QUERY_OP_EQUAL | asl.ASL_QUERY_OP_SUFFIX | asl.ASL_QUERY_OP_CASEFOLD,
+    'Ccontains': asl.ASL_QUERY_OP_EQUAL | asl.ASL_QUERY_OP_SUBSTRING | asl.ASL_QUERY_OP_CASEFOLD,
+}
+
+def do_query(opts):
+    cli = asl.aslclient(ident=None, facility='user', options=0)
+    msg = asl.aslmsg(asl.ASL_TYPE_QUERY)
+    if opts.show_console:
+        msg.set_query(asl.ASL_KEY_FACILITY, "com.apple.console", asl.ASL_QUERY_OP_EQUAL)
+
+    # Other search parameters
+    if opts.query:
+        valid = True
+        for key, op, value in opts.query:
+            try:
+                msg.set_query(key, value, OP_MAP[op])
+
+            except KeyError:
+                valid = False
+                print("Invalid query operation:", op, file=sys.stderr)
+
+        if not valid:
+            sys.exit(1)
+
+    if opts.exists:
+        for key in opts.exists:
+            msg.set_query(key, '', asl.ASL_QUERY_OP_TRUE)
+
+    for record in cli.search(msg):
+        if opts.format is None:
+            for key in sorted(record.keys()):
+                print("{} {}".format(key, record[key]))
+            print()
+
+        else:
+            fmt_arg = collections.defaultdict(str)
+            fmt_arg.update({k: record[k] for k in record.keys()})
+
+            if sys.version_info[0] == 2:
+                print(Formatter().vformat(opts.format, (), fmt_arg))
+            else:
+                print(opts.format.format_map(fmt_arg))
+
+
 def main():
     opts = parser.parse_args()
     if opts.action == 'consolelog':
         do_consolelog(opts)
 
+    elif opts.action == 'sendlog':
+        do_sendlog(opts)
+
+    elif opts.action == 'query':
+        do_query(opts)
+
+    else:
+        raise NotImplementedError("Action: %s" % (opts.action,))
+
 if __name__ == "__main__":
     main()

asl_tests/test_misc.py

 import unittest
 import platform
+import os
+import sys
 
 import asl
 
+try:
+    long
+except NameError:
+    long = int
+
 class TestMiscFunctions (unittest.TestCase):
     def test_mask(self):
         self.assertEqual(asl.ASL_FILTER_MASK(1), 1<<1)
 
     @unittest.skipUnless(platform.mac_ver()[0] >= "10.7", "Requires OSX 10.7")
     def test_create_auxiliary_file(self):
-        self.fail()
+        msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+        msg[asl.ASL_KEY_MSG] = 'hello world'
+
+        fd = asl.create_auxiliary_file(msg, 'title', None)
+        self.assertIsInstance(fd, (int, long))
+        os.write(fd, b'hello world\n')
+        asl.close_auxiliary_file(fd)
+
+        self.assertRaises(OSError, os.write, fd, b'Done')
+
+        msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+        msg[asl.ASL_KEY_MSG] = 'hello world'
+
+        fd = asl.create_auxiliary_file(msg, 'title', 'public.text')
+        self.assertIsInstance(fd, (int, long))
+        os.write(fd, b'hello world\n')
+        asl.close_auxiliary_file(fd)
+
+        msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+        msg[asl.ASL_KEY_MSG] = 'hello world'
+
+        self.assertRaises(TypeError, asl.create_auxiliary_file, 'foo', 'title', None)
+        self.assertRaises(TypeError, asl.create_auxiliary_file, None, 42, None)
+        self.assertRaises(TypeError, asl.create_auxiliary_file, None, 'title', 42)
+
+    @unittest.skipUnless(platform.mac_ver()[0] >= "10.7" and sys.version_info[0] == 3, "Requires OSX 10.7, Python 3 test")
+    def test_no_bytes(self):
+        msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+        msg[asl.ASL_KEY_MSG] = 'hello world'
+        self.assertRaises(TypeError, asl.create_auxiliary_file, None, b'title', None)
+        self.assertRaises(TypeError, asl.create_auxiliary_file, None, 'title', b'public.text')
+
+        msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+        msg[asl.ASL_KEY_MSG] = 'hello world'
+        self.assertRaises(TypeError, asl.log_auxiliary_location, msg, b'title', 'public.txt', 'http://www.python.org/')
+        self.assertRaises(TypeError, asl.log_auxiliary_location, msg, 'title', b'public.txt', 'http://www.python.org/')
+        self.assertRaises(TypeError, asl.log_auxiliary_location, msg, 'title', 'public.txt', b'http://www.python.org/')
+
+    @unittest.skipUnless(platform.mac_ver()[0] >= "10.7" and sys.version_info[0] == 2, "Requires OSX 10.7, Python 2 test")
+    def test_with_unicode(self):
+        msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+        msg[asl.ASL_KEY_MSG] = 'hello world'
+
+        fd = asl.create_auxiliary_file(msg, b'title'.decode('utf-8'), 'public.text')
+        self.assertIsInstance(fd, (int, long))
+        os.write(fd, b'hello world\n')
+        asl.close_auxiliary_file(fd)
+
+        msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+        msg[asl.ASL_KEY_MSG] = 'hello world'
+
+        fd = asl.create_auxiliary_file(msg, 'title', b'public.text'.decode('utf-8'))
+        self.assertIsInstance(fd, (int, long))
+        os.write(fd, b'hello world\n')
+        asl.close_auxiliary_file(fd)
+
+        msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+        msg[asl.ASL_KEY_MSG] = 'hello world'
+        asl.log_auxiliary_location(msg, b'title'.decode('utf-8'), 'public.text', 'http://www.python.org/')
+
+        msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+        msg[asl.ASL_KEY_MSG] = 'hello world'
+        asl.log_auxiliary_location(msg, 'title', b'public.text'.decode('utf-8'), 'http://www.python.org/')
+
+        msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+        msg[asl.ASL_KEY_MSG] = 'hello world'
+        asl.log_auxiliary_location(msg, 'title', 'public.text', b'http://www.python.org/'.decode('utf-8'))
+
 
     @unittest.skipUnless(platform.mac_ver()[0] >= "10.7", "Requires OSX 10.7")
     def test_log_auxiliary_location(self):
-        self.fail()
+        msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+        msg[asl.ASL_KEY_MSG] = 'hello world'
+        asl.log_auxiliary_location(msg, 'title', 'public.text', 'http://www.python.org/')
+
+        msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+        msg[asl.ASL_KEY_MSG] = 'hello world'
+        asl.log_auxiliary_location(msg, 'title', None, 'http://www.python.org/')
+
+        msg = asl.aslmsg(asl.ASL_TYPE_MSG)
+        msg[asl.ASL_KEY_MSG] = 'hello world'
+        self.assertRaises(TypeError, asl.log_auxiliary_location, msg, 'title', None, None)
+        self.assertRaises(TypeError, asl.log_auxiliary_location, msg, 'title', None, 42)
+        self.assertRaises(TypeError, asl.log_auxiliary_location, msg, 'title', 42, 'http://www.python.org')
+        self.assertRaises(TypeError, asl.log_auxiliary_location, msg, 42, None, 'http://www.python.org')
+
 
 
 if __name__ == "__main__":
 goal to the classic unix syslog API, but with a richer interface
 and with an API for querying the logging system.
 
-See Apple's manual page for the C API for more information on how
-to use this module.
+There is more information on the ASL library in `Apple's manual
+page for ASL`__
+
+.. __: https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man3/asl.3.html
 
 .. note::
 
 Message priority levels
 .......................
 
+The levels are listed from highest to lowest priority.
+
 .. data:: ASL_LEVEL_EMERG
 
 .. data::  ASL_LEVEL_ALERT
 Attribute matching operations
 .............................
 
-
 Modifiers
 ~~~~~~~~~
 
 .. data::  ASL_QUERY_OP_CASEFOLD
 
+   String comparisons are case folded
+
 .. data::  ASL_QUERY_OP_PREFIX
 
+   The match is done on a leading substring
+
 .. data::  ASL_QUERY_OP_SUFFIX
 
+   The match is done on a trailing  substring
+
 .. data::  ASL_QUERY_OP_SUBSTRING
 
+   Match any substring
+
 .. data::  ASL_QUERY_OP_NUMERIC
 
-.. data::  ASL_QUERY_OP_REGEX
+   Perform the comparison after converting the value
+   to an integer using the C function ``atoi``.
+
 
 
 Operators
 ~~~~~~~~~
 
+.. data::  ASL_QUERY_OP_REGEX
+
+   Perform a regular expression match using the
+   `regex library`__. When the :data:`ALS_QUERY_OP_CASEFOLD`
+   modifier is specified the regular expression is compiled
+   case insensitive (*REG_ICASE*). All other modifiers
+   are ignored.
+
+.. __: https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man3/regex.3.html
+
 .. data::  ASL_QUERY_OP_EQUAL
 
+   Value equality
+
 .. data::  ASL_QUERY_OP_GREATER
 
+   Value greater than
+
 .. data::  ASL_QUERY_OP_GREATER_EQUAL
 
+   Value greater than or equal to
+
 .. data::  ASL_QUERY_OP_LESS
 
+   Value less than
+
 .. data::  ASL_QUERY_OP_LESS_EQUAL
 
+   Value less than or equal to
+
 .. data::  ASL_QUERY_OP_NOT_EQUAL
 
+   Value not equal
+
 .. data::  ASL_QUERY_OP_TRUE
 
+   Always true. Use this to test if an attribute is present.
+
 
 Standard message attributes
 ...........................

doc/changelog.rst

 Release history
 ===============
 
+asl 1.0
+-------
+
+* First public release
+
+* Adds command-line interface (``python3 -m asl --help``)
+
+* Add translation from numeric ID to string, and the reverse, for log levels
+
+* Updated the documentation
+
 asl 0.9
 -------
 
 
 ::
 
-  $ python3 -m asl consolelog [-i IDENT] [-l LEVEL] message ...
+   $ python3 -m asl consolelog [-i IDENT] [-l LEVEL] message ...
 
 This logs the message arguments as a single line (separated
 by spaces) with the attributes needed to end up in the default
 priority that will end up in the default view of Console.app.
 
 
+Generic logging
+...............
+
+::
+
+    $ python3 -m asl writelog {-k KEY VALUE}...
+
+Write a log message to the ASL, with all message parameters under control
+of the user.
+
+The *IDENT* is the source identifier and is usually an application
+name. It defaults to the name of the user.
+
+The *FACILITY* is the log facility.
+
+The *LEVEL* is the log level, and is one of "Emergency", "Alert",
+"Critical", "Error", "Warning", "Notice", "Info" and "Debug" (from
+high to low priority). The default is "Notice".
+
+The "-k" option is used to set the attributes of the log message. You should
+at least include a "Message" key with the text of the log message.
+
+The standard keys are:
+
+* *Time*: Time of logging (added by the ASL daemon)
+* *TimeNanoSec* Nano-second resolution time (added by the ASL daemon)
+* *Host*: Host where the message was written
+* *Sender*: Sender of the log message
+* *Facility*: Log facility
+
+  This is either one of the regular
+  syslog facilities (auth, authpriv, cron, ..., or an arbitrary string.
+  Messages that should be shown in the default view of Console.app should use facility
+  "com.apple.console".
+
+* *PID*: Process ID of the sending proces (replaced by the ASL daemon)
+
+* *UID*: UID of the sending process (replaced by the ASL daemon)
+
+* *GID*: GUID of the sending proccess (replaced by the ASL daemon)
+
+* *Level*: Log level (see the -l command of the *consolelog* command)
+
+* *Message*: Text of the log message
+
+* *ReadUID*: UID that may read the message, use "-1" for "all users"
+
+* *ReadGID*: GID that may read the message, use "-1" for "all groups"
+
+* *ASLExpireTime*: Expiry time for this message (default to 7 days after the log time)
+
+* *ASLMessageID* Message ID for this message (replaced by the ASL daemon)
+
+* *Session*: Session ID
+
+* *RefPID* Reference PID for messages proxied by launchd
+
+* *RefProc*: Reference process for messages proxied by launchd
+
+* *ASLAuxTitle*: Auxiliary title string
+
+* *ASLAuxUTI*: Auxiliary UTI
+
+* *ASLAuxURL*: Auxiliary URL
+
+* *ASLAuxData*: Auxiliary in-line data
+
+* *SenderInstance*: Sender instance UUID
+
+Query the ASL database
+......................
+
+::
+
+   python3 -m asl query [-f FMT] [--format FMT] [-C] {-e KEY}... {-k KEY OP VALUE}...
+
+Search the ASL database for matching records. All found records are printed on
+stdout. By default all records are printed, and using the "-C", "-e" and "-k" options
+a subset can be selected:
+
+When mutiple "-C", "-e" and "-k" are present all of them must match (that is,
+the subexpressions are combined with an "AND" operator).
+
+* "-f FMT" or "--format FMT"
+
+  Use FMT for formatting the records before printing them. The format string is
+  a format string that can be using with :class:`str.format`, and it will be used
+  in such a way that non-existing keys are ignored.
+
+  When this option is not used all attributes of records are printed and records
+  are separated by a single empty line.
+
+* "-C"
+
+  Add selection for messages destined for the Console.app. This is equivalent
+  to "-k Facility eq com.apple.console"
+
+* "-e KEY"
+
+  Add a test that checks that attribute *KEY* exists.
+
+* "-k KEY OP VALUE"
+
+  Add a test for the value of attribute *KEY*.
+
+  The following operators are recognized:
+
+  * *eq*: The value must be equal to the specified value
+
+  * *ne*: The value must not be equal to the specified value
+
+  * *gt*: The value must be lexographically greater than the specified value
+
+  * *ge*: The value must be lexographically greater than or equal to the specified value
+
+  * *lt*: The value must be lexographically less than the specified value
+
+  * *le*: The value must be lexographically less than or equal to the specified value
+
+  * *match*: The value must match the specified regular expression (see regex(3))
+
+  * *startswith*: The value must start with the specified value
+
+  * *endswith*: The value must end with the specified value
+
+  * *contains*: The value must contain the specified value
+
+  * *==*: The value must be a number and must be equal to the specified value
+
+  * *!=*: The value must be a number and must be different from the specified value
+
+  * *>*: The value must be a number and must be greater than the specified value
+
+  * *>=*: The value must be a number and must be greater than or equal to the specified value
+
+  * *<*: The value must be a number and must be less than the specified value
+
+  * *<=*: The value must be a number and must be less than or equal to the specified value
+
+  The string operators can be prefix with "C" to perform case folding before the comparison.
+
+  See the section on `Generic logging`_ for a description of the standard keys.