458 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			458 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
	
| """
 | |
| Base class for DarwinLog tests.
 | |
| """
 | |
| 
 | |
| # System imports
 | |
| from __future__ import print_function
 | |
| 
 | |
| import json
 | |
| import os
 | |
| import platform
 | |
| import re
 | |
| import sys
 | |
| import threading
 | |
| 
 | |
| 
 | |
| # lldb imports
 | |
| import lldb
 | |
| from lldb import SBProcess, SBTarget
 | |
| 
 | |
| from lldbsuite.test import decorators
 | |
| from lldbsuite.test import lldbtest
 | |
| from lldbsuite.test import lldbtest_config
 | |
| from lldbsuite.test import lldbutil
 | |
| 
 | |
| 
 | |
| def expand_darwinlog_command(command):
 | |
|     return "plugin structured-data darwin-log " + command
 | |
| 
 | |
| 
 | |
| def expand_darwinlog_settings_set_command(command):
 | |
|     return "settings set plugin.structured-data.darwin-log." + command
 | |
| 
 | |
| 
 | |
| class DarwinLogTestBase(lldbtest.TestBase):
 | |
|     """Base class for DarwinLog test cases that are pexpect-based."""
 | |
|     NO_DEBUG_INFO_TESTCASE = True
 | |
| 
 | |
|     CONTINUE_REGEX = re.compile(r"Process \d+ resuming")
 | |
| 
 | |
|     def setUp(self):
 | |
|         # Call super's setUp().
 | |
|         super(DarwinLogTestBase, self).setUp()
 | |
| 
 | |
|         # Until other systems support this, exit
 | |
|         # early if we're not macOS version 10.12
 | |
|         # or greater.
 | |
|         version = platform.mac_ver()[0].split('.')
 | |
|         if ((int(version[0]) == 10) and (int(version[1]) < 12) or
 | |
|                 (int(version[0]) < 10)):
 | |
|             self.skipTest("DarwinLog tests currently require macOS 10.12+")
 | |
|             return
 | |
| 
 | |
|         self.child = None
 | |
|         self.child_prompt = '(lldb) '
 | |
|         self.strict_sources = False
 | |
|         self.enable_process_monitor_logging = False
 | |
| 
 | |
|     def run_lldb_to_breakpoint(self, exe, source_file, line,
 | |
|                                enable_command=None, settings_commands=None):
 | |
| 
 | |
|         import pexpect
 | |
|         # Set self.child_prompt, which is "(lldb) ".
 | |
|         prompt = self.child_prompt
 | |
| 
 | |
|         # So that the child gets torn down after the test.
 | |
|         self.child = pexpect.spawn('%s %s %s' % (lldbtest_config.lldbExec,
 | |
|                                                  self.lldbOption, exe))
 | |
|         child = self.child
 | |
| 
 | |
|         # Turn on logging for what the child sends back.
 | |
|         if self.TraceOn():
 | |
|             child.logfile_read = sys.stdout
 | |
| 
 | |
|         if self.enable_process_monitor_logging:
 | |
|             if platform.system() == 'Darwin':
 | |
|                 self.runCmd(
 | |
|                     "settings set target.process.extra-startup-command "
 | |
|                     "QSetLogging:bitmask=LOG_DARWIN_LOG;")
 | |
|                 self.expect_prompt()
 | |
| 
 | |
|         # Run the enable command if we have one.
 | |
|         if enable_command is not None:
 | |
|             self.runCmd(enable_command)
 | |
|             self.expect_prompt()
 | |
| 
 | |
|         # Disable showing of source lines at our breakpoint.
 | |
|         # This is necessary for the logging tests, because the very
 | |
|         # text we want to match for output from the running inferior
 | |
|         # will show up in the source as well.  We don't want the source
 | |
|         # output to erroneously make a match with our expected output.
 | |
|         self.runCmd("settings set stop-line-count-before 0")
 | |
|         self.expect_prompt()
 | |
|         self.runCmd("settings set stop-line-count-after 0")
 | |
|         self.expect_prompt()
 | |
| 
 | |
|         # While we're debugging, turn on packet logging.
 | |
|         self.runCmd("log enable -f /tmp/packets.log gdb-remote packets")
 | |
|         self.expect_prompt()
 | |
| 
 | |
|         # Prevent mirroring of NSLog/os_log content to stderr.  We want log
 | |
