Sublime Forum

ExecCommand subclass to run multiple commands

#1

MultipleExecCommand


Command intended to execute multiple build_systems synchronously.

multiple_exec.py:

import re
import sys
import os
import time
import sublime
import functools

from Default.exec import AsyncProcess
from Default.exec import ExecCommand


class MultipleExecCommand(ExecCommand):

    def run(self, build_systems=[]):
        if build_systems:
            self.build_systems = build_systems
            self.cmd_index = -1
            self.common_working_dir = os.path.commonprefix(
                [bs["working_dir"] for bs in build_systems]
            )

        if self.build_systems:
            bs = self.build_systems.pop(0)
            self.cmd_index += 1
            self.rel_working_dir = os.path.relpath(
                bs["working_dir"], self.common_working_dir
            )
            self._run(**bs)

    def _run(
            self,
            cmd=None,
            shell_cmd=None,
            file_regex="",
            line_regex="",
            working_dir="",
            encoding="utf-8",
            env={},
            quiet=False,
            kill=False,
            update_phantoms_only=False,
            hide_phantoms_only=False,
            word_wrap=True,
            syntax="Packages/Text/Plain text.tmLanguage",
            # Catches "path" and "shell"
            **kwargs):

        self.file_regex = file_regex

        if update_phantoms_only:
            if self.show_errors_inline:
                self.update_phantoms()
            return
        if hide_phantoms_only:
            self.hide_phantoms()
            return

        # clear the text_queue
        self.text_queue_lock.acquire()
        try:
            self.text_queue.clear()
            self.text_queue_proc = None
        finally:
            self.text_queue_lock.release()

        if kill:
            if self.proc:
                self.proc.kill()
                self.proc = None
                self.append_string(None, "[Cancelled]")
            return

        if not hasattr(self, 'output_view'):
            print('creating output panel')
            # Try not to call get_output_panel until the regexes are assigned
            self.output_view = self.window.create_output_panel("exec")

        # Default the to the current files directory if no working directory was
        # given
        if working_dir == "" and self.window.active_view() and self.window.active_view().file_name():
            working_dir = os.path.dirname(self.window.active_view().file_name())

        self.output_view.settings().set("result_file_regex", file_regex)
        self.output_view.settings().set("result_line_regex", line_regex)
        self.output_view.settings().set(
            "result_base_dir", self.common_working_dir)
        self.output_view.settings().set("word_wrap", word_wrap)
        self.output_view.settings().set("line_numbers", False)
        self.output_view.settings().set("gutter", False)
        self.output_view.settings().set("scroll_past_end", False)
        self.output_view.assign_syntax(syntax)

        self.encoding = encoding
        self.quiet = quiet

        self.proc = None
        if not self.quiet:
            if shell_cmd:
                print("{} - Running {}".format(self.cmd_index, shell_cmd))
            elif cmd:
                print("{} - Running {}".format(self.cmd_index, " ".join(cmd)))
            sublime.status_message("Building")

        show_panel_on_build = sublime.load_settings(
            "Preferences.sublime-settings").get("show_panel_on_build", True)
        if show_panel_on_build:
            self.window.run_command("show_panel", {"panel": "output.exec"})

        self.hide_phantoms()
        self.show_errors_inline = sublime.load_settings(
            "Preferences.sublime-settings").get("show_errors_inline", True)

        merged_env = env.copy()
        if self.window.active_view():
            user_env = self.window.active_view().settings().get('build_env')
            if user_env:
                merged_env.update(user_env)

        # Change to the working dir, rather than spawning the process with it,
        # so that emitted working dir relative path names make sense
        if working_dir != "":
            os.chdir(working_dir)

        self.debug_text = ""
        if shell_cmd:
            self.debug_text += "[shell_cmd: " + shell_cmd + "]\n"
        else:
            self.debug_text += "[cmd: " + str(cmd) + "]\n"
        self.debug_text += "[dir: " + str(os.getcwd()) + "]\n"
        if "PATH" in merged_env:
            self.debug_text += "[path: " + str(merged_env["PATH"]) + "]"
        else:
            self.debug_text += "[path: " + str(os.environ["PATH"]) + "]"

        try:
            # Forward kwargs to AsyncProcess
            self.proc = AsyncProcess(cmd, shell_cmd, merged_env, self, **kwargs)

            self.text_queue_lock.acquire()
            try:
                self.text_queue_proc = self.proc
            finally:
                self.text_queue_lock.release()

        except Exception as e:
            self.append_string(None, str(e) + "\n")
            self.append_string(None, self.debug_text + "\n")
            if not self.quiet:
                self.append_string(None, "[Finished]")

    def on_data(self, proc, data):
        try:
            characters = data.decode(self.encoding)
        except:
            characters = "[Decode error - output not " + self.encoding + "]\n"
            proc = None

        # Normalize newlines, Sublime Text always uses a single \n separator
        # in memory.
        characters = characters.replace('\r\n', '\n').replace('\r', '\n')
        lines = characters.split("\n")

        while lines:
            line = lines.pop(0)
            m = re.match(self.file_regex, line)
            if m:
                line = "{}{}{}".format(
                    line[:m.start(1)],
                    os.path.join(self.rel_working_dir, m.group(1)),
                    line[m.end(1):]
                )
            final_line = "\n" if line == '' else line
            self.append_string(proc, final_line)

    def on_finished(self, proc):
        if not self.quiet:
            elapsed = time.time() - proc.start_time
            exit_code = proc.exit_code()
            if exit_code == 0 or exit_code is None:
                self.append_string(proc, "\n[Finished in %.1fs]\n" % elapsed)
            else:
                self.append_string(
                    proc, "\n[Finished in %.1fs with exit code %d]\n" % (elapsed, exit_code))
                self.append_string(proc, self.debug_text)

        self.append_string(proc, "-" * 80 + "\n")

        if proc != self.proc:
            return

        errs = self.output_view.find_all_results()

        if len(errs) == 0:
            sublime.status_message("Build finished")
        else:
            sublime.status_message("Build finished with %d errors" % len(errs))

        # -------- Consume another build_system --------
        self.run()

