Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

# encoding: utf-8 

from __future__ import print_function, division, absolute_import 

 

import os 

import psutil 

import subprocess 

import sys 

 

from .utils import run_timed 

from .logger import logger as get_logger 

 

 

ENCODING = sys.getdefaultencoding() 

 

""" 

Concept: We start an (matlab|julia|...) interpreter in a sub process and communicate using pipes to 

stdin / from stdout. 

 

To execute given code we first wrap this specific code in a template. For example see 

`julia_runner.py` and `matlab_runner.py`. Then we write this code to STDIN of our server which 

starts to execute this as manually typed commands. The framing code writes special markers to 

STDOUT which the client reads from the implemented pipe. 

 

The markers start with "!!!" followed by a pair MESSAGE:[PAYLOAD]. We use the following markers: 

 

- "ERROR:START" and "ERROR:END" to indicate start and end of exception related output 

 

- "FINISHED:0" is the last output, so the client stops to request output from the server 

""" 

 

 

def used_memory(process): 

"""returns memory consumed by process, unit is MB. This method is not 100% exact and 

usually overestimated the "memory freed if we terminate the process" number. But to 

get the exact numbers with psutil we (might) need root priviledges.""" 

return psutil.Process(process.pid).memory_info()[0] / float(2 ** 20) 

 

 

class InterpreterBridge(object): 

 

NAME = "NOT SET" 

EXTRA_ARGS = [] 

TEMPLATE = "" 

NAME = "NOT SET" 

ENV = {} 

ENV = dict(LC_ALL="en_US.UTF-8", LANG="en_US.UTF-8") 

 

def __init__(self, executable, mem_limit=500, call_limit=1000, noop="0"): 

"""executable: path to executable as matlab or julia 

mem_limit: if the interpreter consumes more that mem_limit MB it will be restarted, 

to disable this use mem_limit=None. 

call_limit: if more than call_limits commands are sent to interpreter, the subprocess 

will be restarted. To disable this use call_limit=None. 

noop: a "no operation" command for health check of subprocess. 

""" 

self.args = [executable] + self.EXTRA_ARGS 

self.logger = get_logger() 

self.p = None 

self.mem_limit = mem_limit 

self.call_limit = call_limit 

self.noop = noop 

 

def start_interpreter(self, verbose=False): 

 

self.call_count = 0 

self.p = self._start_interpreter(verbose) 

self.wait_until_available(verbose=verbose) 

return self 

 

def _start_interpreter(self, verbose=False): 

 

try: 

env = os.environ.copy() 

env.update(self.ENV) 

return subprocess.Popen(self.args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, 

stderr=subprocess.STDOUT, env=env) 

except Exception as e: 

msg = "failed to start '{}'. reason: {}".format(" ".join(self.args), e) 

raise Exception(msg) from None 

 

def kill(self): 

self.p.communicate() 

self.p = None 

 

def wait_until_available(self, verbose=False): 

return self.run_command(self.noop, skip_limits_check=True, 

timeout_in_seconds=None, verbose=verbose) 

 

def is_alive(self, timeout_in_seconds=1): 

try: 

exit_code = run_timed(self.wait_until_available, timeout_in_seconds=timeout_in_seconds) 

except TimeoutError: 

return False 

return exit_code == 0 

 

def _check_and_restart_if_needed(self): 

call_limit_exceeded = (self.call_limit is not None and self.call_count > self.call_limit) 

if call_limit_exceeded: 

self.logger.info("call limit {} exceeded, will restart the process" 

.format(self.call_limit)) 

 

mem_used = used_memory(self.p) 

mem_limit_exceeded = (self.mem_limit is not None and mem_used > self.mem_limit) 

if mem_limit_exceeded: 

self.logger.info("memory limit of {} MB exceeded (actual consumption is {} MB), " 

"will restart the process".format(self.mem_limit, mem_used)) 

 

if call_limit_exceeded or mem_limit_exceeded: 

"""we first start a new one, so we can assume that old and new process will have 

different process ids. some tests rely on this""" 

p = self._start_interpreter() 

self.kill() 

self.p = p 

self.wait_until_available() 

self.call_count = 0 

 

def run_command(self, command, timeout_in_seconds=None, skip_limits_check=False, 

verbose=False): 

assert self.p is not None, "you have to start the interpreter first" 

 

if not skip_limits_check: 

self._check_and_restart_if_needed() 

 

try: 

exit_code = run_timed(self._run_command, (command,), {"verbose": verbose}, 

timeout_in_seconds=timeout_in_seconds) 

except TimeoutError as e: 

raise TimeoutError("command '{}' did not finish with {} seconds" 

.format(command, timeout_in_seconds) 

) from None 

self.call_count += 1 

return exit_code 

 

@property 

def pid(self): 

"""process id of running interpreter""" 

assert self.p is not None, "you have to start the interpreter first" 

return self.p.pid 

 

def _wrap_command(self, command): 

return self.TEMPLATE.format(command=command, MSG_MARKER=self.MSG_MARKER) 

 

def _run_command(self, command, verbose): 

 

code = self._wrap_command(command) 

if verbose: 

print(code) 

 

self.p.stdin.write(code.encode(ENCODING)) 

self.p.stdin.write(b"\n") 

self.p.stdin.flush() 

 

log_error = False 

 

exit_code = 1 

for line in iter(self.p.stdout.readline, b""): 

 

line = str(line, ENCODING).rstrip() 

 

# we might have multiple ">> " before the actual output: 

while line.startswith(">>"): 

line = line[2:] 

line = line.lstrip() # maybe one space or none 

 

if line.startswith(self.MSG_MARKER): 

message, __, payload = line[len(self.MSG_MARKER):].partition(":") 

 

if message == "ERROR": 

log_error = (payload == "START") 

continue 

if message == "EXITCODE": 

exit_code = int(payload) 

continue 

 

if message == "FINISHED": 

return exit_code 

 

if log_error: 

self.logger.error("{}: {}".format(self.NAME, line)) 

elif verbose: 

print(">>", line)