123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- #!/usr/bin/env 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 sys
- import tempfile
- import threading
- import time
- # Path to the version of ninja checked in into Chrome.
- rel_path_to_ninja = os.path.join('third_party', 'depot_tools', 'ninja')
- 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)
- # On Windows the caret character needs to be escaped as it's an escape
- # character.
- carets = '^'
- if sys.platform.startswith('win'):
- carets = '^^'
- command = [
- os.path.join(project_folder, rel_path_to_ninja), "-C",
- os.path.join(project_folder, 'out', target_build),
- source_relative_path + carets]
- 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)
|