Upload upstream chromium 67.0.3396
[platform/framework/web/chromium-efl.git] / tools / sublime / compile_current_file.py
1 #!/usr/bin/python
2 # Copyright 2016 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 import datetime
7 import fnmatch
8 import logging
9 import os
10 import os.path
11 import queue as Queue
12 import sublime
13 import sublime_plugin
14 import subprocess
15 import sys
16 import tempfile
17 import threading
18 import time
19
20 # Path to the version of ninja checked in into Chrome.
21 rel_path_to_ninja = os.path.join('third_party', 'depot_tools', 'ninja')
22
23
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)
30
31
32 class CompileCurrentFile(sublime_plugin.TextCommand):
33   # static thread so that we don't try to run more than once at a time.
34   thread = None
35   lock = threading.Lock()
36
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
42
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 "
47             "will interrupt it.")
48
49   def draw_panel_text(self):
50     """Draw in the output.exec panel the text accumulated in self.text_to_draw.
51
52     This must be called from the main UI thread (e.g., using set_timeout).
53     """
54     assert self.thread_id == threading.current_thread().ident
55     logging.debug("draw_panel_text called.")
56     self.lock.acquire()
57     text_to_draw = self.text_to_draw
58     self.text_to_draw = ""
59     self.lock.release()
60
61     if len(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)
65
66   def update_panel_text(self, text_to_draw):
67     self.lock.acquire()
68     self.text_to_draw += text_to_draw
69     self.lock.release()
70     sublime.set_timeout(self.draw_panel_text, 0)
71
72   def execute_command(self, command, cwd):
73     """Execute the provided command and send ouput to panel.
74
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.
77
78     Args:
79       command: A list containing the command to execute and it's arguments.
80     Returns:
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
85     """
86     logging.debug("Running command: %s", command)
87
88     def EnqueueOutput(out, queue):
89       """Read all the output from the given handle and insert it into the queue.
90
91       Args:
92         queue: The Queue object to write to.
93       """
94       while True:
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
98         # early.
99         # The potential dealock here is acceptable because this isn't run on the
100         # main thread.
101         data = out.readline()
102         if not data:
103           break
104         queue.put(data, block=True)
105       out.close()
106
107     try:
108       os.chdir(cwd)
109       proc = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True,
110                               stderr=subprocess.STDOUT, stdin=subprocess.PIPE)
111     except OSError as e:
112       logging.exception('Execution of %s raised exception: %s.', (command, e))
113       return -1
114
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()
121
122     # We use the self.interrupted flag to stop this thread.
123     while not self.interrupted:
124       try:
125         exit_code = proc.poll()
126       except OSError as e:
127         logging.exception('Polling execution of %s raised exception: %s.',
128                           command, e)
129         return -2
130
131       # Try to read output content from the queue
132       current_content = ""
133       for _ in range(2048):
134         try:
135           current_content += stdout_queue.get_nowait().decode('utf-8')
136         except Queue.Empty:
137           break
138       self.update_panel_text(current_content)
139       current_content = ""
140       if exit_code is not None:
141         while stdout_thread.isAlive() or not stdout_queue.empty():
142           try:
143             current_content += stdout_queue.get(
144                                block=True, timeout=1).decode('utf-8')
145           except Queue.Empty:
146             # Queue could still potentially contain more input later.
147             pass
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))
151         return exit_code
152       # We sleep a little to give the child process a chance to move forward
153       # before we poll it again.
154       time.sleep(0.1)
155
156     # If we get here, it's because we were interrupted, kill the process.
157     proc.terminate()
158     return 1
159
160   def run(self, edit, target_build):
161     """The method called by Sublime Text to execute our command.
162
163     Note that this command is a toggle, so if the thread is are already running,
164     calling run will interrupt it.
165
166     Args:
167       edit: Sumblime Text specific edit brace.
168       target_build: Release/Debug/Other... Used for the subfolder of out.
169     """
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)
175       self.thread = None
176       return
177
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))
194     else:
195       output_dir = os.path.join(project_folder, 'out', target_build)
196       source_relative_path = os.path.relpath(self.view.file_name(),
197                                              output_dir)
198       # On Windows the caret character needs to be escaped as it's an escape
199       # character.
200       carets = '^'
201       if sys.platform.startswith('win'):
202         carets = '^^'
203       command = [
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,
211                                              "cwd": output_dir})
212       self.thread.start()
213
214     time_length = datetime.datetime.now() - self.start_time
215     logging.debug("Took %s seconds on UI thread to startup",
216                   time_length.seconds)
217     self.view.window().focus_view(self.view)