|  | #!/usr/bin/python | 
|  | # Copyright 2016 The Chromium Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | import datetime | 
|  | import fnmatch | 
|  | import logging | 
|  | import os | 
|  | import os.path | 
|  | import queue as Queue | 
|  | import sublime | 
|  | import sublime_plugin | 
|  | import subprocess | 
|  | import tempfile | 
|  | import threading | 
|  | import time | 
|  |  | 
|  | # Change to an absolute reference if ninja is not on your path | 
|  | path_to_ninja = 'ninja' | 
|  |  | 
|  | def find_ninja_file(ninja_root_path, relative_file_path_to_find): | 
|  | ''' | 
|  | Returns the first *.ninja file in ninja_root_path that contains | 
|  | relative_file_path_to_find. Otherwise, returns None. | 
|  | ''' | 
|  | matches = [] | 
|  | for root, dirnames, filenames in os.walk(ninja_root_path): | 
|  | for filename in fnmatch.filter(filenames, '*.ninja'): | 
|  | matches.append(os.path.join(root, filename)) | 
|  | logging.debug("Found %d Ninja targets", len(matches)) | 
|  |  | 
|  | for ninja_file in matches: | 
|  | for line in open(ninja_file): | 
|  | if relative_file_path_to_find in line: | 
|  | return ninja_file | 
|  | return None | 
|  |  | 
|  |  | 
|  | class PrintOutputCommand(sublime_plugin.TextCommand): | 
|  | def run(self, edit, **args): | 
|  | self.view.set_read_only(False) | 
|  | self.view.insert(edit, self.view.size(), args['text']) | 
|  | self.view.show(self.view.size()) | 
|  | self.view.set_read_only(True) | 
|  |  | 
|  |  | 
|  | class CompileCurrentFile(sublime_plugin.TextCommand): | 
|  | # static thread so that we don't try to run more than once at a time. | 
|  | thread = None | 
|  | lock = threading.Lock() | 
|  |  | 
|  | def __init__(self, args): | 
|  | super(CompileCurrentFile, self).__init__(args) | 
|  | self.thread_id = threading.current_thread().ident | 
|  | self.text_to_draw = "" | 
|  | self.interrupted = False | 
|  |  | 
|  | def description(self): | 
|  | return ("Compiles the file in the current view using Ninja, so all that " | 
|  | "this file and it's project depends on will be built first\n" | 
|  | "Note that this command is a toggle so invoking it while it runs " | 
|  | "will interrupt it.") | 
|  |  | 
|  | def draw_panel_text(self): | 
|  | """Draw in the output.exec panel the text accumulated in self.text_to_draw. | 
|  |  | 
|  | This must be called from the main UI thread (e.g., using set_timeout). | 
|  | """ | 
|  | assert self.thread_id == threading.current_thread().ident | 
|  | logging.debug("draw_panel_text called.") | 
|  | self.lock.acquire() | 
|  | text_to_draw = self.text_to_draw | 
|  | self.text_to_draw = "" | 
|  | self.lock.release() | 
|  |  | 
|  | if len(text_to_draw): | 
|  | self.output_panel.run_command('print_output', {'text': text_to_draw}) | 
|  | self.view.window().run_command("show_panel", {"panel": "output.exec"}) | 
|  | logging.debug("Added text:\n%s.", text_to_draw) | 
|  |  | 
|  | def update_panel_text(self, text_to_draw): | 
|  | self.lock.acquire() | 
|  | self.text_to_draw += text_to_draw | 
|  | self.lock.release() | 
|  | sublime.set_timeout(self.draw_panel_text, 0) | 
|  |  | 
|  | def execute_command(self, command, cwd): | 
|  | """Execute the provided command and send ouput to panel. | 
|  |  | 
|  | Because the implementation of subprocess can deadlock on windows, we use | 
|  | a Queue that we write to from another thread to avoid blocking on IO. | 
|  |  | 
|  | Args: | 
|  | command: A list containing the command to execute and it's arguments. | 
|  | Returns: | 
|  | The exit code of the process running the command or, | 
|  | 1 if we got interrupted. | 
|  | -1 if we couldn't start the process | 
|  | -2 if we couldn't poll the running process | 
|  | """ | 
|  | logging.debug("Running command: %s", command) | 
|  |  | 
|  | def EnqueueOutput(out, queue): | 
|  | """Read all the output from the given handle and insert it into the queue. | 
|  |  | 
|  | Args: | 
|  | queue: The Queue object to write to. | 
|  | """ | 
|  | while True: | 
|  | # This readline will block until there is either new input or the handle | 
|  | # is closed. Readline will only return None once the handle is close, so | 
|  | # even if the output is being produced slowly, this function won't exit | 
|  | # early. | 
|  | # The potential dealock here is acceptable because this isn't run on the | 
|  | # main thread. | 
|  | data = out.readline() | 
|  | if not data: | 
|  | break | 
|  | queue.put(data, block=True) | 
|  | out.close() | 
|  |  | 
|  | try: | 
|  | os.chdir(cwd) | 
|  | proc = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, | 
|  | stderr=subprocess.STDOUT, stdin=subprocess.PIPE) | 
|  | except OSError as e: | 
|  | logging.exception('Execution of %s raised exception: %s.', (command, e)) | 
|  | return -1 | 
|  |  | 
|  | # Use a Queue to pass the text from the reading thread to this one. | 
|  | stdout_queue = Queue.Queue() | 
|  | stdout_thread = threading.Thread(target=EnqueueOutput, | 
|  | args=(proc.stdout, stdout_queue)) | 
|  | stdout_thread.daemon = True  # Ensure this exits if the parent dies | 
|  | stdout_thread.start() | 
|  |  | 
|  | # We use the self.interrupted flag to stop this thread. | 
|  | while not self.interrupted: | 
|  | try: | 
|  | exit_code = proc.poll() | 
|  | except OSError as e: | 
|  | logging.exception('Polling execution of %s raised exception: %s.', | 
|  | command, e) | 
|  | return -2 | 
|  |  | 
|  | # Try to read output content from the queue | 
|  | current_content = "" | 
|  | for _ in range(2048): | 
|  | try: | 
|  | current_content += stdout_queue.get_nowait().decode('utf-8') | 
|  | except Queue.Empty: | 
|  | break | 
|  | self.update_panel_text(current_content) | 
|  | current_content = "" | 
|  | if exit_code is not None: | 
|  | while stdout_thread.isAlive() or not stdout_queue.empty(): | 
|  | try: | 
|  | current_content += stdout_queue.get( | 
|  | block=True, timeout=1).decode('utf-8') | 
|  | except Queue.Empty: | 
|  | # Queue could still potentially contain more input later. | 
|  | pass | 
|  | time_length = datetime.datetime.now() - self.start_time | 
|  | self.update_panel_text("%s\nDone!\n(%s seconds)" % | 
|  | (current_content, time_length.seconds)) | 
|  | return exit_code | 
|  | # We sleep a little to give the child process a chance to move forward | 
|  | # before we poll it again. | 
|  | time.sleep(0.1) | 
|  |  | 
|  | # If we get here, it's because we were interrupted, kill the process. | 
|  | proc.terminate() | 
|  | return 1 | 
|  |  | 
|  | def run(self, edit, target_build): | 
|  | """The method called by Sublime Text to execute our command. | 
|  |  | 
|  | Note that this command is a toggle, so if the thread is are already running, | 
|  | calling run will interrupt it. | 
|  |  | 
|  | Args: | 
|  | edit: Sumblime Text specific edit brace. | 
|  | target_build: Release/Debug/Other... Used for the subfolder of out. | 
|  | """ | 
|  | # There can only be one... If we are running, interrupt and return. | 
|  | if self.thread and self.thread.is_alive(): | 
|  | self.interrupted = True | 
|  | self.thread.join(5.0) | 
|  | self.update_panel_text("\n\nInterrupted current command:\n%s\n" % command) | 
|  | self.thread = None | 
|  | return | 
|  |  | 
|  | # It's nice to display how long it took to build. | 
|  | self.start_time = datetime.datetime.now() | 
|  | # Output our results in the same panel as a regular build. | 
|  | self.output_panel = self.view.window().get_output_panel("exec") | 
|  | self.output_panel.set_read_only(True) | 
|  | self.view.window().run_command("show_panel", {"panel": "output.exec"}) | 
|  | # TODO(mad): Not sure if the project folder is always the first one... ??? | 
|  | project_folder = self.view.window().folders()[0] | 
|  | self.update_panel_text("Compiling current file %s\n" % | 
|  | self.view.file_name()) | 
|  | # The file must be somewhere under the project folder... | 
|  | if (project_folder.lower() != | 
|  | self.view.file_name()[:len(project_folder)].lower()): | 
|  | self.update_panel_text( | 
|  | "ERROR: File %s is not in current project folder %s\n" % | 
|  | (self.view.file_name(), project_folder)) | 
|  | else: | 
|  | output_dir = os.path.join(project_folder, 'out', target_build) | 
|  | source_relative_path = os.path.relpath(self.view.file_name(), | 
|  | output_dir) | 
|  | command = [ | 
|  | path_to_ninja, "-C", os.path.join(project_folder, 'out', | 
|  | target_build), | 
|  | source_relative_path + '^'] | 
|  | self.update_panel_text(' '.join(command) + '\n') | 
|  | self.interrupted = False | 
|  | self.thread = threading.Thread(target=self.execute_command, | 
|  | kwargs={"command":command, | 
|  | "cwd": output_dir}) | 
|  | self.thread.start() | 
|  |  | 
|  | time_length = datetime.datetime.now() - self.start_time | 
|  | logging.debug("Took %s seconds on UI thread to startup", | 
|  | time_length.seconds) | 
|  | self.view.window().focus_view(self.view) |