2 # Copyright 2016 The Chromium Authors
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
20 # Path to the version of ninja checked in into Chrome.
21 rel_path_to_ninja = os.path.join('third_party', 'ninja', 'ninja')
24 class PrintOutputCommand(sublime_plugin.TextCommand):
25 def run(self, edit, **args):
26 self.view.set_read_only(False)
27 self.view.insert(edit, self.view.size(), args['text'])
28 self.view.show(self.view.size())
29 self.view.set_read_only(True)
32 class CompileCurrentFile(sublime_plugin.TextCommand):
33 # static thread so that we don't try to run more than once at a time.
35 lock = threading.Lock()
37 def __init__(self, args):
38 super(CompileCurrentFile, self).__init__(args)
39 self.thread_id = threading.current_thread().ident
40 self.text_to_draw = ""
41 self.interrupted = False
43 def description(self):
44 return ("Compiles the file in the current view using Ninja, so all that "
45 "this file and it's project depends on will be built first\n"
46 "Note that this command is a toggle so invoking it while it runs "
49 def draw_panel_text(self):
50 """Draw in the output.exec panel the text accumulated in self.text_to_draw.
52 This must be called from the main UI thread (e.g., using set_timeout).
54 assert self.thread_id == threading.current_thread().ident
55 logging.debug("draw_panel_text called.")
57 text_to_draw = self.text_to_draw
58 self.text_to_draw = ""
62 self.output_panel.run_command('print_output', {'text': text_to_draw})
63 self.view.window().run_command("show_panel", {"panel": "output.exec"})
64 logging.debug("Added text:\n%s.", text_to_draw)
66 def update_panel_text(self, text_to_draw):
68 self.text_to_draw += text_to_draw
70 sublime.set_timeout(self.draw_panel_text, 0)
72 def execute_command(self, command, cwd):
73 """Execute the provided command and send ouput to panel.
75 Because the implementation of subprocess can deadlock on windows, we use
76 a Queue that we write to from another thread to avoid blocking on IO.
79 command: A list containing the command to execute and it's arguments.
81 The exit code of the process running the command or,
82 1 if we got interrupted.
83 -1 if we couldn't start the process
84 -2 if we couldn't poll the running process
86 logging.debug("Running command: %s", command)
88 def EnqueueOutput(out, queue):
89 """Read all the output from the given handle and insert it into the queue.
92 queue: The Queue object to write to.
95 # This readline will block until there is either new input or the handle
96 # is closed. Readline will only return None once the handle is close, so
97 # even if the output is being produced slowly, this function won't exit
99 # The potential dealock here is acceptable because this isn't run on the
101 data = out.readline()
104 queue.put(data, block=True)
109 proc = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True,
110 stderr=subprocess.STDOUT, stdin=subprocess.PIPE)
112 logging.exception('Execution of %s raised exception: %s.', (command, e))
115 # Use a Queue to pass the text from the reading thread to this one.
116 stdout_queue = Queue.Queue()
117 stdout_thread = threading.Thread(target=EnqueueOutput,
118 args=(proc.stdout, stdout_queue))
119 stdout_thread.daemon = True # Ensure this exits if the parent dies
120 stdout_thread.start()
122 # We use the self.interrupted flag to stop this thread.
123 while not self.interrupted:
125 exit_code = proc.poll()
127 logging.exception('Polling execution of %s raised exception: %s.',
131 # Try to read output content from the queue
133 for _ in range(2048):
135 current_content += stdout_queue.get_nowait().decode('utf-8')
138 self.update_panel_text(current_content)
140 if exit_code is not None:
141 while stdout_thread.isAlive() or not stdout_queue.empty():
143 current_content += stdout_queue.get(
144 block=True, timeout=1).decode('utf-8')
146 # Queue could still potentially contain more input later.
148 time_length = datetime.datetime.now() - self.start_time
149 self.update_panel_text("%s\nDone!\n(%s seconds)" %
150 (current_content, time_length.seconds))
152 # We sleep a little to give the child process a chance to move forward
153 # before we poll it again.
156 # If we get here, it's because we were interrupted, kill the process.
160 def run(self, edit, target_build):
161 """The method called by Sublime Text to execute our command.
163 Note that this command is a toggle, so if the thread is are already running,
164 calling run will interrupt it.
167 edit: Sumblime Text specific edit brace.
168 target_build: Release/Debug/Other... Used for the subfolder of out.
170 # There can only be one... If we are running, interrupt and return.
171 if self.thread and self.thread.is_alive():
172 self.interrupted = True
173 self.thread.join(5.0)
174 self.update_panel_text("\n\nInterrupted current command:\n%s\n" % command)
178 # It's nice to display how long it took to build.
179 self.start_time = datetime.datetime.now()
180 # Output our results in the same panel as a regular build.
181 self.output_panel = self.view.window().get_output_panel("exec")
182 self.output_panel.set_read_only(True)
183 self.view.window().run_command("show_panel", {"panel": "output.exec"})
184 # TODO(mad): Not sure if the project folder is always the first one... ???
185 project_folder = self.view.window().folders()[0]
186 self.update_panel_text("Compiling current file %s\n" %
187 self.view.file_name())
188 # The file must be somewhere under the project folder...
189 if (project_folder.lower() !=
190 self.view.file_name()[:len(project_folder)].lower()):
191 self.update_panel_text(
192 "ERROR: File %s is not in current project folder %s\n" %
193 (self.view.file_name(), project_folder))
195 output_dir = os.path.join(project_folder, 'out', target_build)
196 source_relative_path = os.path.relpath(self.view.file_name(),
198 # On Windows the caret character needs to be escaped as it's an escape
201 if sys.platform.startswith('win'):
204 os.path.join(project_folder, rel_path_to_ninja), "-C",
205 os.path.join(project_folder, 'out', target_build),
206 source_relative_path + carets]
207 self.update_panel_text(' '.join(command) + '\n')
208 self.interrupted = False
209 self.thread = threading.Thread(target=self.execute_command,
210 kwargs={"command":command,
214 time_length = datetime.datetime.now() - self.start_time
215 logging.debug("Took %s seconds on UI thread to startup",
217 self.view.window().focus_view(self.view)