|         # messages to come exclusively through our log channel.
 | |
|         self.runCmd(
 | |
|             "settings set target.env-vars IDE_DISABLED_OS_ACTIVITY_DT_MODE=1")
 | |
|         self.expect_prompt()
 | |
| 
 | |
|         # Run any darwin-log settings commands now, before we enable logging.
 | |
|         if settings_commands is not None:
 | |
|             for setting_command in settings_commands:
 | |
|                 self.runCmd(
 | |
|                     expand_darwinlog_settings_set_command(setting_command))
 | |
|                 self.expect_prompt()
 | |
| 
 | |
|         # Set breakpoint right before the os_log() macros.  We don't
 | |
|         # set it on the os_log*() calls because these are a number of
 | |
|         # nested-scoped calls that will cause the debugger to stop
 | |
|         # multiple times on the same line.  That is difficult to match
 | |
|         # os_log() content by since it is non-deterministic what the
 | |
|         # ordering between stops and log lines will be.  This is why
 | |
|         # we stop before, and then have the process run in a sleep
 | |
|         # afterwards, so we get the log messages while the target
 | |
|         # process is "running" (sleeping).
 | |
|         child.sendline('breakpoint set -f %s -l %d' % (source_file, line))
 | |
|         child.expect_exact(prompt)
 | |
| 
 | |
|         # Now run to the breakpoint that we just set.
 | |
|         child.sendline('run')
 | |
|         child.expect_exact(prompt)
 | |
| 
 | |
|         # Ensure we stopped at a breakpoint.
 | |
|         self.runCmd("thread list")
 | |
|         self.expect(re.compile(r"stop reason = breakpoint"))
 | |
| 
 | |
|         # Now we're ready to check if DarwinLog is available.
 | |
|         if not self.darwin_log_available():
 | |
|             self.skipTest("DarwinLog not available")
 | |
| 
 | |
|     def runCmd(self, cmd):
 | |
|         self.child.sendline(cmd)
 | |
| 
 | |
|     def expect_prompt(self, exactly=True):
 | |
|         self.expect(self.child_prompt, exactly=exactly)
 | |
| 
 | |
|     def expect(self, pattern, exactly=False, *args, **kwargs):
 | |
|         if exactly:
 | |
|             return self.child.expect_exact(pattern, *args, **kwargs)
 | |
|         return self.child.expect(pattern, *args, **kwargs)
 | |
| 
 | |
|     def darwin_log_available(self):
 | |
|         self.runCmd("plugin structured-data darwin-log status")
 | |
|         self.expect(re.compile(r"Availability: ([\S]+)"))
 | |
|         return self.child.match is not None and (
 | |
|             self.child.match.group(1) == "available")
 | |
| 
 | |
|     def do_test(self, enable_options, expect_regexes=None,
 | |
|                 settings_commands=None):
 | |
|         """Test that a single fall-through reject rule rejects all logging."""
 | |
|         self.build(dictionary=self.d)
 | |
|         self.setTearDownCleanup(dictionary=self.d)
 | |
| 
 | |
|         # Build the darwin-log enable command.
 | |
|         enable_cmd = expand_darwinlog_command('enable')
 | |
|         if enable_options is not None and len(enable_options) > 0:
 | |
|             enable_cmd += ' ' + ' '.join(enable_options)
 | |
| 
 | |
|         exe = os.path.join(os.getcwd(), self.exe_name)
 | |
|         self.run_lldb_to_breakpoint(exe, self.source, self.line,
 | |
|                                     enable_command=enable_cmd,
 | |
|                                     settings_commands=settings_commands)
 | |
|         self.expect_prompt()
 | |
| 
 | |
|         # Now go.
 | |
|         self.runCmd("process continue")
 | |
|         self.expect(self.CONTINUE_REGEX)
 | |
| 
 | |
|         if expect_regexes is None:
 | |
|             # Expect matching a log line or program exit.
 | |
|             # Test methods determine which ones are valid.
 | |
|             expect_regexes = (
 | |
|                 [re.compile(r"source-log-([^-]+)-(\S+)"),
 | |
|                  re.compile(r"exited with status")
 | |
|                  ])
 | |
|         self.expect(expect_regexes)
 | |
| 
 | |
| 
 | |
| def remove_add_mode_entry(log_entries):
 | |
