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 import os
34 import shlex
35 import subprocess
36
37 import rospy
38
39 from detailed_msg_box import DetailedError
40 from supervised_popen import SupervisedPopen
41 import node_manager_fkie as nm
46
49 ''' '''
50
55
57 return "ScreenSelectionRequest from " + self.choices + "::" + repr(self.error)
58
61 '''
62 The class to handle the running screen sessions and create new sessions on
63 start of the ROS nodes.
64 '''
65
66 LOG_PATH = os.environ.get('ROS_LOG_DIR') if os.environ.get('ROS_LOG_DIR') else os.path.join(os.path.expanduser('~'), '.ros/log/')
67 SCREEN = "/usr/bin/screen"
68 SLASH_SEP = '_'
69
70 @classmethod
72 '''
73 Creates a name for the screen session. All slash separators are replaced by
74 L{SLASH_SEP}
75 @param node: the name of the node
76 @type node: C{str}
77 @return: name for the screen session.
78 @rtype: C{str}
79 '''
80
81
82 node_name = str(node).replace('/', cls.SLASH_SEP) if node is not None else ''
83
84 return node_name
85
86 @classmethod
88 '''
89 Splits the screen session name into PID and session name generated by
90 L{createSessionName()}.
91 @param session: the screen session name
92 @type session: C{str}
93 @return: PID, session name generated by L{createSessionName()}. Not presented
94 values are coded as empty strings. Not valid session names have an empty
95 PID string.
96 @rtype: C{str, str}
97 '''
98 result = session.split('.', 1)
99 if len(result) != 2:
100 return '', ''
101 pid = result[0]
102 node = result[1]
103
104
105 return pid, node
106
107 @classmethod
109 '''
110 Tests for whether the L{SCREEN} binary exists and raise an exception if not.
111 @raise ScreenHandlerException: if the screen binary not exists.
112 '''
113 if not os.path.isfile(cls.SCREEN):
114 raise ScreenHandlerException(''.join([cls.SCREEN, " is missing"]))
115
116 @classmethod
118 '''
119 Generates a log file name for the screen session.
120 @param session: the name of the screen session
121 @type session: C{str}
122 @return: the log file name
123 @rtype: C{str}
124 '''
125 if session is not None:
126 return os.path.join(cls.LOG_PATH, session + '.log')
127 elif node is not None:
128 return os.path.join(cls.LOG_PATH, cls.createSessionName(node) + '.log')
129 else:
130 return os.path.join(cls.LOG_PATH, 'unknown.log')
131
132 @classmethod
134 '''
135 Generates a log file name of the ROS log.
136 @param node: the name of the node
137 @type node: C{str}
138 @return: the ROS log file name
139 @rtype: C{str}
140 @todo: get the run_id from the ROS parameter server and search in this log folder
141 for the log file (handle the node started using a launch file).
142 '''
143 if node is not None:
144 return os.path.join(cls.LOG_PATH, node.strip(rospy.names.SEP).replace(rospy.names.SEP, '_') + '.log')
145 else:
146 return ''
147
148 @classmethod
150 '''
151 Generates a configuration file name for the screen session.
152 @param session: the name of the screen session
153 @type session: C{str}
154 @return: the configuration file name
155 @rtype: C{str}
156 '''
157 if session is not None:
158 return os.path.join(cls.LOG_PATH, session + '.conf')
159 elif node is not None:
160 return os.path.join(cls.LOG_PATH, cls.createSessionName(node) + '.conf')
161 else:
162 return os.path.join(cls.LOG_PATH, 'unknown.conf')
163
164 @classmethod
166 '''
167 Generates a PID file name for the screen session.
168 @param session: the name of the screen session
169 @type session: C{str}
170 @return: the PID file name
171 @rtype: C{str}
172 '''
173 if session is not None:
174 return os.path.join(cls.LOG_PATH, session + '.pid')
175 elif node is not None:
176 return os.path.join(cls.LOG_PATH, cls.createSessionName(node) + '.pid')
177 else:
178 return os.path.join(cls.LOG_PATH, 'unknown.pid')
179
180 @classmethod
181 - def getActiveScreens(cls, host, session='', auto_pw_request=True, user=None, pwd=None):
182 '''
183 Returns the list with all compatible screen names. If the session is set to
184 an empty string all screens will be returned.
185 @param host: the host name or IP to search for the screen session.
186 @type host: C{str}
187 @param session: the name or the suffix of the screen session
188 @type session: C{str} (Default: C{''})
189 @return: the list with session names
190 @rtype: C{[str(session name), ...]}
191 @raise Exception: on errors while resolving host
192 @see: L{node_manager_fkie.is_local()}
193 '''
194 try:
195 return cls._getActiveScreens(host, session, auto_pw_request, user, pwd)
196 except nm.AuthenticationRequest as e:
197 raise nm.InteractionNeededError(e, cls.getActiveScreens, (host, session, auto_pw_request))
198
199 @classmethod
200 - def _getActiveScreens(cls, host, session='', auto_pw_request=True, user=None, pwd=None):
201 '''
202 Returns the list with all compatible screen names. If the session is set to
203 an empty string all screens will be returned.
204 @param host: the host name or IP to search for the screen session.
205 @type host: C{str}
206 @param session: the name or the suffix of the screen session
207 @type session: C{str} (Default: C{''})
208 @return: the list with session names
209 @rtype: C{[str(session name), ...]}
210 @raise Exception: on errors while resolving host
211 @see: L{node_manager_fkie.is_local()}
212 '''
213 output = None
214 result = []
215 if nm.is_local(host):
216 output = cls.getLocalOutput([cls.SCREEN, '-ls'])
217 else:
218 _, stdout, _, _ = nm.ssh().ssh_exec(host, [cls.SCREEN, ' -ls'], user, pwd, auto_pw_request, close_stdin=True, close_stderr=True)
219 output = stdout.read()
220 stdout.close()
221 if output:
222 splits = output.split()
223 for i in splits:
224 if i.count('.') > 0 and i.endswith(session) and i.find('._') >= 0:
225 result.append(i)
226 return result
227
228 @classmethod
230 '''
231 Open the screen output in a new terminal.
232 @param host: the host name or ip where the screen is running.
233 @type host: C{str}
234 @param screen_name: the name of the screen to show
235 @type screen_name: C{str}
236 @param nodename: the name of the node is used for the title of the terminal
237 @type nodename: C{str}
238 @raise Exception: on errors while resolving host
239 @see: L{node_manager_fkie.is_local()}
240 '''
241
242
243 title_opt = 'SCREEN %s on %s' % (nodename, host)
244 if nm.is_local(host):
245 cmd = nm.settings().terminal_cmd([cls.SCREEN, '-x', screen_name], title_opt)
246 rospy.loginfo("Open screen terminal: %s", cmd)
247 SupervisedPopen(shlex.split(cmd), object_id=title_opt, description="Open screen terminal: %s" % title_opt)
248 else:
249 ps = nm.ssh().ssh_x11_exec(host, [cls.SCREEN, '-x', screen_name], title_opt, user)
250 rospy.loginfo("Open remote screen terminal: %s", ps)
251
252 @classmethod
253 - def openScreen(cls, node, host, auto_item_request=False, user=None, pw=None, items=[]):
254 '''
255 Searches for the screen associated with the given node and open the screen
256 output in a new terminal.
257 @param node: the name of the node those screen output to show
258 @type node: C{str}
259 @param host: the host name or ip where the screen is running
260 @type host: C{str}
261 @raise Exception: on errors while resolving host
262 @see: L{openScreenTerminal()} or L{_getActiveScreens()}
263 '''
264 if node is None or len(node) == 0:
265 return False
266 try:
267 if items:
268 for item in items:
269
270 cls.openScreenTerminal(host, item, node, user)
271 else:
272
273 screens = cls._getActiveScreens(host, cls.createSessionName(node), auto_item_request, user, pw)
274 if len(screens) == 1:
275 cls.openScreenTerminal(host, screens[0], node, user)
276 else:
277
278 choices = {}
279 for s in screens:
280 pid, session_name = cls.splitSessionName(s)
281 choices[''.join([session_name, ' [', pid, ']'])] = s
282
283 if len(choices) > 0:
284 if len(choices) == 1:
285 cls.openScreenTerminal(host, choices[0], node, user)
286 elif auto_item_request:
287 from select_dialog import SelectDialog
288 items, _ = SelectDialog.getValue('Show screen', '', choices.keys(), False)
289 for item in items:
290
291 cls.openScreenTerminal(host, choices[item], node, user)
292 else:
293 raise ScreenSelectionRequest(choices, 'Show screen')
294 else:
295 raise DetailedError('Show screen', 'No screen for %s on %s found!\nSee log for details!' % (node, host))
296 return len(screens) > 0
297 except nm.AuthenticationRequest as e:
298 raise nm.InteractionNeededError(e, cls.openScreen, (node, host, auto_item_request))
299 except ScreenSelectionRequest as e:
300 raise nm.InteractionNeededError(e, cls.openScreen, (node, host, auto_item_request, user, pw))
301
302 @classmethod
303 - def killScreens(cls, node, host, auto_ok_request=True, user=None, pw=None):
304 '''
305 Searches for the screen associated with the given node and kill this screens.
306 @param node: the name of the node those screen output to show
307 @type node: C{str}
308 @param host: the host name or ip where the screen is running
309 @type host: C{str}
310 '''
311 if node is None or len(node) == 0:
312 return False
313 try:
314
315 screens = cls._getActiveScreens(host, cls.createSessionName(node), auto_ok_request, user=user, pwd=pw)
316 if screens:
317 do_kill = True
318 if auto_ok_request:
319 try:
320 from python_qt_binding.QtGui import QMessageBox
321 except:
322 from python_qt_binding.QtWidgets import QMessageBox
323 result = QMessageBox.question(None, "Kill SCREENs?", '\n'.join(screens), QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok)
324 if result & QMessageBox.Ok:
325 do_kill = True
326 if do_kill:
327 for s in screens:
328 pid, _, _ = s.partition('.')
329 if pid:
330 try:
331 nm.starter()._kill_wo(host, int(pid), auto_ok_request, user, pw)
332 except:
333 import traceback
334 rospy.logwarn("Error while kill screen (PID: %s) on host '%s': %s", str(pid), str(host), traceback.format_exc(1))
335 if nm.is_local(host):
336 SupervisedPopen([cls.SCREEN, '-wipe'], object_id='screen -wipe', description="screen: clean up the socket with -wipe")
337 else:
338 nm.ssh().ssh_exec(host, [cls.SCREEN, '-wipe'], close_stdin=True, close_stdout=True, close_stderr=True)
339 except nm.AuthenticationRequest as e:
340 raise nm.InteractionNeededError(e, cls.killScreens, (node, host, auto_ok_request))
341
342 @classmethod
344 '''
345 This method is used to read the output of the command executed in a terminal.
346 @param cmd: the command to execute in a terminal
347 @type cmd: C{str}
348 @return: the output generated by the execution of the command.
349 @rtype: output
350 '''
351 ps = SupervisedPopen(cmd, stdout=subprocess.PIPE)
352 result = ps.stdout.read()
353 return result
354
355 @classmethod
357 '''
358 Generates a configuration file and return the command prefix to start the given node
359 in a screen terminal.
360 @param node: the name of the node
361 @type node: C{str}
362 @return: the command prefix
363 @rtype: C{str}
364 '''
365 f = open(cls.getScreenCfgFile(node=node), 'w')
366 f.write(''.join(["logfile ", cls.getScreenLogFile(node=node), "\n"]))
367 f.write("logfile flush 0\n")
368 f.write("defscrollback 10000\n")
369 ld_library_path = os.getenv('LD_LIBRARY_PATH', '')
370 if ld_library_path:
371 f.write(' '.join(['setenv', 'LD_LIBRARY_PATH', ld_library_path, "\n"]))
372 ros_etc_dir = os.getenv('ROS_ETC_DIR', '')
373 if ros_etc_dir:
374 f.write(' '.join(['setenv', 'ROS_ETC_DIR', ros_etc_dir, "\n"]))
375 f.close()
376 return ' '.join([cls.SCREEN, '-c', cls.getScreenCfgFile(node=node), '-L', '-dmS', cls.createSessionName(node=node)])
377