Sublime Forum

No build output half the time

#1

When I use CTRL+b to build, about 50% it works as expected, and 50% of the time the output window opens but nothing is printed. The status bar does show “Building” for a few seconds then clears out. I see in the console this exception:

Running make -C /home/odoth/cpp_proj/lua53_64 run
Exception in thread Thread-195:
Traceback (most recent call last):
  File "./python3.3/threading.py", line 901, in _bootstrap_inner
  File "./python3.3/threading.py", line 858, in run
  File "/opt/sublime_text/Packages/Default.sublime-package/exec.py", line 152, in read_fileno
    decoder_cls = codecs.getincrementaldecoder(self.listener.encoding)
AttributeError: 'NoneType' object has no attribute 'encoding'

Environment details:
Sublime build 3176 (registered)
CentOS Linux release 7.5.1804

Project file:

{
    "folders":
    [
        {
            "path": ".",
            "folder_exclude_patterns": 
            [
                "obj",
                "bin"
            ]
        }
    ],
    "build_systems":
    [
        {
            "name": "make",
            "shell_cmd": "make -C $project_path",
            "file_regex": "^(..[^:]*):([0-9]+):?([0-9]+)?:? (.*)$",
            "variants":
            [
                {
                    "name": "make run",
                    "shell_cmd": "make -C $project_path run"
                },
                {
                    "name": "make clean",
                    "shell_cmd": "make -C $project_path clean"
                }
            ]
        }
    ]
}
0 Likes

#2

it looks like self.listener is only set to None when you cancel/kill a running build, so I guess that is where the problem lies

0 Likes

#3

Where do you see that?
I’m definitely not cancelling or killing anything. I get the same issue running with CTRL+B or starting build from the menu.

0 Likes

#4

Also interestingly enough I never saw this issue before 3.1 release.

0 Likes

#5

Also tried this on a fresh Centos 6 server today with no packages installed and still see the same issue.

0 Likes

#6

I’ve also been seeing this for a long time, but kept ignoring it since it works the second time. Today I decided to actually open the Console to look at what happens and saw this error, then found this and https://github.com/SublimeTextIssues/Core/issues/2436 .

Since just triggering a build again makes it work it’s not a big problem, but it is annoying.

0 Likes

#7

Workaround: Always sleep 1 before running a command that prints any output.
Fix to exec.py:

--- exec_old.py	2018-10-11 19:11:54.000000000 +0200
+++ exec_new.py	2019-06-04 14:00:02.157801936 +0200
@@ -129,17 +129,19 @@
     def kill(self):
         if not self.killed:
             self.killed = True
-            if sys.platform == "win32":
-                # terminate would not kill process opened by the shell cmd.exe,
-                # it will only kill cmd.exe leaving the child running
-                startupinfo = subprocess.STARTUPINFO()
-                startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
-                subprocess.Popen(
-                    "taskkill /PID %d /T /F" % self.proc.pid,
-                    startupinfo=startupinfo)
-            else:
-                os.killpg(self.proc.pid, signal.SIGTERM)
-                self.proc.terminate()
+            # only hard kill the process if it is still around
+            if self.poll():
+                if sys.platform == "win32":
+                    # terminate would not kill process opened by the shell cmd.exe,
+                    # it will only kill cmd.exe leaving the child running
+                    startupinfo = subprocess.STARTUPINFO()
+                    startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+                    subprocess.Popen(
+                        "taskkill /PID %d /T /F" % self.proc.pid,
+                        startupinfo=startupinfo)
+                else:
+                    os.killpg(self.proc.pid, signal.SIGTERM)
+                    self.proc.terminate()
             self.listener = None
 
     def poll(self):
@@ -241,7 +243,12 @@
         self.encoding = encoding
         self.quiet = quiet
 
+        if self.proc:
+            if not self.quiet:
+                print("Killing previous process")
+            self.proc.kill()
         self.proc = None
+
         if not self.quiet:
             if shell_cmd:
                 print("Running " + shell_cmd)
@@ -283,9 +290,8 @@
 
         try:
             # Forward kwargs to AsyncProcess
-            self.proc = AsyncProcess(cmd, shell_cmd, merged_env, self, **kwargs)
-
             with self.text_queue_lock:
+                self.proc = AsyncProcess(cmd, shell_cmd, merged_env, self, **kwargs)
                 self.text_queue_proc = self.proc
 
         except Exception as e:
@@ -306,6 +312,11 @@
             if proc != self.text_queue_proc and proc:
                 # a second call to exec has been made before the first one
                 # finished, ignore it instead of intermingling the output.
+                #
+                # This shouldn't happen any more, since we kill a stale
+                # process before starting a new one. Keep this check
+                # in case something did go very wrong and we keep
+                # getting output from some old process that won't die.
                 proc.kill()
                 return
 
@@ -363,8 +374,8 @@
                 self.append_string(proc, "[Finished in %.1fs with exit code %d]\n" % (elapsed, exit_code))
                 self.append_string(proc, self.debug_text)
 
-        if proc != self.proc:
-            return
+        # Process finished cleanly, so stop tracking it.
+        self.proc = None
 
         errs = self.output_view.find_all_results()
         if len(errs) == 0:
@@ -464,3 +475,4 @@
         w = view.window()
         if w is not None:
             w.run_command('exec', {'update_phantoms_only': True})
+

The first issue was that self.proc = AsyncProcess outside the lock would sometimes cause the new process to receive output before self.text_queue_proc was set which means append_string would be called and the test there for if proc != self.text_queue_proc would be true and the newly started process would incorrectly be killed on the first line of output. This could be worked around by always adding a sleep 1 to any build command to make sure there is no output while the threads start up.

The second issue is that a stale process would be killed only if it produced output while a new process is active. A simple echo Test; sleep 5 script being run twice 2 seconds apart would show that the old process was actually only killed much later. I changed exec.py to kill a stale process before starting a new one, which should also take care of file access/overwriting problems of long running compiler commands.

Third issue (somewhere in the middle of the change timeline from old to new) was that a process which exits cleanly without final output will still trigger append_string which, if it was a stale process, would then result in append_string wanting to kill the stale process. The AsyncProcess.kill call would then throw an exception about os.killpg not being able to find the PID (since it already exited cleanly). So now a process is only killed if we think it is still active.

0 Likes