18F/federalist-garden-build

View on GitHub
src/runner/__init__.py

Summary

Maintainability
A
0 mins
Test Coverage
A
95%
import grp
import os
import pwd
import shlex
import subprocess  # nosec

NVM_PATH = '~/.nvm/nvm.sh'
RVM_PATH = '/usr/local/rvm/scripts/rvm'


def setuser():
    os.setgid(grp.getgrnam('rvm').gr_gid)
    os.setuid(pwd.getpwnam('customer').pw_uid)


def run(logger, command, cwd=None, env=None, shell=False, check=False, node=False, ruby=False):
    '''
    Run an OS command with provided cwd or env, stream logs to logger, and return the exit code.

    Errors that occur BEFORE the command is actually executed are caught and handled here.

    Errors encountered by the executed command are NOT caught. Instead a non-zero exit code
    will be returned to be handled by the caller.

    See https://docs.python.org/3/library/subprocess.html#popen-constructor for details.
    '''

    # TODO - refactor to put the appropriate bundler binaries in PATH so this isn't necessary
    if ruby:
        command = f'source {RVM_PATH} && {command}'
        shell = True

    # TODO - refactor to put the appropriate node/npm binaries in PATH so this isn't necessary
    if node:
        command = f'source {NVM_PATH} && nvm use default && {command}'
        shell = True

    if isinstance(command, str) and not shell:
        command = shlex.split(command)

    # When a shell is needed, use `bash` instead of `sh`
    executable = '/bin/bash' if shell else None

    try:
        p = subprocess.Popen(  # nosec
            command,
            cwd=cwd,
            env=env,
            shell=shell,
            executable=executable,
            stderr=subprocess.STDOUT,
            stdout=subprocess.PIPE,
            bufsize=1,
            encoding='utf-8',
            text=True,
            preexec_fn=setuser
        )
        while p.poll() is None:
            logger.info(p.stdout.readline().strip())

        logger.info(p.stdout.readline().strip())

        if check and p.returncode:
            raise subprocess.CalledProcessError(p.returncode, command)

        return p.returncode

    # This occurs when Popen itself is called with invalid arguments
    except ValueError as err:
        logger.error('Encountered a problem invoking Popen.')
        logger.error(str(err))

        if check:
            raise err

        return 1

    # This occurs when the command given to Popen cannot be executed.
    # Ex. the file doesn't exist, there was a typo, etc...
    except OSError as err:
        logger.error('Encountered a problem executing `' + ' '.join(command) + '`.')
        logger.error(str(err))

        if check:
            raise err

        return 1