Sublime Forum

Long-running subprocess.call() zombifies ST3--threading?

#1

Hey All,

I continue to work on my custom build comand (a WindowCommand). Unfortunately, I’m finding that when I run my external process via subprocess.call(), the ST3 app stops updating the window–it’s pretty much completely nonresponive until the external app finishes. That seems to be true of all ST3 instances–that is, if I have 3 projects open w/3 different ST3 windows, building one of them zombifies all 3 windows, which is a major bummer.

So I guess this means I need to run my external process in a newly-spawned thread–is that right? The behavior I’m looking for is:

- user edits script
  • user builds–my WindowCommand runs & shells out to the external program

  • all instances of ST3 stay responsive while external program runs

  • external program ends–control returns to my WindowCommand, which trolls the external-process-created log file for errors/warnings, pops dialogs & opens various output files for inspection.

Unfortunately I’m a python noob (and have only unsuccessful attempts at multi-threading in other langs). So I don’t want to go down this road if it can’t work b/c of some quirk of the ST3 runtime environment, or won’t give me the behavior I need. Can any kind soul out there tell me:

  • am I right that executing my external program on a python thread will give me the behavior I’m after?

  • can said threading work in the ST3 environment?

  • in figuring out how to do threading, am I okay just reading the raw python docs (e.g., docs.python.org/3/library/threading.html), or is there an ST3 API that I need to use?

Many thanks!

-Roy

0 Likes

#2

You can use sublime.set_async_timeout to run something in another thread – you’re free to pass a delay of 0.

0 Likes

#3

Thanks so much for the reply–that method looks really promising! Unfortunately it’s not proving as dead simple as I was hoping. If I sub out my actual build process for method that just shells out to calc.exe

def run_calc(self): # sublime.message_dialog("boobies!") subprocess.call('calc.exe')
ST3 stays nice and responsive while the calculator is running.

But when I copy/paste my actual code into something I can call with set_timeout_async, it works just the same as before–zombie city.

Is there something obvious I’m doing wrong?

  def shell_out_to_sas(self, call_args, prg_filename, lst_filename, log_filename, err_regx, sas_path):
    subprocess.call(call_args)
    sublime.status_message("Finished running " + prg_filename)
    if os.path.exists(lst_filename):
      self.window.open_file(lst_filename)
    if os.path.exists(log_filename):
      res = "Finished!\n"
      for l in self.find_logs(log_filename):
        res += self.check_log(l, err_regx)
      sublime.message_dialog(res)
    else:
      sublime.message_dialog("Problem!  Did not find the expected log file (" + log_filename + ").")
    # print sas_path + " exists?: " + str(os.path.exists(sas_path))
    # sublime.message_dialog("Pretend I ran " + sas_path)
    # self.window.open_file(r'C:\Users\Roy\AppData\Roaming\Sublime Text 3\Packages\SAS\notes.txt')
    


  def run(self):
    self.window.active_view().run_command('save')
    prg_filename = self.window.active_view().file_name()
    extension = os.path.splitext(prg_filename)-1].lower()
    if extension == '.sas':
      log_filename = prg_filename:-3] + 'log'
      lst_filename = prg_filename:-3] + 'lst'
      lrn_filename = lst_filename + '.last.run'
      if os.path.exists(lrn_filename):
        os.remove(lrn_filename)
      s = sublime.load_settings('sas.sublime-settings')
      sas_path = s.get('sas-path', "C:\\Program Files\\SAS\\SASFoundation\\9.2\\sas.exe")
      sas_args = s.get('sas-args', '-nologo', '-noovp'])
      err_regx = s.get('err-regx', "(^(error|warning:)|uninitialized|^l]remerge|Invalid data for)(?! (the .{4,15} product with which|your system is scheduled|will be expiring soon, and|this upcoming expiration.|information on your warning period.))")
      s.set('sas-path', sas_path)
      s.set('sas-args', sas_args)
      s.set('err-regx', err_regx)
      sublime.save_settings('sas.sublime-settings')
      err_regx = re.compile(err_regx, re.MULTILINE + re.IGNORECASE)
      if os.path.exists(sas_path):
        call_args = [sas_path, '-sysin', prg_filename, '-log', log_filename, '-print', lst_filename] + sas_args
        # print subprocess.list2cmdline(call_args)
        # sublime.set_timeout_async(self.run_calc, 0)
        sublime.set_timeout_async(self.shell_out_to_sas(call_args, prg_filename, lst_filename, log_filename, err_regx, sas_path), 0)
      else:
        sublime.message_dialog("Problem--could not find sas.exe at " + sas_path + ".  Please update the sas-path setting in sas.sublime-settings in the User package folder.")
    else:
      sublime.message_dialog('Sorry--this only works with .sas files.')

Thanks again!

0 Likes

#4

In the following: sublime.set_timeout_async(self.shell_out_to_sas(call_args, prg_filename, lst_filename, log_filename, err_regx, sas_path), 0)
you need to pass a callback as the first argument. What you are doing is calling the function, and then using its return value (in this case None) as the parameter.

There are a variety of ways you could fix it. Perhaps the simplest is to stick a “lambda:” in front like this:

sublime.set_timeout_async(lambda: self.shell_out_to_sas(call_args, prg_filename, lst_filename, log_filename, err_regx, sas_path), 0)
1 Like

#5

Genius! Thank you so much! Adding lambda: gives me just the behavior I wanted.

0 Likes