|     """libtrace creates an "Add Mode:..." message when logging is enabled.
 | |
|     Strip this out of results since our test subjects don't create it."""
 | |
|     return [entry for entry in log_entries
 | |
|             if "message" in entry and
 | |
|             not entry["message"].startswith("Add Mode:")]
 | |
| 
 | |
| 
 | |
| class DarwinLogEventBasedTestBase(lldbtest.TestBase):
 | |
|     """Base class for event-based DarwinLog tests."""
 | |
|     NO_DEBUG_INFO_TESTCASE = True
 | |
| 
 | |
|     class EventListenerThread(threading.Thread):
 | |
| 
 | |
|         def __init__(self, listener, process, trace_on, max_entry_count):
 | |
|             super(
 | |
|                 DarwinLogEventBasedTestBase.EventListenerThread,
 | |
|                 self).__init__()
 | |
|             self.process = process
 | |
|             self.listener = listener
 | |
|             self.trace_on = trace_on
 | |
|             self.max_entry_count = max_entry_count
 | |
|             self.exception = None
 | |
|             self.structured_data_event_count = 0
 | |
|             self.wait_seconds = 2
 | |
|             self.max_timeout_count = 4
 | |
|             self.log_entries = []
 | |
| 
 | |
|         def handle_structured_data_event(self, event):
 | |
|             structured_data = SBProcess.GetStructuredDataFromEvent(event)
 | |
|             if not structured_data.IsValid():
 | |
|                 if self.trace_on:
 | |
|                     print("invalid structured data")
 | |
|                 return
 | |
| 
 | |
|             # Track that we received a valid structured data event.
 | |
|             self.structured_data_event_count += 1
 | |
| 
 | |
|             # Grab the individual log entries from the JSON.
 | |
|             stream = lldb.SBStream()
 | |
|             structured_data.GetAsJSON(stream)
 | |
|             dict = json.loads(stream.GetData())
 | |
|             self.log_entries.extend(dict["events"])
 | |
|             if self.trace_on:
 | |
|                 print("Structured data (raw):", stream.GetData())
 | |
| 
 | |
|             # Print the pretty-printed version.
 | |
|             if self.trace_on:
 | |
|                 stream.Clear()
 | |
|                 structured_data.PrettyPrint(stream)
 | |
|                 print("Structured data (pretty print):",
 | |
|                       stream.GetData())
 | |
| 
 | |
|         def done(self, timeout_count):
 | |
|             """Returns True when we're done listening for events."""
 | |
|             # See if we should consider the number of events retrieved.
 | |
|             if self.max_entry_count is not None:
 | |
|                 if len(self.log_entries) >= self.max_entry_count:
 | |
|                     # We've received the max threshold of events expected,
 | |
|                     # we can exit here.
 | |
|                     if self.trace_on:
 | |
|                         print("Event listener thread exiting due to max "
 | |
|                               "expected log entry count being reached.")
 | |
|                     return True
 | |
| 
 | |
|             # If our event timeout count has exceeded our maximum timeout count,
 | |
|             # we're done.
 | |
|             if timeout_count >= self.max_timeout_count:
 | |
|                 if self.trace_on:
 | |
|                     print("Event listener thread exiting due to max number of "
 | |
|                           "WaitForEvent() timeouts being reached.")
 | |
|                 return True
 | |
| 
 | |
|             # If our process is dead, we're done.
 | |
|             if not self.process.is_alive:
 | |
|                 if self.trace_on:
 | |
|                     print("Event listener thread exiting due to test inferior "
 | |
|                           "exiting.")
 | |
|                 return True
 | |
| 
 | |
|             # We're not done.
 | |
|             return False
 | |
| 
 | |
|         def run(self):
 | |
|             event = lldb.SBEvent()
 | |
|             try:
 | |
|                 timeout_count = 0
 | |
| 
 | |
|                 # Wait up to 4 times for the event to arrive.
 | |
|                 while not self.done(timeout_count):
 | |
|                     if self.trace_on:
 | |
|                         print("Calling wait for event...")
 | |
|                     if self.listener.WaitForEvent(self.wait_seconds, event):
 | |
|                         while event.IsValid():
 | |
|                             # Check if it's a process event.
 | |
|                             if SBProcess.EventIsStructuredDataEvent(event):
 | |
