perfkitbenchmarker/scripts/execute_command.py (109 lines of code) (raw):
# Copyright 2015 PerfKitBenchmarker Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -*- coding: utf-8 -*-
"""Runs a command, saving stdout, stderr, and the return code in files.
Simplifies executing long-running commands on a remote host.
The status file (as specified by --status) is exclusively locked until the
child process running the user-specified command exits.
This command will fail if the status file cannot be successfully locked.
To await completion, "wait_for_command.py" acquires a shared lock on the
status file, which blocks until the process completes.
*Runs on the guest VM. Supports Python 3.x.*
"""
import fcntl
import logging
import optparse
import subprocess
import sys
import threading
# By default, set a timeout of 100 years to mimic no timeout.
_DEFAULT_NEAR_ETERNAL_TIMEOUT = 60 * 60 * 24 * 365 * 100
def main():
parser = optparse.OptionParser()
parser.add_option(
'-o',
'--stdout',
dest='stdout',
metavar='FILE',
help="""Write stdout to FILE. Required.""",
)
parser.add_option(
'-e',
'--stderr',
dest='stderr',
metavar='FILE',
help="""Write stderr to FILE. Required.""",
)
parser.add_option(
'-p', '--pid', dest='pid', help="""Write PID to FILE.""", metavar='FILE'
)
parser.add_option(
'-s',
'--status',
dest='status',
help="""Write process exit
status to FILE. An exclusive lock will be placed on FILE
until this process exits. Required.""",
metavar='FILE',
)
parser.add_option(
'-c',
'--command',
dest='command',
help="""Shell command to
execute. Required.""",
)
parser.add_option(
'-x',
'--exclusive',
dest='exclusive',
help=(
'Make FILE exist to indicate the exclusive lock on status has been '
'placed. Required.'
),
metavar='FILE',
)
parser.add_option(
'-t',
'--timeout',
dest='timeout',
default=_DEFAULT_NEAR_ETERNAL_TIMEOUT,
type='int',
help="""Timeout in seconds before killing
the command.""",
)
options, args = parser.parse_args()
if args:
sys.stderr.write('Unexpected arguments: {0}\n'.format(args))
return 1
missing = []
for option in ('stdout', 'stderr', 'status', 'command', 'exclusive'):
if getattr(options, option) is None:
missing.append(option)
if missing:
parser.print_usage()
msg = 'Missing required flag(s): {0}\n'.format(
', '.join('--' + i for i in missing)
)
sys.stderr.write(msg)
return 1
with open(options.status, 'w+') as status:
with open(options.stdout, 'w') as stdout:
with open(options.stderr, 'w') as stderr:
logging.info('Acquiring lock on %s', options.status)
# Non-blocking exclusive lock acquisition; will raise an IOError if
# acquisition fails, which is desirable here.
fcntl.lockf(status, fcntl.LOCK_EX | fcntl.LOCK_NB)
p = subprocess.Popen(
options.command, stdout=stdout, stderr=stderr, shell=True
)
logging.info('Started pid %d: %s', p.pid, options.command)
# Create empty file to inform consumers of status that we've taken an
# exclusive lock on it.
with open(options.exclusive, 'w'):
pass
if options.pid:
with open(options.pid, 'w') as pid:
pid.write(str(p.pid))
def _KillProcess():
logging.error(
'ExecuteCommand timed out after %d seconds. Killing command.',
options.timeout,
)
p.kill()
timer = threading.Timer(options.timeout, _KillProcess)
timer.start()
logging.info('Waiting on PID %s', p.pid)
try:
return_code = p.wait()
finally:
timer.cancel()
logging.info('Return code: %s', return_code)
status.truncate()
status.write(str(return_code))
# File lock will be released when the status file is closed.
return return_code
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
sys.exit(main())