blob: 36957e2879cc7558d5253b43775d587c72f18d6a [file] [log] [blame]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001#!/usr/bin/python -u
2# -*- coding: utf-8 -*-
3#
4# Copyright 2015 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08008import argparse
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08009import fcntl
10import json
11import logging
12import os
13import Queue
14import select
15import socket
16import subprocess
17import sys
18import threading
19import time
20import uuid
21
22
23_OVERLORD_PORT = 4455
24_OVERLORD_LAN_DISCOVERY_PORT = 4456
25
26_BUFSIZE = 8192
27_RETRY_INTERVAL = 2
28_SEPARATOR = '\r\n'
29_PING_TIMEOUT = 3
30_PING_INTERVAL = 5
31_REQUEST_TIMEOUT_SECS = 60
32_SHELL = os.getenv('SHELL', '/bin/bash')
33
34RESPONSE_SUCCESS = 'success'
35RESPONSE_FAILED = 'failed'
36
37
38class PingTimeoutError(Exception):
39 pass
40
41
42class RequestError(Exception):
43 pass
44
45
46class Ghost(object):
47 """Ghost implements the client protocol of Overlord.
48
49 Ghost provide terminal/shell/logcat functionality and manages the client
50 side connectivity.
51 """
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080052 NONE, AGENT, TERMINAL, SHELL, LOGCAT = range(5)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080053
54 MODE_NAME = {
55 NONE: 'NONE',
56 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080057 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080058 SHELL: 'Shell',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080059 LOGCAT: 'Logcat'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080060 }
61
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080062 def __init__(self, overlord_addrs, mode=AGENT, sid=None, command=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080063 """Constructor.
64
65 Args:
66 overlord_addrs: a list of possible address of overlord.
67 mode: client mode, either AGENT, SHELL or LOGCAT
68 sid: session id. If the connection is requested by overlord, sid should
69 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080070 shell: the command to execute when we are in SHELL mode.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080071 """
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080072 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL]
73 if mode == Ghost.SHELL:
74 assert command is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080075
76 self._overlord_addrs = overlord_addrs
77 self._mode = mode
78 self._sock = None
79 self._machine_id = self.GetMachineID()
80 self._client_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +080081 self._properties = {}
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080082 self._shell_command = command
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080083 self._buf = ''
84 self._requests = {}
85 self._reset = False
86 self._last_ping = 0
87 self._queue = Queue.Queue()
88
Wei-Ning Huang7d029b12015-03-06 10:32:15 +080089 def LoadPropertiesFromFile(self, filename):
90 try:
91 with open(filename, 'r') as f:
92 self._properties = json.loads(f.read())
93 except Exception as e:
94 logging.exception('LoadPropertiesFromFile: ' + str(e))
95
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080096 def SpawnGhost(self, mode, sid, command=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080097 """Spawn a child ghost with specific mode.
98
99 Returns:
100 The spawned child process pid.
101 """
102 pid = os.fork()
103 if pid == 0:
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800104 g = Ghost(self._overlord_addrs, mode, sid, command)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800105 g.Start(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800106 sys.exit(0)
107 else:
108 return pid
109
110 def Timestamp(self):
111 return int(time.time())
112
113 def GetGateWayIP(self):
114 with open('/proc/net/route', 'r') as f:
115 lines = f.readlines()
116
117 ips = []
118 for line in lines:
119 parts = line.split('\t')
120 if parts[2] == '00000000':
121 continue
122
123 try:
124 h = parts[2].decode('hex')
125 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
126 except TypeError:
127 pass
128
129 return ips
130
131 def GetMachineID(self):
132 """Generates machine-dependent ID string for a machine.
133 There are many ways to generate a machine ID:
134 1. factory device-data
135 2. /sys/class/dmi/id/product_uuid (only available on intel machines)
136 3. MAC address
137 We follow the listed order to generate machine ID, and fallback to the next
138 alternative if the previous doesn't work.
139 """
140 try:
141 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
142 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800143 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800144 stdout, _ = p.communicate()
145 if stdout == '':
146 raise RuntimeError("empty mlb number")
147 return stdout.strip()
148 except Exception:
149 pass
150
151 try:
152 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
153 return f.read().strip()
154 except Exception:
155 pass
156
157 try:
158 macs = []
159 ifaces = sorted(os.listdir('/sys/class/net'))
160 for iface in ifaces:
161 if iface == 'lo':
162 continue
163
164 with open('/sys/class/net/%s/address' % iface, 'r') as f:
165 macs.append(f.read().strip())
166
167 return ';'.join(macs)
168 except Exception:
169 pass
170
171 raise RuntimeError("can't generate machine ID")
172
173 def Reset(self):
174 """Reset state and clear request handlers."""
175 self._reset = False
176 self._buf = ""
177 self._last_ping = 0
178 self._requests = {}
179
180 def SendMessage(self, msg):
181 """Serialize the message and send it through the socket."""
182 self._sock.send(json.dumps(msg) + _SEPARATOR)
183
184 def SendRequest(self, name, args, handler=None,
185 timeout=_REQUEST_TIMEOUT_SECS):
186 if handler and not callable(handler):
187 raise RequestError('Invalid requiest handler for msg "%s"' % name)
188
189 rid = str(uuid.uuid4())
190 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
191 self._requests[rid] = [self.Timestamp(), timeout, handler]
192 self.SendMessage(msg)
193
194 def SendResponse(self, omsg, status, params=None):
195 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
196 self.SendMessage(msg)
197
198 def SpawnPTYServer(self, _):
199 """Spawn a PTY server and forward I/O to the TCP socket."""
200 logging.info('SpawnPTYServer: started')
201
202 pid, fd = os.forkpty()
203 if pid == 0:
204 env = os.environ.copy()
205 env['USER'] = os.getenv('USER', 'root')
206 env['HOME'] = os.getenv('HOME', '/root')
207 os.chdir(env['HOME'])
208 os.execve(_SHELL, [_SHELL], env)
209 else:
210 try:
211 while True:
212 rd, _, _ = select.select([self._sock, fd], [], [])
213
214 if fd in rd:
215 self._sock.send(os.read(fd, _BUFSIZE))
216
217 if self._sock in rd:
218 ret = self._sock.recv(_BUFSIZE)
219 if len(ret) == 0:
220 raise RuntimeError("socket closed")
221 os.write(fd, ret)
222 except (OSError, socket.error, RuntimeError):
223 self._sock.close()
224 logging.info('SpawnPTYServer: terminated')
225 sys.exit(0)
226
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800227 def SpawnShellServer(self, _):
228 """Spawn a shell server and forward input/output from/to the TCP socket."""
229 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800230
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800231 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800232 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
233 shell=True)
234
235 def make_non_block(fd):
236 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
237 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
238
239 make_non_block(p.stdout)
240 make_non_block(p.stderr)
241
242 try:
243 while True:
244 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800245 p.poll()
246
247 if p.returncode != None:
248 raise RuntimeError("process complete")
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800249
250 if p.stdout in rd:
251 self._sock.send(p.stdout.read(_BUFSIZE))
252
253 if p.stderr in rd:
254 self._sock.send(p.stderr.read(_BUFSIZE))
255
256 if self._sock in rd:
257 ret = self._sock.recv(_BUFSIZE)
258 if len(ret) == 0:
259 raise RuntimeError("socket closed")
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800260 p.stdin.write(ret)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800261 except (OSError, socket.error, RuntimeError):
262 self._sock.close()
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800263 logging.info('SpawnShellServer: terminated')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800264 sys.exit(0)
265
266
267 def Ping(self):
268 def timeout_handler(x):
269 if x is None:
270 raise PingTimeoutError
271
272 self._last_ping = self.Timestamp()
273 self.SendRequest('ping', {}, timeout_handler, 5)
274
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800275 def HandleRequest(self, msg):
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800276 if msg['name'] == 'terminal':
277 self.SpawnGhost(self.TERMINAL, msg['params']['sid'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800278 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800279 elif msg['name'] == 'shell':
280 self.SpawnGhost(self.SHELL, msg['params']['sid'],
281 msg['params']['command'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800282 self.SendResponse(msg, RESPONSE_SUCCESS)
283
284 def HandleResponse(self, response):
285 rid = str(response['rid'])
286 if rid in self._requests:
287 handler = self._requests[rid][2]
288 del self._requests[rid]
289 if callable(handler):
290 handler(response)
291 else:
292 print(response, self._requests.keys())
293 logging.warning('Recvied unsolicited response, ignored')
294
295 def ParseMessage(self):
296 msgs_json = self._buf.split(_SEPARATOR)
297 self._buf = msgs_json.pop()
298
299 for msg_json in msgs_json:
300 try:
301 msg = json.loads(msg_json)
302 except ValueError:
303 # Ignore mal-formed message.
304 continue
305
306 if 'name' in msg:
307 self.HandleRequest(msg)
308 elif 'response' in msg:
309 self.HandleResponse(msg)
310 else: # Ingnore mal-formed message.
311 pass
312
313 def ScanForTimeoutRequests(self):
314 for rid in self._requests.keys()[:]:
315 request_time, timeout, handler = self._requests[rid]
316 if self.Timestamp() - request_time > timeout:
317 handler(None)
318 del self._requests[rid]
319
320 def Listen(self):
321 try:
322 while True:
323 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
324
325 if self._sock in rds:
326 self._buf += self._sock.recv(_BUFSIZE)
327 self.ParseMessage()
328
329 if self.Timestamp() - self._last_ping > _PING_INTERVAL:
330 self.Ping()
331 self.ScanForTimeoutRequests()
332
333 if self._reset:
334 self.Reset()
335 break
336 except socket.error:
337 raise RuntimeError('Connection dropped')
338 except PingTimeoutError:
339 raise RuntimeError('Connection timeout')
340 finally:
341 self._sock.close()
342
343 self._queue.put('resume')
344
345 if self._mode != Ghost.AGENT:
346 sys.exit(1)
347
348 def Register(self):
349 non_local = {}
350 for addr in self._overlord_addrs:
351 non_local['addr'] = addr
352 def registered(response):
353 if response is None:
354 self._reset = True
355 raise RuntimeError('Register request timeout')
356 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
357 self._queue.put("pause", True)
358
359 try:
360 logging.info('Trying %s:%d ...', *addr)
361 self.Reset()
362 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
363 self._sock.settimeout(_PING_TIMEOUT)
364 self._sock.connect(addr)
365
366 logging.info('Connection established, registering...')
367 handler = {
368 Ghost.AGENT: registered,
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800369 Ghost.TERMINAL: self.SpawnPTYServer,
370 Ghost.SHELL: self.SpawnShellServer
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800371 }[self._mode]
372
373 # Machine ID may change if MAC address is used (USB-ethernet dongle
374 # plugged/unplugged)
375 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800376 self.SendRequest('register',
377 {'mode': self._mode, 'mid': self._machine_id,
378 'cid': self._client_id,
379 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800380 except socket.error:
381 pass
382 else:
383 self._sock.settimeout(None)
384 self.Listen()
385
386 raise RuntimeError("Cannot connect to any server")
387
388 def StartLanDiscovery(self):
389 """Start to listen to LAN discovery packet at
390 _OVERLORD_LAN_DISCOVERY_PORT."""
391 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
392 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
393 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
394 try:
395 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
396 except socket.error as e:
397 logging.error("LAN discovery: %s, abort", e)
398 return
399
400 logging.info('LAN Discovery: started')
401 while True:
402 rd, _, _ = select.select([s], [], [], 1)
403
404 if s in rd:
405 data, source_addr = s.recvfrom(_BUFSIZE)
406 parts = data.split()
407 if parts[0] == 'OVERLORD':
408 ip = source_addr[0]
409 port = int(parts[1].lstrip(':'))
410 addr = (ip, port)
411 self._queue.put(addr, True)
412
413 try:
414 obj = self._queue.get(False)
415 except Queue.Empty:
416 pass
417 else:
418 if type(obj) is not str:
419 self._queue.put(obj)
420 elif obj == 'pause':
421 logging.info('LAN Discovery: paused')
422 while True:
423 obj = self._queue.get(True)
424 if obj == 'resume':
425 logging.info('LAN Discovery: resumed')
426 break
427
428 def ScanGateway(self):
429 for addr in [(x, _OVERLORD_PORT) for x in self.GetGateWayIP()]:
430 if addr not in self._overlord_addrs:
431 self._overlord_addrs.append(addr)
432
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800433 def Start(self, no_lan_disc=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800434 logging.info('%s started', self.MODE_NAME[self._mode])
435 logging.info('MID: %s', self._machine_id)
436 logging.info('CID: %s', self._client_id)
437
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800438 if not no_lan_disc:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800439 t = threading.Thread(target=self.StartLanDiscovery)
440 t.daemon = True
441 t.start()
442
443 try:
444 while True:
445 try:
446 addr = self._queue.get(False)
447 except Queue.Empty:
448 pass
449 else:
450 if type(addr) == tuple and addr not in self._overlord_addrs:
451 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
452 self._overlord_addrs.append(addr)
453
454 try:
455 self.ScanGateway()
456 self.Register()
457 except Exception as e:
458 logging.info(str(e) + ', retrying in %ds' % _RETRY_INTERVAL)
459 time.sleep(_RETRY_INTERVAL)
460
461 self.Reset()
462 except KeyboardInterrupt:
463 logging.error('Received keyboard interrupt, quit')
464 sys.exit(0)
465
466
467def main():
468 logger = logging.getLogger()
469 logger.setLevel(logging.INFO)
470
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800471 parser = argparse.ArgumentParser()
472 parser.add_argument('--no-lan-disc', dest='no_lan_disc', action='store_true',
473 default=False, help='disable LAN discovery')
474 parser.add_argument("--prop-file", dest="prop_file", type=str, default=None,
475 help='file containing the JSON representation of client '
476 'properties')
477 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
478 nargs='*', help='overlord server address')
479 args = parser.parse_args()
480
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800481 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800482 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800483
484 g = Ghost(addrs)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800485 if args.prop_file:
486 g.LoadPropertiesFromFile(args.prop_file)
487 g.Start(args.no_lan_disc)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800488
489
490if __name__ == '__main__':
491 main()