1#!/usr/bin/env python
2
3# Copyright (c) 2016 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7import functools
8import inspect
9import os
10import sys
11import time
12import platform
13
14
15def GetCatapultDir():
16  return os.path.normpath(
17      os.path.join(os.path.dirname(__file__), '..', '..', '..'))
18
19
20def IsRunningOnCrosDevice():
21  """Returns True if we're on a ChromeOS device."""
22  lsb_release = '/etc/lsb-release'
23  if sys.platform.startswith('linux') and os.path.exists(lsb_release):
24    with open(lsb_release, 'r') as f:
25      res = f.read()
26      if res.count('CHROMEOS_RELEASE_NAME'):
27        return True
28  return False
29
30
31def GetHostOsName():
32  if IsRunningOnCrosDevice():
33    return 'chromeos'
34  elif sys.platform.startswith('linux'):
35    return 'linux'
36  elif sys.platform == 'darwin':
37    return 'mac'
38  elif sys.platform == 'win32':
39    return 'win'
40
41
42def GetHostArchName():
43  return platform.machine()
44
45
46def _ExecutableExtensions():
47  # pathext is, e.g. '.com;.exe;.bat;.cmd'
48  exts = os.getenv('PATHEXT').split(';') #e.g. ['.com','.exe','.bat','.cmd']
49  return [x[1:].upper() for x in exts] #e.g. ['COM','EXE','BAT','CMD']
50
51
52def IsExecutable(path):
53  if os.path.isfile(path):
54    if hasattr(os, 'name') and os.name == 'nt':
55      return path.split('.')[-1].upper() in _ExecutableExtensions()
56    else:
57      return os.access(path, os.X_OK)
58  else:
59    return False
60
61
62def _AddDirToPythonPath(*path_parts):
63  path = os.path.abspath(os.path.join(*path_parts))
64  if os.path.isdir(path) and path not in sys.path:
65    # Some callsite that use telemetry assumes that sys.path[0] is the directory
66    # containing the script, so we add these extra paths to right after it.
67    sys.path.insert(1, path)
68
69_AddDirToPythonPath(os.path.join(GetCatapultDir(), 'devil'))
70_AddDirToPythonPath(os.path.join(GetCatapultDir(), 'dependency_manager'))
71_AddDirToPythonPath(os.path.join(GetCatapultDir(), 'third_party', 'mock'))
72# mox3 is needed for pyfakefs usage, but not for pylint.
73_AddDirToPythonPath(os.path.join(GetCatapultDir(), 'third_party', 'mox3'))
74_AddDirToPythonPath(
75    os.path.join(GetCatapultDir(), 'third_party', 'pyfakefs'))
76
77from devil.utils import timeout_retry
78from devil.utils import reraiser_thread
79
80
81# Decorator that adds timeout functionality to a function.
82def Timeout(default_timeout):
83  return lambda func: TimeoutDeco(func, default_timeout)
84
85# Note: Even though the "timeout" keyword argument is the only
86# keyword argument that will need to be given to the decorated function,
87# we still have to use the **kwargs syntax, because we have to use
88# the *args syntax here before (since the decorator decorates functions
89# with different numbers of positional arguments) and Python doesn't allow
90# a single named keyword argument after *args.
91# (e.g., 'def foo(*args, bar=42):' is a syntax error)
92
93def TimeoutDeco(func, default_timeout):
94  @functools.wraps(func)
95  def RunWithTimeout(*args, **kwargs):
96    if 'timeout' in kwargs:
97      timeout = kwargs['timeout']
98    else:
99      timeout = default_timeout
100    try:
101      return timeout_retry.Run(func, timeout, 0, args=args)
102    except reraiser_thread.TimeoutError:
103      print '%s timed out.' % func.__name__
104      return False
105  return RunWithTimeout
106
107
108MIN_POLL_INTERVAL_IN_SECONDS = 0.1
109MAX_POLL_INTERVAL_IN_SECONDS = 5
110OUTPUT_INTERVAL_IN_SECONDS = 300
111
112def WaitFor(condition, timeout):
113  """Waits for up to |timeout| secs for the function |condition| to return True.
114
115  Polling frequency is (elapsed_time / 10), with a min of .1s and max of 5s.
116
117  Returns:
118    Result of |condition| function (if present).
119  """
120  def GetConditionString():
121    if condition.__name__ == '<lambda>':
122      try:
123        return inspect.getsource(condition).strip()
124      except IOError:
125        pass
126    return condition.__name__
127
128  # Do an initial check to see if its true.
129  res = condition()
130  if res:
131    return res
132  start_time = time.time()
133  last_output_time = start_time
134  elapsed_time = time.time() - start_time
135  while elapsed_time < timeout:
136    res = condition()
137    if res:
138      return res
139    now = time.time()
140    elapsed_time = now - start_time
141    last_output_elapsed_time = now - last_output_time
142    if last_output_elapsed_time > OUTPUT_INTERVAL_IN_SECONDS:
143      last_output_time = time.time()
144    poll_interval = min(max(elapsed_time / 10., MIN_POLL_INTERVAL_IN_SECONDS),
145                        MAX_POLL_INTERVAL_IN_SECONDS)
146    time.sleep(poll_interval)
147  raise TimeoutException('Timed out while waiting %ds for %s.' %
148                         (timeout, GetConditionString()))
149
150class TimeoutException(Exception):
151  """The operation failed to complete because of a timeout.
152
153  It is possible that waiting for a longer period of time would result in a
154  successful operation.
155  """
156  pass
157