Usage example


Create the below 3 files and use the UnitTesting package.

foo.cpp:

#include <stdio.h>
void main() { foo1 }

foo2/foo2.cpp

#include <stdio.h>
void main() { foo2 }

test_multiple_exec.py:

import os
from multiple_exec import MultipleExecCommand
import sublime

code = "import time; print('pythontest_'*30);time.sleep(1)"

build_systems = [
    {
        "file_regex": "^(?:\\.{2}\\\\)*([a-z]?:?[^\\(\n]+)\\(([^\\)]+)\\): ([^\n]+)",
        "shell_cmd": "cl /c foo.cpp",
        "working_dir": os.path.join(os.path.dirname(__file__), "."),
        "quiet": False
    },
    {
        "file_regex": "^(?:\\.{2}\\\\)*([a-z]?:?[^\\(\n]+)\\(([^\\)]+)\\): ([^\n]+)",
        "shell_cmd": 'python -c "{}"'.format(code),
        "working_dir": os.path.join(os.path.dirname(__file__), "foo"),
        "quiet": False
    },
    {
        "file_regex": "^(?:\\.{2}\\\\)*([a-z]?:?[^\\(\n]+)\\(([^\\)]+)\\): ([^\n]+)",
        "shell_cmd": "cl /c foo2.cpp",
        "working_dir": os.path.join(os.path.dirname(__file__), "foo"),
        "quiet": False
    },
]
command = MultipleExecCommand(sublime.active_window())
command.run(build_systems)

Question


Problem with the above command is that the output is totally undeterministic… take a look to this video to see what I mean.

So, how can I make sure given a certain set of build_systems S = [b1,b2,…, bN] the output is always the same? For instance:

