blob: 5d7006d8f3c29b425fdb7729a55ee183b4268c6d [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
Wei-Ning Huang2132de32015-04-13 17:24:38 +080022import jsonrpclib
23from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
24
25
26_GHOST_RPC_PORT = 4499
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080027
28_OVERLORD_PORT = 4455
29_OVERLORD_LAN_DISCOVERY_PORT = 4456
30
31_BUFSIZE = 8192
32_RETRY_INTERVAL = 2
33_SEPARATOR = '\r\n'
34_PING_TIMEOUT = 3
35_PING_INTERVAL = 5
36_REQUEST_TIMEOUT_SECS = 60
37_SHELL = os.getenv('SHELL', '/bin/bash')
Wei-Ning Huang2132de32015-04-13 17:24:38 +080038_DEFAULT_BIND_ADDRESS = '0.0.0.0'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080039
40RESPONSE_SUCCESS = 'success'
41RESPONSE_FAILED = 'failed'
42
43
44class PingTimeoutError(Exception):
45 pass
46
47
48class RequestError(Exception):
49 pass
50
51
52class Ghost(object):
53 """Ghost implements the client protocol of Overlord.
54
55 Ghost provide terminal/shell/logcat functionality and manages the client
56 side connectivity.
57 """
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080058 NONE, AGENT, TERMINAL, SHELL, LOGCAT = range(5)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080059
60 MODE_NAME = {
61 NONE: 'NONE',
62 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080063 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080064 SHELL: 'Shell',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080065 LOGCAT: 'Logcat'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080066 }
67
Wei-Ning Huangaed90452015-03-23 17:50:21 +080068 def __init__(self, overlord_addrs, mode=AGENT, rand_mid=False, sid=None,
69 command=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080070 """Constructor.
71
72 Args:
73 overlord_addrs: a list of possible address of overlord.
74 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangaed90452015-03-23 17:50:21 +080075 rand_mid: whether to use random machine ID or not
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080076 sid: session id. If the connection is requested by overlord, sid should
77 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080078 shell: the command to execute when we are in SHELL mode.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080079 """
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080080 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL]
81 if mode == Ghost.SHELL:
82 assert command is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080083
84 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +080085 self._connected_addr = None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080086 self._mode = mode
Wei-Ning Huangaed90452015-03-23 17:50:21 +080087 self._rand_mid = rand_mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080088 self._sock = None
89 self._machine_id = self.GetMachineID()
90 self._client_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +080091 self._properties = {}
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080092 self._shell_command = command
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080093 self._buf = ''
94 self._requests = {}
Wei-Ning Huang2132de32015-04-13 17:24:38 +080095 self._reset = threading.Event()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080096 self._last_ping = 0
97 self._queue = Queue.Queue()
98
Wei-Ning Huang7d029b12015-03-06 10:32:15 +080099 def LoadPropertiesFromFile(self, filename):
100 try:
101 with open(filename, 'r') as f:
102 self._properties = json.loads(f.read())
103 except Exception as e:
104 logging.exception('LoadPropertiesFromFile: ' + str(e))
105
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800106 def SpawnGhost(self, mode, sid, command=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800107 """Spawn a child ghost with specific mode.
108
109 Returns:
110 The spawned child process pid.
111 """
112 pid = os.fork()
113 if pid == 0:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800114 g = Ghost([self._connected_addr], mode, True, sid, command)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800115 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800116 sys.exit(0)
117 else:
118 return pid
119
120 def Timestamp(self):
121 return int(time.time())
122
123 def GetGateWayIP(self):
124 with open('/proc/net/route', 'r') as f:
125 lines = f.readlines()
126
127 ips = []
128 for line in lines:
129 parts = line.split('\t')
130 if parts[2] == '00000000':
131 continue
132
133 try:
134 h = parts[2].decode('hex')
135 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
136 except TypeError:
137 pass
138
139 return ips
140
141 def GetMachineID(self):
142 """Generates machine-dependent ID string for a machine.
143 There are many ways to generate a machine ID:
144 1. factory device-data
145 2. /sys/class/dmi/id/product_uuid (only available on intel machines)
146 3. MAC address
147 We follow the listed order to generate machine ID, and fallback to the next
148 alternative if the previous doesn't work.
149 """
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800150 if self._rand_mid:
151 return str(uuid.uuid4())
152
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800153 try:
154 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
155 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800156 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800157 stdout, _ = p.communicate()
158 if stdout == '':
159 raise RuntimeError("empty mlb number")
160 return stdout.strip()
161 except Exception:
162 pass
163
164 try:
165 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
166 return f.read().strip()
167 except Exception:
168 pass
169
170 try:
171 macs = []
172 ifaces = sorted(os.listdir('/sys/class/net'))
173 for iface in ifaces:
174 if iface == 'lo':
175 continue
176
177 with open('/sys/class/net/%s/address' % iface, 'r') as f:
178 macs.append(f.read().strip())
179
180 return ';'.join(macs)
181 except Exception:
182 pass
183
184 raise RuntimeError("can't generate machine ID")
185
186 def Reset(self):
187 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800188 self._reset.clear()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800189 self._buf = ""
190 self._last_ping = 0
191 self._requests = {}
192
193 def SendMessage(self, msg):
194 """Serialize the message and send it through the socket."""
195 self._sock.send(json.dumps(msg) + _SEPARATOR)
196
197 def SendRequest(self, name, args, handler=None,
198 timeout=_REQUEST_TIMEOUT_SECS):
199 if handler and not callable(handler):
200 raise RequestError('Invalid requiest handler for msg "%s"' % name)
201
202 rid = str(uuid.uuid4())
203 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
204 self._requests[rid] = [self.Timestamp(), timeout, handler]
205 self.SendMessage(msg)
206
207 def SendResponse(self, omsg, status, params=None):
208 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
209 self.SendMessage(msg)
210
211 def SpawnPTYServer(self, _):
212 """Spawn a PTY server and forward I/O to the TCP socket."""
213 logging.info('SpawnPTYServer: started')
214
215 pid, fd = os.forkpty()
216 if pid == 0:
217 env = os.environ.copy()
218 env['USER'] = os.getenv('USER', 'root')
219 env['HOME'] = os.getenv('HOME', '/root')
220 os.chdir(env['HOME'])
221 os.execve(_SHELL, [_SHELL], env)
222 else:
223 try:
224 while True:
225 rd, _, _ = select.select([self._sock, fd], [], [])
226
227 if fd in rd:
228 self._sock.send(os.read(fd, _BUFSIZE))
229
230 if self._sock in rd:
231 ret = self._sock.recv(_BUFSIZE)
232 if len(ret) == 0:
233 raise RuntimeError("socket closed")
234 os.write(fd, ret)
235 except (OSError, socket.error, RuntimeError):
236 self._sock.close()
237 logging.info('SpawnPTYServer: terminated')
238 sys.exit(0)
239
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800240 def SpawnShellServer(self, _):
241 """Spawn a shell server and forward input/output from/to the TCP socket."""
242 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800243
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800244 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800245 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
246 shell=True)
247
248 def make_non_block(fd):
249 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
250 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
251
252 make_non_block(p.stdout)
253 make_non_block(p.stderr)
254
255 try:
256 while True:
257 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800258 p.poll()
259
260 if p.returncode != None:
261 raise RuntimeError("process complete")
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800262
263 if p.stdout in rd:
264 self._sock.send(p.stdout.read(_BUFSIZE))
265
266 if p.stderr in rd:
267 self._sock.send(p.stderr.read(_BUFSIZE))
268
269 if self._sock in rd:
270 ret = self._sock.recv(_BUFSIZE)
271 if len(ret) == 0:
272 raise RuntimeError("socket closed")
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800273 p.stdin.write(ret)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800274 except (OSError, socket.error, RuntimeError):
275 self._sock.close()
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800276 logging.info('SpawnShellServer: terminated')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800277 sys.exit(0)
278
279
280 def Ping(self):
281 def timeout_handler(x):
282 if x is None:
283 raise PingTimeoutError
284
285 self._last_ping = self.Timestamp()
286 self.SendRequest('ping', {}, timeout_handler, 5)
287
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800288 def HandleRequest(self, msg):
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800289 if msg['name'] == 'terminal':
290 self.SpawnGhost(self.TERMINAL, msg['params']['sid'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800291 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800292 elif msg['name'] == 'shell':
293 self.SpawnGhost(self.SHELL, msg['params']['sid'],
294 msg['params']['command'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800295 self.SendResponse(msg, RESPONSE_SUCCESS)
296
297 def HandleResponse(self, response):
298 rid = str(response['rid'])
299 if rid in self._requests:
300 handler = self._requests[rid][2]
301 del self._requests[rid]
302 if callable(handler):
303 handler(response)
304 else:
305 print(response, self._requests.keys())
306 logging.warning('Recvied unsolicited response, ignored')
307
308 def ParseMessage(self):
309 msgs_json = self._buf.split(_SEPARATOR)
310 self._buf = msgs_json.pop()
311
312 for msg_json in msgs_json:
313 try:
314 msg = json.loads(msg_json)
315 except ValueError:
316 # Ignore mal-formed message.
317 continue
318
319 if 'name' in msg:
320 self.HandleRequest(msg)
321 elif 'response' in msg:
322 self.HandleResponse(msg)
323 else: # Ingnore mal-formed message.
324 pass
325
326 def ScanForTimeoutRequests(self):
327 for rid in self._requests.keys()[:]:
328 request_time, timeout, handler = self._requests[rid]
329 if self.Timestamp() - request_time > timeout:
330 handler(None)
331 del self._requests[rid]
332
333 def Listen(self):
334 try:
335 while True:
336 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
337
338 if self._sock in rds:
339 self._buf += self._sock.recv(_BUFSIZE)
340 self.ParseMessage()
341
342 if self.Timestamp() - self._last_ping > _PING_INTERVAL:
343 self.Ping()
344 self.ScanForTimeoutRequests()
345
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800346 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800347 self.Reset()
348 break
349 except socket.error:
350 raise RuntimeError('Connection dropped')
351 except PingTimeoutError:
352 raise RuntimeError('Connection timeout')
353 finally:
354 self._sock.close()
355
356 self._queue.put('resume')
357
358 if self._mode != Ghost.AGENT:
359 sys.exit(1)
360
361 def Register(self):
362 non_local = {}
363 for addr in self._overlord_addrs:
364 non_local['addr'] = addr
365 def registered(response):
366 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800367 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800368 raise RuntimeError('Register request timeout')
369 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
370 self._queue.put("pause", True)
371
372 try:
373 logging.info('Trying %s:%d ...', *addr)
374 self.Reset()
375 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
376 self._sock.settimeout(_PING_TIMEOUT)
377 self._sock.connect(addr)
378
379 logging.info('Connection established, registering...')
380 handler = {
381 Ghost.AGENT: registered,
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800382 Ghost.TERMINAL: self.SpawnPTYServer,
383 Ghost.SHELL: self.SpawnShellServer
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800384 }[self._mode]
385
386 # Machine ID may change if MAC address is used (USB-ethernet dongle
387 # plugged/unplugged)
388 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800389 self.SendRequest('register',
390 {'mode': self._mode, 'mid': self._machine_id,
391 'cid': self._client_id,
392 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800393 except socket.error:
394 pass
395 else:
396 self._sock.settimeout(None)
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800397 self._connected_addr = addr
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800398 self.Listen()
399
400 raise RuntimeError("Cannot connect to any server")
401
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800402 def Reconnect(self):
403 logging.info('Received reconnect request from RPC server, reconnecting...')
404 self._reset.set()
405
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800406 def StartLanDiscovery(self):
407 """Start to listen to LAN discovery packet at
408 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800409
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800410 def thread_func():
411 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
412 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
413 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800414 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800415 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
416 except socket.error as e:
417 logging.error("LAN discovery: %s, abort", e)
418 return
419
420 logging.info('LAN Discovery: started')
421 while True:
422 rd, _, _ = select.select([s], [], [], 1)
423
424 if s in rd:
425 data, source_addr = s.recvfrom(_BUFSIZE)
426 parts = data.split()
427 if parts[0] == 'OVERLORD':
428 ip, port = parts[1].split(':')
429 if not ip:
430 ip = source_addr[0]
431 self._queue.put((ip, int(port)), True)
432
433 try:
434 obj = self._queue.get(False)
435 except Queue.Empty:
436 pass
437 else:
438 if type(obj) is not str:
439 self._queue.put(obj)
440 elif obj == 'pause':
441 logging.info('LAN Discovery: paused')
442 while obj != 'resume':
443 obj = self._queue.get(True)
444 logging.info('LAN Discovery: resumed')
445
446 t = threading.Thread(target=thread_func)
447 t.daemon = True
448 t.start()
449
450 def StartRPCServer(self):
451 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
452 logRequests=False)
453 rpc_server.register_function(self.Reconnect, 'Reconnect')
454 t = threading.Thread(target=rpc_server.serve_forever)
455 t.daemon = True
456 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800457
458 def ScanGateway(self):
459 for addr in [(x, _OVERLORD_PORT) for x in self.GetGateWayIP()]:
460 if addr not in self._overlord_addrs:
461 self._overlord_addrs.append(addr)
462
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800463 def Start(self, lan_disc=False, rpc_server=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800464 logging.info('%s started', self.MODE_NAME[self._mode])
465 logging.info('MID: %s', self._machine_id)
466 logging.info('CID: %s', self._client_id)
467
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800468 if lan_disc:
469 self.StartLanDiscovery()
470
471 if rpc_server:
472 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800473
474 try:
475 while True:
476 try:
477 addr = self._queue.get(False)
478 except Queue.Empty:
479 pass
480 else:
481 if type(addr) == tuple and addr not in self._overlord_addrs:
482 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
483 self._overlord_addrs.append(addr)
484
485 try:
486 self.ScanGateway()
487 self.Register()
488 except Exception as e:
489 logging.info(str(e) + ', retrying in %ds' % _RETRY_INTERVAL)
490 time.sleep(_RETRY_INTERVAL)
491
492 self.Reset()
493 except KeyboardInterrupt:
494 logging.error('Received keyboard interrupt, quit')
495 sys.exit(0)
496
497
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800498def GhostRPCServer():
499 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
500
501
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800502def main():
503 logger = logging.getLogger()
504 logger.setLevel(logging.INFO)
505
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800506 parser = argparse.ArgumentParser()
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800507 parser.add_argument('--rand-mid', dest='rand_mid', action='store_true',
508 default=False, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800509 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
510 default=True, help='disable LAN discovery')
511 parser.add_argument('--no-rpc-server', dest='rpc_server',
512 action='store_false', default=True,
513 help='disable RPC server')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800514 parser.add_argument("--prop-file", dest="prop_file", type=str, default=None,
515 help='file containing the JSON representation of client '
516 'properties')
517 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
518 nargs='*', help='overlord server address')
519 args = parser.parse_args()
520
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800521 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800522 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800523
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800524 g = Ghost(addrs, Ghost.AGENT, args.rand_mid)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800525 if args.prop_file:
526 g.LoadPropertiesFromFile(args.prop_file)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800527 g.Start(args.lan_disc, args.rpc_server)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800528
529
530if __name__ == '__main__':
531 main()