1
1
# Copyright lowRISC contributors (OpenTitan project).
2
2
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
3
3
# SPDX-License-Identifier: Apache-2.0
4
+ """Launcher implementation to run jobs as subprocesses on the local machine."""
4
5
5
6
import datetime
6
7
import os
7
8
import shlex
8
9
import subprocess
10
+ from pathlib import Path
11
+ from typing import Union
9
12
10
- from Launcher import ErrorMessage , Launcher , LauncherError
13
+ from Launcher import ErrorMessage , Launcher , LauncherBusy , LauncherError
11
14
12
15
13
16
class LocalLauncher (Launcher ):
@@ -18,7 +21,8 @@ def __init__(self, deploy):
18
21
super ().__init__ (deploy )
19
22
20
23
# Popen object when launching the job.
21
- self .process = None
24
+ self ._process = None
25
+ self ._log_file = None
22
26
23
27
def _do_launch (self ) -> None :
24
28
# Update the shell's env vars with self.exports. Values in exports must
@@ -37,34 +41,37 @@ def _do_launch(self) -> None:
37
41
self ._dump_env_vars (exports )
38
42
39
43
if not self .deploy .sim_cfg .interactive :
44
+ log_path = Path (self .deploy .get_log_path ())
45
+ timeout_mins = self .deploy .get_timeout_mins ()
46
+
47
+ self .timeout_secs = timeout_mins * 60 if timeout_mins else None
48
+
40
49
try :
41
- f = open (
42
- self .deploy .get_log_path (),
50
+ self ._log_file = log_path .open (
43
51
"w" ,
44
52
encoding = "UTF-8" ,
45
53
errors = "surrogateescape" ,
46
54
)
47
- f .write ("[Executing]:\n {}\n \n " .format (self .deploy .cmd ))
48
- f .flush ()
49
- timeout_mins = self .deploy .get_timeout_mins ()
50
- if timeout_mins :
51
- self .timeout_secs = timeout_mins * 60
52
- else :
53
- self .timeout_secs = None
54
- self .process = subprocess .Popen (
55
+ self ._log_file .write (f"[Executing]:\n { self .deploy .cmd } \n \n " )
56
+ self ._log_file .flush ()
57
+
58
+ self ._process = subprocess .Popen (
55
59
shlex .split (self .deploy .cmd ),
56
60
bufsize = 4096 ,
57
61
universal_newlines = True ,
58
- stdout = f ,
59
- stderr = f ,
62
+ stdout = self . _log_file ,
63
+ stderr = self . _log_file ,
60
64
env = exports ,
61
65
)
66
+
67
+ except BlockingIOError as e :
68
+ raise LauncherBusy (f"Failed to launch job: { e } " ) from e
69
+
62
70
except subprocess .SubprocessError as e :
63
- raise LauncherError (
64
- "IO Error: {}\n See {}" .format (e , self .deploy .get_log_path ())
65
- )
71
+ raise LauncherError (f"IO Error: { e } \n See { log_path } " ) from e
72
+
66
73
finally :
67
- self ._close_process ()
74
+ self ._close_job_log_file ()
68
75
else :
69
76
# Interactive: Set RUN_INTERACTIVE to 1
70
77
exports ["RUN_INTERACTIVE" ] = "1"
@@ -73,7 +80,7 @@ def _do_launch(self) -> None:
73
80
# no timeout and blocking op as user controls the flow
74
81
print ("Interactive mode is not supported yet." )
75
82
print (f"Cmd : { self .deploy .cmd } " )
76
- self .process = subprocess .Popen (
83
+ self ._process = subprocess .Popen (
77
84
shlex .split (self .deploy .cmd ),
78
85
stdin = None ,
79
86
stdout = None ,
@@ -84,12 +91,12 @@ def _do_launch(self) -> None:
84
91
)
85
92
86
93
# Wait until the process exit
87
- self .process .wait ()
94
+ self ._process .wait ()
88
95
89
96
self ._link_odir ("D" )
90
97
91
- def poll (self ):
92
- """Check status of the running process
98
+ def poll (self ) -> Union [ str , None ] :
99
+ """Check status of the running process.
93
100
94
101
This returns 'D', 'P', 'F', or 'K'. If 'D', the job is still running.
95
102
If 'P', the job finished successfully. If 'F', the job finished with
@@ -98,20 +105,20 @@ def poll(self):
98
105
This function must only be called after running self.dispatch_cmd() and
99
106
must not be called again once it has returned 'P' or 'F'.
100
107
"""
108
+ if self ._process is None :
109
+ return "E"
101
110
102
- assert self .process is not None
103
111
elapsed_time = datetime .datetime .now () - self .start_time
104
112
self .job_runtime_secs = elapsed_time .total_seconds ()
105
- if self .process .poll () is None :
113
+ if self ._process .poll () is None :
106
114
if (
107
- self .timeout_secs and
108
- (self .job_runtime_secs > self .timeout_secs ) and not
109
- (self .deploy .gui )
115
+ self .timeout_secs
116
+ and (self .job_runtime_secs > self .timeout_secs ) # noqa: W503
117
+ and not (self .deploy .gui ) # noqa: W503
110
118
):
111
119
self ._kill ()
112
- timeout_message = (
113
- f"Job timed out after { self .deploy .get_timeout_mins ()} minutes"
114
- )
120
+ timeout_mins = self .deploy .get_timeout_mins ()
121
+ timeout_message = f"Job timed out after { timeout_mins } minutes"
115
122
self ._post_finish (
116
123
"K" ,
117
124
ErrorMessage (
@@ -124,44 +131,46 @@ def poll(self):
124
131
125
132
return "D"
126
133
127
- self .exit_code = self .process .returncode
134
+ self .exit_code = self ._process .returncode
128
135
status , err_msg = self ._check_status ()
129
136
self ._post_finish (status , err_msg )
137
+
130
138
return self .status
131
139
132
- def _kill (self ):
140
+ def _kill (self ) -> None :
133
141
"""Kill the running process.
134
142
135
143
Try to kill the running process. Send SIGTERM first, wait a bit,
136
144
and then send SIGKILL if it didn't work.
137
145
"""
138
- assert self .process is not None
139
- self .process .terminate ()
146
+ if self ._process is None :
147
+ # process already dead or didn't start
148
+ return
149
+
150
+ self ._process .terminate ()
140
151
try :
141
- self .process .wait (timeout = 2 )
152
+ self ._process .wait (timeout = 2 )
142
153
except subprocess .TimeoutExpired :
143
- self .process .kill ()
154
+ self ._process .kill ()
144
155
145
- def kill (self ):
156
+ def kill (self ) -> None :
146
157
"""Kill the running process.
147
158
148
159
This must be called between dispatching and reaping the process (the
149
160
same window as poll()).
150
-
151
161
"""
152
162
self ._kill ()
153
163
self ._post_finish (
154
- "K" , ErrorMessage (line_number = None , message = "Job killed!" , context = [])
164
+ "K" ,
165
+ ErrorMessage (line_number = None , message = "Job killed!" , context = []),
155
166
)
156
167
157
- def _post_finish (self , status , err_msg ) :
158
- self ._close_process ()
159
- self .process = None
168
+ def _post_finish (self , status : str , err_msg : Union [ ErrorMessage , None ]) -> None :
169
+ self ._close_job_log_file ()
170
+ self ._process = None
160
171
super ()._post_finish (status , err_msg )
161
172
162
- def _close_process (self ):
173
+ def _close_job_log_file (self ) -> None :
163
174
"""Close the file descriptors associated with the process."""
164
-
165
- assert self .process
166
- if self .process .stdout :
167
- self .process .stdout .close ()
175
+ if self ._log_file :
176
+ self ._log_file .close ()
0 commit comments