build_system1 line1
build_system1 line2
...
build_system1 lineN
[Finished in build_system1_time s]
--------------------------------------------------------------------------------
build_system2 line1
build_system2 line2
...
build_system2 lineN
[Finished in build_system2_time s]
--------------------------------------------------------------------------------
...
--------------------------------------------------------------------------------
build_systemN line1
build_systemN line2
...
build_system2 lineN
[Finished in build_systemN_time s]
--------------------------------------------------------------------------------

Thanks in advance.

0 Likes

#2

@wbond Any idea about making the output of the above command consistent&deterministic? I’m struggling quite a bit to understand why the output is the way it is.

For instance, take a look to a couple of dumped sessions:

TEST1
-----

Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

foo.cpp
.\foo.cpp(2): error C2065: 'foo1': undeclared identifier
.\foo.cpp(2): error C2143: syntax error: missing ';' before '}'

[Finished in 0.1s]
--------------------------------------------------------------------------------
pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_

[Finished in 1.1s]
--------------------------------------------------------------------------------
Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

foo2.cpp
foo\foo2.cpp(2): error C2065: 'foo2': undeclared identifier
foo\foo2.cpp(2): error C2143: syntax error: missing ';' before '}'

[Finished in 0.1s]
--------------------------------------------------------------------------------



TEST2
-----

Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

foo.cpp
.\foo.cpp(2): error C2065: 'foo1': undeclared identifier
.\foo.cpp(2): error C2143: syntax error: missing ';' before '}'
pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_pythontest_
Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

foo2.cpp
foo\foo2.cpp(2): error C2065: 'foo2': undeclared identifier
foo\foo2.cpp(2): error C2143: syntax error: missing ';' before '}'

[Finished in 0.1s]
--------------------------------------------------------------------------------

You can see how clearly both of them are different ones, in fact, I’d expect the [Finished in ...s] string to be appended to the output panel 3 times but you can see how in TEST2 has only appended once… I don’t get it at all :confused:

Thanks in advance.

0 Likes

#3

@wbond Ok, never mind about my above question, after getting a little bit familiar with python threading I think I’ve understood the reason of all my issues. So finally my choice will be just using a custom AsyncProcess instead of the existing one, as that one won’t guarantee stdout/stderr order correctness in the output panel… If I’m not mistaken, at the moment you decide to use (stdout=PIPE, stderr=PIPE) instead (stdout=PIPE, stderr=STDOUT) and then spawning a couple of parallel threads to consume the files you can’t guarantee the right order… Not sure why you decided to code it this way… I mean, I can imagine the reason being sacrificing output correctness by being little bit faster.

Btw, I’ve been pinging you because if I’m not mistaken you’re the ExecCommand author, right?

0 Likes

How to avoid ST UI freezing while running several threads in serial?
#4

Jon wrote ExecCommand, but I’ve done some work on it. I haven’t had time to go through all of your comments yet, but if you haven’t checked out https://www.sublimetext.com/docs/3/build_systems.html, it may help in understanding it all. Or perhaps you’ve already got a good handle on it, and the issue you are having is more subtle.

0 Likes

#5

@wbond Hiya! ah, ok ok, good to know, no worries at all, at the end I’ve ended up understanding and solving the whole thing by myself, well… actually that’s not true, @deathaxe gave me a really good advice on another related thread :wink:

I must to say it’s been a really hard experience… good news it’s i’ve learned quite a lot about really cool stuff: threading, file descriptors, subprocesses. Solving the hardest problems are tipically the ones more rewarding and this one definitely was one of them (at least to me) :wink:

One question still remains… it’s more a curiosity actually, the fact you guys decided to handle independently both stdout/stderr file descriptors intrigues me, why not just one thread where stderr=subprocess.STDOUT? any particular reason for this or …?

1 Like

#6

I noticed that when I was fixing the decoding bug recently. I am not sure why Jon wrote it that way. At some point I’ll make a point of asking.

1 Like