Coverage for src/robotide/run/process.py: 90%
78 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
1# Copyright 2008-2015 Nokia Networks
2# Copyright 2016- Robot Framework Foundation
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
16import os
17import subprocess
18import tempfile
19import time
20from ..publish import RideRunnerStarted, RideRunnerStopped
23class Process(object):
25 def __init__(self, command):
26 self._command = self._parse_command(command) 1gdebca
27 self._process = None 1gdebca
28 self._error = None 1gdebca
29 self._out_file = None 1gdebca
30 self._out_path = None 1gdebca
31 self._out_fd = None 1gdebca
32 self._fuse = False 1gdebca
33 self._pid =None 1gdebca
35 @property
36 def pid(self):
37 return self._pid 1ebca
39 @staticmethod
40 def _parse_command(command):
41 if isinstance(command, str): 41 ↛ 43line 41 didn't jump to line 43 because the condition on line 41 was always true1gdebca
42 return [val.replace('<SPACE>', ' ') for val in command.split()] 1gdebca
43 return command
45 def start(self):
46 self._out_fd, self._out_path = tempfile.mkstemp(prefix='rfproc_', suffix='.txt', text=True) 1debca
47 self._out_file = open(self._out_path, 'w+b') 1debca
48 if not self._command: 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true1debca
49 self._error = 'The command is missing from this run configuration.'
50 return
51 try: 1debca
52 self._process = subprocess.Popen(self._command, stdout=self._out_fd, stderr=subprocess.STDOUT) 1debca
53 self._pid = self._process.pid 1dbca
54 RideRunnerStarted(process=self._pid).publish() 1dbca
55 except OSError as err: 1e
56 self._error = str(err) 1e
58 def is_finished(self):
59 return self._error is not None or self._process.poll() is not None 1debca
61 def stop(self):
62 self._process.kill() 1a
63 self._close_outputs() 1a
64 RideRunnerStopped(process=self._pid).publish() 1a
66 def wait(self):
67 if self._process is not None: 1ebc
68 self._process.wait() 1bc
70 def get_output(self, wait_until_finished=False):
71 """Returns the output produced by the process.
73 If ``wait_until_finished`` is True, blocks until the process is
74 finished and returns all output. Otherwise, the currently available
75 output is returned immediately.
77 Currently available output depends on buffering and might not include
78 everything that has been written by the process.
79 """
80 if self._error: 1debca
81 self._close_outputs() 1e
82 return self._error 1e
83 if wait_until_finished: 1dbca
84 self._process.wait() 1d
85 try: 1dbca
86 output = self._out_file.read() 1dbca
87 except ValueError: 1a
88 output = b"\nRIDE: ValueError when reading output.\n\n" 1a
89 self._fuse = True 1a
90 self._close_outputs() 1a
91 return output 1a
92 if self.is_finished(): 92 ↛ 94line 92 didn't jump to line 94 because the condition on line 92 was always true1dbc
93 self._close_outputs() 1dbc
94 return output 1dbc
96 def _close_outputs(self):
97 self._out_file.close() 1debca
98 try: 1debca
99 if not self._fuse: 1debca
100 os.close(self._out_fd) 1debca
101 self._remove_tempfile() 1debca
102 except ValueError:
103 self._fuse = True
104 return
106 def _remove_tempfile(self, attempts=5):
107 try: 1debca
108 os.remove(self._out_path) 1debca
109 except OSError: 1a
110 if not attempts: 1a
111 self._fuse = True 1a
112 return 1a
113 time.sleep(1) 1a
114 self._remove_tempfile(attempts - 1) 1a