Skip to content

Commit

Permalink
Update Alfred-Workflow dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
fniephaus committed Jan 14, 2018
1 parent d3fac37 commit 31e47b8
Show file tree
Hide file tree
Showing 11 changed files with 1,012 additions and 578 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ clean:

update-lib:
/usr/bin/python -m pip install --target src --upgrade Alfred-Workflow
rm -rf src/Alfred_Workflow-*.dist-info/
rm -rf src/Alfred_Workflow-*.*info/
2 changes: 1 addition & 1 deletion src/version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v6.7
v7.0
2 changes: 1 addition & 1 deletion src/workflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
__version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read()
__author__ = 'Dean Jackson'
__licence__ = 'MIT'
__copyright__ = 'Copyright 2014 Dean Jackson'
__copyright__ = 'Copyright 2014-2017 Dean Jackson'

__all__ = [
'Variables',
Expand Down
161 changes: 106 additions & 55 deletions src/workflow/background.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@
# Created on 2014-04-06
#

"""Run background tasks."""
"""
This module provides an API to run commands in background processes.
Combine with the :ref:`caching API <caching-data>` to work from cached data
while you fetch fresh data in the background.
See :ref:`the User Manual <background-processes>` for more information
and examples.
"""

from __future__ import print_function, unicode_literals

import signal
import sys
import os
import subprocess
Expand All @@ -31,6 +39,10 @@ def wf():
return _wf


def _log():
return wf().logger


def _arg_cache(name):
"""Return path to pickle cache file for arguments.
Expand All @@ -40,7 +52,7 @@ def _arg_cache(name):
:rtype: ``unicode`` filepath
"""
return wf().cachefile('{0}.argcache'.format(name))
return wf().cachefile(name + '.argcache')


def _pid_file(name):
Expand All @@ -52,7 +64,7 @@ def _pid_file(name):
:rtype: ``unicode`` filepath
"""
return wf().cachefile('{0}.pid'.format(name))
return wf().cachefile(name + '.pid')


def _process_exists(pid):
Expand All @@ -71,35 +83,52 @@ def _process_exists(pid):
return True


def is_running(name):
"""Test whether task is running under ``name``.
def _job_pid(name):
"""Get PID of job or `None` if job does not exist.
:param name: name of task
:type name: ``unicode``
:returns: ``True`` if task with name ``name`` is running, else ``False``
:rtype: ``Boolean``
Args:
name (str): Name of job.
Returns:
int: PID of job process (or `None` if job doesn't exist).
"""
pidfile = _pid_file(name)
if not os.path.exists(pidfile):
return False
return

with open(pidfile, 'rb') as file_obj:
pid = int(file_obj.read().strip())
with open(pidfile, 'rb') as fp:
pid = int(fp.read())

if _process_exists(pid):
return True
if _process_exists(pid):
return pid

elif os.path.exists(pidfile):
try:
os.unlink(pidfile)
except Exception: # pragma: no cover
pass


def is_running(name):
"""Test whether task ``name`` is currently running.
:param name: name of task
:type name: unicode
:returns: ``True`` if task with name ``name`` is running, else ``False``
:rtype: bool
"""
if _job_pid(name) is not None:
return True

return False


def _background(stdin='/dev/null', stdout='/dev/null',
def _background(pidfile, stdin='/dev/null', stdout='/dev/null',
stderr='/dev/null'): # pragma: no cover
"""Fork the current process into a background daemon.
:param pidfile: file to write PID of daemon process to.
:type pidfile: filepath
:param stdin: where to read input
:type stdin: filepath
:param stdout: where to write stdout output
Expand All @@ -108,25 +137,31 @@ def _background(stdin='/dev/null', stdout='/dev/null',
:type stderr: filepath
"""
def _fork_and_exit_parent(errmsg):
def _fork_and_exit_parent(errmsg, wait=False, write=False):
try:
pid = os.fork()
if pid > 0:
if write: # write PID of child process to `pidfile`
tmp = pidfile + '.tmp'
with open(tmp, 'wb') as fp:
fp.write(str(pid))
os.rename(tmp, pidfile)
if wait: # wait for child process to exit
os.waitpid(pid, 0)
os._exit(0)
except OSError as err:
wf().logger.critical('%s: (%d) %s', errmsg, err.errno,
err.strerror)
_log().critical('%s: (%d) %s', errmsg, err.errno, err.strerror)
raise err

# Do first fork.
_fork_and_exit_parent('fork #1 failed')
# Do first fork and wait for second fork to finish.
_fork_and_exit_parent('fork #1 failed', wait=True)

# Decouple from parent environment.
os.chdir(wf().workflowdir)
os.setsid()

# Do second fork.
_fork_and_exit_parent('fork #2 failed')
# Do second fork and write PID to pidfile.
_fork_and_exit_parent('fork #2 failed', write=True)

# Now I am a daemon!
# Redirect standard file descriptors.
Expand All @@ -141,15 +176,35 @@ def _fork_and_exit_parent(errmsg):
os.dup2(se.fileno(), sys.stderr.fileno())


def kill(name, sig=signal.SIGTERM):
"""Send a signal to job ``name`` via :func:`os.kill`.
.. versionadded:: 1.29
Args:
name (str): Name of the job
sig (int, optional): Signal to send (default: SIGTERM)
Returns:
bool: `False` if job isn't running, `True` if signal was sent.
"""
pid = _job_pid(name)
if pid is None:
return False

os.kill(pid, sig)
return True


def run_in_background(name, args, **kwargs):
r"""Cache arguments then call this script again via :func:`subprocess.call`.
:param name: name of task
:type name: ``unicode``
:param name: name of job
:type name: unicode
:param args: arguments passed as first argument to :func:`subprocess.call`
:param \**kwargs: keyword arguments to :func:`subprocess.call`
:returns: exit code of sub-process
:rtype: ``int``
:rtype: int
When you call this function, it caches its arguments and then calls
``background.py`` in a subprocess. The Python subprocess will load the
Expand All @@ -167,24 +222,26 @@ def run_in_background(name, args, **kwargs):
"""
if is_running(name):
wf().logger.info('Task `{0}` is already running'.format(name))
_log().info('[%s] job already running', name)
return

argcache = _arg_cache(name)

# Cache arguments
with open(argcache, 'wb') as file_obj:
pickle.dump({'args': args, 'kwargs': kwargs}, file_obj)
wf().logger.debug('Command arguments cached to `{0}`'.format(argcache))
with open(argcache, 'wb') as fp:
pickle.dump({'args': args, 'kwargs': kwargs}, fp)
_log().debug('[%s] command cached: %s', name, argcache)

# Call this script
cmd = ['/usr/bin/python', __file__, name]
wf().logger.debug('Calling {0!r} ...'.format(cmd))
_log().debug('[%s] passing job to background runner: %r', name, cmd)
retcode = subprocess.call(cmd)

if retcode: # pragma: no cover
wf().logger.error('Failed to call task in background')
_log().error('[%s] background runner failed with %d', name, retcode)
else:
wf().logger.debug('Executing task `{0}` in background...'.format(name))
_log().debug('[%s] background job started', name)

return retcode


Expand All @@ -195,15 +252,21 @@ def main(wf): # pragma: no cover
:meth:`subprocess.call` with cached arguments.
"""
log = wf.logger
name = wf.args[0]
argcache = _arg_cache(name)
if not os.path.exists(argcache):
wf.logger.critical('No arg cache found : {0!r}'.format(argcache))
return 1
msg = '[{0}] command cache not found: {1}'.format(name, argcache)
log.critical(msg)
raise IOError(msg)

# Fork to background and run command
pidfile = _pid_file(name)
_background(pidfile)

# Load cached arguments
with open(argcache, 'rb') as file_obj:
data = pickle.load(file_obj)
with open(argcache, 'rb') as fp:
data = pickle.load(fp)

# Cached arguments
args = data['args']
Expand All @@ -212,30 +275,18 @@ def main(wf): # pragma: no cover
# Delete argument cache file
os.unlink(argcache)

pidfile = _pid_file(name)

# Fork to background
_background()

# Write PID to file
with open(pidfile, 'wb') as file_obj:
file_obj.write('{0}'.format(os.getpid()))

# Run the command
try:
wf.logger.debug('Task `{0}` running'.format(name))
wf.logger.debug('cmd : {0!r}'.format(args))
# Run the command
log.debug('[%s] running command: %r', name, args)

retcode = subprocess.call(args, **kwargs)

if retcode:
wf.logger.error('Command failed with [{0}] : {1!r}'.format(
retcode, args))

log.error('[%s] command failed with status %d', name, retcode)
finally:
if os.path.exists(pidfile):
os.unlink(pidfile)
wf.logger.debug('Task `{0}` finished'.format(name))
os.unlink(pidfile)

log.debug('[%s] job complete', name)


if __name__ == '__main__': # pragma: no cover
Expand Down
Loading

0 comments on commit 31e47b8

Please sign in to comment.