helpers.py revision 11828:36b064696175
1#!/usr/bin/env python2
2#
3# Copyright (c) 2016 ARM Limited
4# All rights reserved
5#
6# The license below extends only to copyright in the software and shall
7# not be construed as granting a license to any other intellectual
8# property including but not limited to intellectual property relating
9# to a hardware implementation of the functionality of the software
10# licensed hereunder.  You may use the software subject to the license
11# terms below provided that you ensure that this notice is replicated
12# unmodified and in its entirety in all distributions of the software,
13# modified or unmodified, in source code or in binary form.
14#
15# Redistribution and use in source and binary forms, with or without
16# modification, are permitted provided that the following conditions are
17# met: redistributions of source code must retain the above copyright
18# notice, this list of conditions and the following disclaimer;
19# redistributions in binary form must reproduce the above copyright
20# notice, this list of conditions and the following disclaimer in the
21# documentation and/or other materials provided with the distribution;
22# neither the name of the copyright holders nor the names of its
23# contributors may be used to endorse or promote products derived from
24# this software without specific prior written permission.
25#
26# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
27# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
28# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
29# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
30# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
31# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
32# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
33# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
34# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
35# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
36# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37#
38# Authors: Andreas Sandberg
39
40import subprocess
41from threading import Timer
42import time
43import re
44
45class CallTimeoutException(Exception):
46    """Exception that indicates that a process call timed out"""
47
48    def __init__(self, status, stdout, stderr):
49        self.status = status
50        self.stdout = stdout
51        self.stderr = stderr
52
53class ProcessHelper(subprocess.Popen):
54    """Helper class to run child processes.
55
56    This class wraps a subprocess.Popen class and adds support for
57    using it in a with block. When the process goes out of scope, it's
58    automatically terminated.
59
60    with ProcessHelper(["/bin/ls"], stdout=subprocess.PIPE) as p:
61        return p.call()
62    """
63    def __init__(self, *args, **kwargs):
64        super(ProcessHelper, self).__init__(*args, **kwargs)
65
66    def _terminate_nicely(self, timeout=5):
67        def on_timeout():
68            self.kill()
69
70        if self.returncode is not None:
71            return self.returncode
72
73        timer = Timer(timeout, on_timeout)
74        self.terminate()
75        status = self.wait()
76        timer.cancel()
77
78        return status
79
80    def __enter__(self):
81        return self
82
83    def __exit__(self, exc_type, exc_value, traceback):
84        if self.returncode is None:
85            self._terminate_nicely()
86
87    def call(self, timeout=0):
88        self._timeout = False
89        def on_timeout():
90            self._timeout = True
91            self._terminate_nicely()
92
93        status, stdout, stderr = None, None, None
94        timer = Timer(timeout, on_timeout)
95        if timeout:
96            timer.start()
97
98        stdout, stderr = self.communicate()
99        status = self.wait()
100
101        timer.cancel()
102
103        if self._timeout:
104            self._terminate_nicely()
105            raise CallTimeoutException(self.returncode, stdout, stderr)
106        else:
107            return status, stdout, stderr
108
109class FileIgnoreList(object):
110    """Helper class to implement file ignore lists.
111
112    This class implements ignore lists using plain string matching and
113    regular expressions. In the simplest use case, rules are created
114    statically upon initialization:
115
116        ignore_list = FileIgnoreList(name=("ignore_me.txt", ), rex=(r".*~", )
117
118    Ignores can be queried using in the same ways as normal Python
119    containers:
120
121        if file_name in ignore_list:
122            print "Ignoring %s" % file_name
123
124
125    New rules can be added at runtime by extending the list in the
126    rules attribute:
127
128        ignore_list.rules.append(FileIgnoreList.simple("bar.txt"))
129    """
130
131    @staticmethod
132    def simple(r):
133        return lambda f: f == r
134
135    @staticmethod
136    def rex(r):
137        re_obj = r if hasattr(r, "search") else re.compile(r)
138        return lambda name: re_obj.search(name)
139
140    def __init__(self, names=(), rex=()):
141        self.rules = [ FileIgnoreList.simple(n) for n in names ] + \
142                     [ FileIgnoreList.rex(r) for r in rex ]
143
144    def __contains__(self, name):
145        for rule in self.rules:
146            if rule(name):
147                return True
148        return False
149
150if __name__ == "__main__":
151    # Run internal self tests to ensure that the helpers are working
152    # properly. The expected output when running this script is
153    # "SUCCESS!".
154
155    cmd_foo = [ "/bin/echo", "-n", "foo" ]
156    cmd_sleep = [ "/bin/sleep", "10" ]
157
158    # Test that things don't break if the process hasn't been started
159    with ProcessHelper(cmd_foo) as p:
160        pass
161
162    with ProcessHelper(cmd_foo, stdout=subprocess.PIPE) as p:
163        status, stdout, stderr = p.call()
164    assert stdout == "foo"
165    assert status == 0
166
167    try:
168        with ProcessHelper(cmd_sleep) as p:
169            status, stdout, stderr = p.call(timeout=1)
170        assert False, "Timeout not triggered"
171    except CallTimeoutException:
172        pass
173
174    ignore_list = FileIgnoreList(
175        names=("ignore.txt", "foo/test.txt"),
176        rex=(r"~$", re.compile("^#")))
177
178    assert "ignore.txt" in ignore_list
179    assert "bar.txt" not in ignore_list
180    assert "foo/test.txt" in ignore_list
181    assert "test.txt" not in ignore_list
182    assert "file1.c~" in ignore_list
183    assert "file1.c" not in ignore_list
184    assert "#foo" in ignore_list
185    assert "foo#" not in ignore_list
186
187    ignore_list.rules.append(FileIgnoreList.simple("bar.txt"))
188    assert "bar.txt" in ignore_list
189
190    print "SUCCESS!"
191