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 Local process implementation for running and monitoring nodes.
37 """
38
39 import os
40 import sys
41 import signal
42 import socket
43 import subprocess
44 import time
45 import traceback
46
47 import roslib.rosenv
48 import roslib.network
49
50 from roslaunch.core import *
51 from roslaunch.node_args import create_local_process_env, create_local_process_args
52 from roslaunch.pmon import Process, FatalProcessLaunch
53
54 import logging
55 _logger = logging.getLogger("roslaunch")
56
57 _TIMEOUT_SIGINT = 15.0
58 _TIMEOUT_SIGTERM = 2.0
59
60 _counter = 0
65
67 """
68 Launch a master
69 @param type_: name of master executable (currently just Master.ZENMASTER)
70 @type type_: str
71 @param ros_root: ROS_ROOT environment setting
72 @type ros_root: str
73 @param port: port to launch master on
74 @type port: int
75 @raise RLException: if type_ or port is invalid
76 """
77 if port < 1 or port > 65535:
78 raise RLException("invalid port assignment: %s"%port)
79
80 _logger.info("create_master_process: %s, %s, %s", type_, ros_root, port)
81 master = os.path.join(ros_root, 'bin', type_)
82
83 if type_ in [Master.ROSMASTER, Master.ZENMASTER]:
84 package = 'rosmaster'
85 args = [master, '--core', '-p', str(port)]
86 else:
87 raise RLException("unknown master typ_: %s"%type_)
88
89 _logger.info("process[master]: launching with args [%s]"%args)
90 log_output = False
91 return LocalProcess(run_id, package, 'master', args, os.environ, log_output, None)
92
94 """
95 Factory for generating processes for launching local ROS
96 nodes. Also registers the process with the L{ProcessMonitor} so that
97 events can be generated when the process dies.
98
99 @param run_id: run_id of launch
100 @type run_id: str
101 @param node: node to launch. Node name must be assigned.
102 @type node: L{Node}
103 @param master_uri: API URI for master node
104 @type master_uri: str
105 @return: local process instance
106 @rtype: L{LocalProcess}
107 @raise NodeParamsException: If the node's parameters are improperly specific
108 """
109 _logger.info("create_node_process: package[%s] type[%s] machine[%s] master_uri[%s]", node.package, node.type, node.machine, master_uri)
110
111 machine = node.machine
112 if machine is None:
113 raise RLException("Internal error: no machine selected for node of type [%s/%s]"%(node.package, node.type))
114 if not node.name:
115 raise ValueError("node name must be assigned")
116
117
118 env = create_local_process_env(node, machine, master_uri)
119
120 if not node.name:
121 raise ValueError("node name must be assigned")
122
123
124
125
126 name = "%s-%s"%(roslib.names.ns_join(node.namespace, node.name), _next_counter())
127 if name[0] == '/':
128 name = name[1:]
129
130 _logger.info('process[%s]: env[%s]', name, env)
131
132 args = create_local_process_args(node, machine)
133
134 _logger.info('process[%s]: args[%s]', name, args)
135
136
137 log_output = node.output != 'screen'
138 _logger.debug('process[%s]: returning LocalProcess wrapper')
139 return LocalProcess(run_id, node.package, name, args, env, log_output, respawn=node.respawn, required=node.required, cwd=node.cwd)
140
141
143 """
144 Process launched on local machine
145 """
146
147 - def __init__(self, run_id, package, name, args, env, log_output, respawn=False, required=False, cwd=None, is_node=True):
148 """
149 @param run_id: unique run ID for this roslaunch. Used to
150 generate log directory location. run_id may be None if this
151 feature is not being used.
152 @type run_id: str
153 @param package: name of package process is part of
154 @type package: str
155 @param name: name of process
156 @type name: str
157 @param args: list of arguments to process
158 @type args: [str]
159 @param env: environment dictionary for process
160 @type env: {str : str}
161 @param log_output: if True, log output streams of process
162 @type log_output: bool
163 @param respawn: respawn process if it dies (default is False)
164 @type respawn: bool
165 @param cwd: working directory of process, or None
166 @type cwd: str
167 @param is_node: (optional) if True, process is ROS node and accepts ROS node command-line arguments. Default: True
168 @type is_node: False
169 """
170 super(LocalProcess, self).__init__(package, name, args, env, respawn, required)
171 self.run_id = run_id
172 self.popen = None
173 self.log_output = log_output
174 self.started = False
175 self.stopped = False
176 self.cwd = cwd
177 self.log_dir = None
178 self.pid = -1
179 self.is_node = is_node
180
181
183 """
184 Get all data about this process in dictionary form
185 """
186 info = super(LocalProcess, self).get_info()
187 info['pid'] = self.pid
188 if self.run_id:
189 info['run_id'] = self.run_id
190 info['log_output'] = self.log_output
191 if self.cwd is not None:
192 info['cwd'] = self.cwd
193 return info
194
241
243 """
244 Start the process.
245
246 @raise FatalProcessLaunch: if process cannot be started and it
247 is not likely to ever succeed
248 """
249 super(LocalProcess, self).start()
250 try:
251 self.lock.acquire()
252 if self.started:
253 _logger.info("process[%s]: restarting os process", self.name)
254 else:
255 _logger.info("process[%s]: starting os process", self.name)
256 self.started = self.stopped = False
257
258 full_env = self.env
259
260
261 try:
262 logfileout, logfileerr = self._configure_logging()
263 except Exception, e:
264 _logger.error(traceback.format_exc())
265 printerrlog("[%s] ERROR: unable to configure logging [%s]"%(self.name, str(e)))
266
267
268
269 logfileout, logfileerr = subprocess.PIPE, subprocess.PIPE
270
271 if self.cwd == 'node':
272 cwd = os.path.dirname(self.args[0])
273 elif self.cwd == 'cwd':
274 cwd = os.getcwd()
275 elif self.cwd == 'ros-root':
276 cwd = get_ros_root()
277 else:
278 cwd = roslib.rosenv.get_ros_home()
279
280 _logger.info("process[%s]: start w/ args [%s]", self.name, self.args)
281 _logger.info("process[%s]: cwd will be [%s]", self.name, cwd)
282
283 try:
284 self.popen = subprocess.Popen(self.args, cwd=cwd, stdout=logfileout, stderr=logfileerr, env=full_env, close_fds=True, preexec_fn=os.setsid)
285 except OSError, (errno, msg):
286 self.started = True
287 _logger.error("OSError(%d, %s)", errno, msg)
288 if errno == 8:
289 raise FatalProcessLaunch("Unable to launch [%s]. \nIf it is a script, you may be missing a '#!' declaration at the top."%self.name)
290 elif errno == 2:
291 raise FatalProcessLaunch("""Roslaunch got a '%s' error while attempting to run:
292
293 %s
294
295 Please make sure that all the executables in this command exist and have
296 executable permission. This is often caused by a bad launch-prefix."""%(msg, ' '.join(self.args)))
297 else:
298 raise FatalProcessLaunch("unable to launch [%s]: %s"%(' '.join(self.args), msg))
299
300 self.started = True
301
302
303
304 poll_result = self.popen.poll()
305 if poll_result is None or poll_result == 0:
306 self.pid = self.popen.pid
307 printlog_bold("process[%s]: started with pid [%s]"%(self.name, self.pid))
308 return True
309 else:
310 printerrlog("failed to start local process: %s"%(' '.join(self.args)))
311 return False
312 finally:
313 self.lock.release()
314
316 return self.name.replace('/', '-')
317
319 """
320 @return: True if process is still running
321 @rtype: bool
322 """
323 if not self.started:
324 return True
325 if self.stopped or self.popen is None:
326 return False
327 self.exit_code = self.popen.poll()
328 if self.exit_code is not None:
329 return False
330 return True
331
333 """
334 @return: human-readable description of exit state
335 @rtype: str
336 """
337
338 if self.exit_code is not None:
339 if self.exit_code:
340 if self.log_dir:
341 return 'process has died [pid %s, exit code %s].\nlog files: %s*.log'%(self.pid, self.exit_code, os.path.join(self.log_dir, self._log_name()))
342 else:
343 return 'process has died [pid %s, exit code %s]'%(self.pid, self.exit_code)
344 else:
345 if self.log_dir:
346 return 'process has finished cleanly.\nlog file: %s*.log'%(os.path.join(self.log_dir, self._log_name()))
347 else:
348 return 'process has finished cleanly'
349 else:
350 return 'process has died'
351
353 """
354 UNIX implementation of process killing
355
356 @param errors: error messages. stop() will record messages into this list.
357 @type errors: [str]
358 """
359 self.exit_code = self.popen.poll()
360 if self.exit_code is not None:
361 _logger.debug("process[%s].stop(): process has already returned %s", self.name, self.exit_code)
362
363 self.popen = None
364 self.stopped = True
365 return
366
367 pid = self.popen.pid
368 pgid = os.getpgid(pid)
369 _logger.info("process[%s]: killing os process with pid[%s] pgid[%s]", self.name, pid, pgid)
370
371 try:
372
373 _logger.info("[%s] sending SIGINT to pgid [%s]", self.name, pgid)
374 os.killpg(pgid, signal.SIGINT)
375 _logger.info("[%s] sent SIGINT to pgid [%s]", self.name, pgid)
376 timeout_t = time.time() + _TIMEOUT_SIGINT
377 retcode = self.popen.poll()
378 while time.time() < timeout_t and retcode is None:
379 time.sleep(0.1)
380 retcode = self.popen.poll()
381
382 if retcode is None:
383 printerrlog("[%s] escalating to SIGTERM"%self.name)
384 timeout_t = time.time() + _TIMEOUT_SIGTERM
385 os.killpg(pgid, signal.SIGTERM)
386 _logger.info("[%s] sent SIGTERM to pgid [%s]"%(self.name, pgid))
387 retcode = self.popen.poll()
388 while time.time() < timeout_t and retcode is None:
389 time.sleep(0.2)
390 _logger.debug('poll for retcode')
391 retcode = self.popen.poll()
392 if retcode is None:
393 printerrlog("[%s] escalating to SIGKILL"%self.name)
394 errors.append("process[%s, pid %s]: required SIGKILL. May still be running."%(self.name, pid))
395 try:
396 os.killpg(pgid, signal.SIGKILL)
397 _logger.info("[%s] sent SIGKILL to pgid [%s]"%(self.name, pgid))
398
399
400
401 _logger.info("process[%s]: sent SIGKILL", self.name)
402 except OSError, e:
403 if e.args[0] == 3:
404 printerrlog("no [%s] process with pid [%s]"%(self.name, pid))
405 else:
406 printerrlog("errors shutting down [%s], see log for details"%self.name)
407 _logger.error(traceback.format_exc())
408 else:
409 _logger.info("process[%s]: SIGTERM killed with return value %s", self.name, retcode)
410 else:
411 _logger.info("process[%s]: SIGINT killed with return value %s", self.name, retcode)
412
413 finally:
414 self.popen = None
415
416 - def stop(self, errors=None):
417 """
418 Stop the process. Record any significant error messages in the errors parameter
419
420 @param errors: error messages. stop() will record messages into this list.
421 @type errors: [str]
422 """
423 if errors is None:
424 errors = []
425 super(LocalProcess, self).stop(errors)
426 self.lock.acquire()
427 try:
428 try:
429 _logger.debug("process[%s].stop() starting", self.name)
430 if self.popen is None:
431 _logger.debug("process[%s].stop(): popen is None, nothing to kill")
432 return
433
434
435 self._stop_unix(errors)
436 except:
437
438 _logger.error("[%s] EXCEPTION %s", self.name, traceback.format_exc())
439 finally:
440 self.stopped = True
441 self.lock.release()
442
443
444
446 """
447 Remove all instances of args that start with prefix. This is used
448 to remove args that were previously added (and are now being
449 regenerated due to respawning)
450 """
451 existing_args = [a for a in args if a.startswith(prefix)]
452 for a in existing_args:
453 args.remove(a)
454 return args
455