|                                 self.handle_structured_data_event(event)
 | |
|                             else:
 | |
|                                 if self.trace_on:
 | |
|                                     print("ignoring unexpected event:",
 | |
|                                           lldbutil.get_description(event))
 | |
|                             # Grab the next event, if there is one.
 | |
|                             event.Clear()
 | |
|                             if not self.listener.GetNextEvent(event):
 | |
|                                 if self.trace_on:
 | |
|                                     print("listener has no more events "
 | |
|                                           "available at this time")
 | |
|                     else:
 | |
|                         if self.trace_on:
 | |
|                             print("timeout occurred waiting for event...")
 | |
|                         timeout_count += 1
 | |
|                 self.listener.Clear()
 | |
|             except Exception as e:
 | |
|                 self.exception = e
 | |
| 
 | |
|     def setUp(self):
 | |
|         # Call super's setUp().
 | |
|         super(DarwinLogEventBasedTestBase, self).setUp()
 | |
| 
 | |
|         # Until other systems support this, exit
 | |
|         # early if we're not macOS version 10.12
 | |
|         # or greater.
 | |
|         version = platform.mac_ver()[0].split('.')
 | |
|         if ((int(version[0]) == 10) and (int(version[1]) < 12) or
 | |
|                 (int(version[0]) < 10)):
 | |
|             self.skipTest("DarwinLog tests currently require macOS 10.12+")
 | |
|             return
 | |
| 
 | |
|         # Source filename.
 | |
|         self.source = 'main.c'
 | |
| 
 | |
|         # Output filename.
 | |
|         self.exe_name = 'a.out'
 | |
|         self.d = {'C_SOURCES': self.source, 'EXE': self.exe_name}
 | |
| 
 | |
|         # Locate breakpoint.
 | |
|         self.line = lldbtest.line_number(self.source, '// break here')
 | |
| 
 | |
|         # Enable debugserver logging of the darwin log collection
 | |
|         # mechanism.
 | |
|         self.runCmd("settings set target.process.extra-startup-command "
 | |
|                     "QSetLogging:bitmask=LOG_DARWIN_LOG;")
 | |
| 
 | |
|     def darwin_log_available(self):
 | |
|         match = self.match("plugin structured-data darwin-log status",
 | |
|                            patterns=[r"Availability: ([\S]+)"])
 | |
|         return match is not None and (match.group(1) == "available")
 | |
| 
 | |
|     def do_test(self, enable_options, settings_commands=None,
 | |
|                 run_enable_after_breakpoint=False, max_entry_count=None):
 | |
|         """Runs the test inferior, returning collected events.
 | |
| 
 | |
|         This method runs the test inferior to the first breakpoint hit.
 | |
|         It then adds a listener for structured data events, and collects
 | |
|         all events from that point forward until end of execution of the
 | |
|         test inferior.  It then returns those events.
 | |
| 
 | |
|         @return
 | |
|             A list of structured data events received, in the order they
 | |
|             were received.
 | |
|         """
 | |
|         self.build(dictionary=self.d)
 | |
|         self.setTearDownCleanup(dictionary=self.d)
 | |
| 
 | |
|         exe = os.path.join(os.getcwd(), self.exe_name)
 | |
| 
 | |
|         # Create a target by the debugger.
 | |
|         target = self.dbg.CreateTarget(exe)
 | |
|         self.assertTrue(target, lldbtest.VALID_TARGET)
 | |
| 
 | |
|         # Run the darwin-log settings commands.
 | |
|         if settings_commands is not None:
 | |
|             for setting_command in settings_commands:
 | |
|                 self.runCmd(
 | |
|                     expand_darwinlog_settings_set_command(setting_command))
 | |
| 
 | |
|         # Build the darwin-log enable command.
 | |
|         enable_cmd = expand_darwinlog_command("enable")
 | |
|         if enable_options is not None and len(enable_options) > 0:
 | |
|             enable_cmd += ' ' + ' '.join(enable_options)
 | |
| 
 | |
|         # Run the darwin-log enable command now if we are not supposed
 | |
|         # to do it at the first breakpoint.  This tests the start-up
 | |
|         # code, which has the benefit of being able to set os_log-related
 | |
|         # environment variables.
 | |
|         if not run_enable_after_breakpoint:
 | |
|             self.runCmd(enable_cmd)
 | |
| 
 | |
|         # Create the breakpoint.
 | |
|         breakpoint = target.BreakpointCreateByLocation(self.source, self.line)
 | |
|         self.assertIsNotNone(breakpoint)
 | |
|         self.assertTrue(breakpoint.IsValid())
 | |
|         self.assertEqual(1, breakpoint.GetNumLocations(),
 | |
|                          "Should have found one breakpoint")
 | |
| 
 | |
|         # Enable packet logging.
 | |
|         # self.runCmd("log enable -f /tmp/packets.log gdb-remote packets")
 | |
|         # self.runCmd("log enable lldb process")
 | |
| 
 | |
|         # Launch the process - doesn't stop at entry.
 | |
|         process = target.LaunchSimple(None, None, os.getcwd())
 | |
|         self.assertIsNotNone(process, lldbtest.PROCESS_IS_VALID)
 | |
| 
 | |
|         # Keep track of whether we're tracing output.
 | |
|         trace_on = self.TraceOn()
 | |
| 
 | |
|         # Get the next thread that stops.
 | |
|         from lldbsuite.test.lldbutil import get_stopped_thread
 | |
|         thread = get_stopped_thread(process, lldb.eStopReasonBreakpoint)
 | |
| 
 | |
|         self.assertIsNotNone(thread, "There should be a thread stopped "
 | |
|                              "due to breakpoint")
 | |
| 
 | |
|         # The process should be stopped at this point.
 | |
|         self.expect("process status", lldbtest.PROCESS_STOPPED,
 | |
|                     patterns=['Process .* stopped'])
 | |
| 
 | |
|         # The stop reason of the thread should be breakpoint.
 | |
|         self.expect("thread list", lldbtest.STOPPED_DUE_TO_BREAKPOINT,
 | |
|                     substrs=['stopped', 'stop reason = breakpoint'])
 | |
| 
 | |
|         # And our one and only breakpoint should have been hit.
 | |
|         self.assertEquals(breakpoint.GetHitCount(), 1)
 | |
| 
 | |
|         # Check if DarwinLog is available.  This check cannot be done
 | |
|         # until after the process has started, as the feature availability
 | |
|         # comes through the stub.  The stub isn't running until
 | |
|         # the target process is running.  So this is really the earliest
 | |
|         # we can check.
 | |
|         if not self.darwin_log_available():
 | |
|             self.skipTest("DarwinLog not available")
 | |
| 
 | |
|         # Now setup the structured data listener.
 | |
|         #
 | |
|         # Grab the broadcaster for the process.  We'll be attaching our
 | |
|         # listener to it.
 | |
|         broadcaster = process.GetBroadcaster()
 | |
|         self.assertIsNotNone(broadcaster)
 | |
| 
 | |
|         listener = lldb.SBListener("SBStructuredData listener")
 | |
|         self.assertIsNotNone(listener)
 | |
| 
 | |
|         rc = broadcaster.AddListener(
 | |
|             listener, lldb.SBProcess.eBroadcastBitStructuredData)
 | |
|         self.assertTrue(rc, "Successfully add listener to process broadcaster")
 | |
| 
 | |
|         # Start the listening thread to retrieve the events.
 | |
|         # Bump up max entry count for the potentially included Add Mode:
 | |
|         # entry.
 | |
|         if max_entry_count is not None:
 | |
|             max_entry_count += 1
 | |
|         event_thread = self.EventListenerThread(listener, process, trace_on,
 | |
|                                                 max_entry_count)
 | |
|         event_thread.start()
 | |
| 
 | |
|         # Continue the test inferior.  We should get any events after this.
 | |
|         process.Continue()
 | |
| 
 | |
|         # Wait until the event thread terminates.
 | |
|         # print("main thread now waiting for event thread to receive events.")
 | |
|         event_thread.join()
 | |
| 
 | |
|         # If the process is still alive, we kill it here.
 | |
|         if process.is_alive:
 | |
|             process.Kill()
 | |
| 
 | |
|         # Fail on any exceptions that occurred during event execution.
 | |
|         if event_thread.exception is not None:
 | |
|             # Re-raise it here so it shows up as a test error.
 | |
|             raise event_thread
 | |
| 
 | |
|         # Return the collected logging events.
 | |
|         return remove_add_mode_entry(event_thread.log_entries)
 |