blob: 51b3dee83c87d31f512ee88284666b3b56fedfe9 [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 Huangc9c97f02015-05-19 15:05:42 +080068 RANDOM_MID = '##random_mid##'
69
70 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None,
Wei-Ning Huangaed90452015-03-23 17:50:21 +080071 command=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080072 """Constructor.
73
74 Args:
75 overlord_addrs: a list of possible address of overlord.
76 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +080077 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
78 id is randomly generated.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080079 sid: session id. If the connection is requested by overlord, sid should
80 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080081 shell: the command to execute when we are in SHELL mode.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080082 """
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080083 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL]
84 if mode == Ghost.SHELL:
85 assert command is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080086
87 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +080088 self._connected_addr = None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080089 self._mode = mode
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +080090 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080091 self._sock = None
92 self._machine_id = self.GetMachineID()
93 self._client_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +080094 self._properties = {}
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080095 self._shell_command = command
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080096 self._buf = ''
97 self._requests = {}
Wei-Ning Huang2132de32015-04-13 17:24:38 +080098 self._reset = threading.Event()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080099 self._last_ping = 0
100 self._queue = Queue.Queue()
101
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800102 def LoadPropertiesFromFile(self, filename):
103 try:
104 with open(filename, 'r') as f:
105 self._properties = json.loads(f.read())
106 except Exception as e:
107 logging.exception('LoadPropertiesFromFile: ' + str(e))
108
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800109 def SpawnGhost(self, mode, sid, command=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800110 """Spawn a child ghost with specific mode.
111
112 Returns:
113 The spawned child process pid.
114 """
115 pid = os.fork()
116 if pid == 0:
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800117 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid, command)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800118 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800119 sys.exit(0)
120 else:
121 return pid
122
123 def Timestamp(self):
124 return int(time.time())
125
126 def GetGateWayIP(self):
127 with open('/proc/net/route', 'r') as f:
128 lines = f.readlines()
129
130 ips = []
131 for line in lines:
132 parts = line.split('\t')
133 if parts[2] == '00000000':
134 continue
135
136 try:
137 h = parts[2].decode('hex')
138 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
139 except TypeError:
140 pass
141
142 return ips
143
144 def GetMachineID(self):
145 """Generates machine-dependent ID string for a machine.
146 There are many ways to generate a machine ID:
147 1. factory device-data
148 2. /sys/class/dmi/id/product_uuid (only available on intel machines)
149 3. MAC address
150 We follow the listed order to generate machine ID, and fallback to the next
151 alternative if the previous doesn't work.
152 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800153 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800154 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800155 elif self._mid:
156 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800157
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800158 try:
159 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
160 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800161 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800162 stdout, _ = p.communicate()
163 if stdout == '':
164 raise RuntimeError("empty mlb number")
165 return stdout.strip()
166 except Exception:
167 pass
168
169 try:
170 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
171 return f.read().strip()
172 except Exception:
173 pass
174
175 try:
176 macs = []
177 ifaces = sorted(os.listdir('/sys/class/net'))
178 for iface in ifaces:
179 if iface == 'lo':
180 continue
181
182 with open('/sys/class/net/%s/address' % iface, 'r') as f:
183 macs.append(f.read().strip())
184
185 return ';'.join(macs)
186 except Exception:
187 pass
188
189 raise RuntimeError("can't generate machine ID")
190
191 def Reset(self):
192 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800193 self._reset.clear()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800194 self._buf = ""
195 self._last_ping = 0
196 self._requests = {}
197
198 def SendMessage(self, msg):
199 """Serialize the message and send it through the socket."""
200 self._sock.send(json.dumps(msg) + _SEPARATOR)
201
202 def SendRequest(self, name, args, handler=None,
203 timeout=_REQUEST_TIMEOUT_SECS):
204 if handler and not callable(handler):
205 raise RequestError('Invalid requiest handler for msg "%s"' % name)
206
207 rid = str(uuid.uuid4())
208 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
209 self._requests[rid] = [self.Timestamp(), timeout, handler]
210 self.SendMessage(msg)
211
212 def SendResponse(self, omsg, status, params=None):
213 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
214 self.SendMessage(msg)
215
216 def SpawnPTYServer(self, _):
217 """Spawn a PTY server and forward I/O to the TCP socket."""
218 logging.info('SpawnPTYServer: started')
219
220 pid, fd = os.forkpty()
221 if pid == 0:
222 env = os.environ.copy()
223 env['USER'] = os.getenv('USER', 'root')
224 env['HOME'] = os.getenv('HOME', '/root')
225 os.chdir(env['HOME'])
226 os.execve(_SHELL, [_SHELL], env)
227 else:
228 try:
229 while True:
230 rd, _, _ = select.select([self._sock, fd], [], [])
231
232 if fd in rd:
233 self._sock.send(os.read(fd, _BUFSIZE))
234
235 if self._sock in rd:
236 ret = self._sock.recv(_BUFSIZE)
237 if len(ret) == 0:
238 raise RuntimeError("socket closed")
239 os.write(fd, ret)
240 except (OSError, socket.error, RuntimeError):
241 self._sock.close()
242 logging.info('SpawnPTYServer: terminated')
243 sys.exit(0)
244
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800245 def SpawnShellServer(self, _):
246 """Spawn a shell server and forward input/output from/to the TCP socket."""
247 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800248
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800249 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800250 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
251 shell=True)
252
253 def make_non_block(fd):
254 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
255 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
256
257 make_non_block(p.stdout)
258 make_non_block(p.stderr)
259
260 try:
261 while True:
262 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800263 p.poll()
264
265 if p.returncode != None:
266 raise RuntimeError("process complete")
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800267
268 if p.stdout in rd:
269 self._sock.send(p.stdout.read(_BUFSIZE))
270
271 if p.stderr in rd:
272 self._sock.send(p.stderr.read(_BUFSIZE))
273
274 if self._sock in rd:
275 ret = self._sock.recv(_BUFSIZE)
276 if len(ret) == 0:
277 raise RuntimeError("socket closed")
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800278 p.stdin.write(ret)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800279 except (OSError, socket.error, RuntimeError):
280 self._sock.close()
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800281 logging.info('SpawnShellServer: terminated')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800282 sys.exit(0)
283
284
285 def Ping(self):
286 def timeout_handler(x):
287 if x is None:
288 raise PingTimeoutError
289
290 self._last_ping = self.Timestamp()
291 self.SendRequest('ping', {}, timeout_handler, 5)
292
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800293 def HandleRequest(self, msg):
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800294 if msg['name'] == 'terminal':
295 self.SpawnGhost(self.TERMINAL, msg['params']['sid'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800296 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800297 elif msg['name'] == 'shell':
298 self.SpawnGhost(self.SHELL, msg['params']['sid'],
299 msg['params']['command'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800300 self.SendResponse(msg, RESPONSE_SUCCESS)
301
302 def HandleResponse(self, response):
303 rid = str(response['rid'])
304 if rid in self._requests:
305 handler = self._requests[rid][2]
306 del self._requests[rid]
307 if callable(handler):
308 handler(response)
309 else:
310 print(response, self._requests.keys())
311 logging.warning('Recvied unsolicited response, ignored')
312
313 def ParseMessage(self):
314 msgs_json = self._buf.split(_SEPARATOR)
315 self._buf = msgs_json.pop()
316
317 for msg_json in msgs_json:
318 try:
319 msg = json.loads(msg_json)
320 except ValueError:
321 # Ignore mal-formed message.
322 continue
323
324 if 'name' in msg:
325 self.HandleRequest(msg)
326 elif 'response' in msg:
327 self.HandleResponse(msg)
328 else: # Ingnore mal-formed message.
329 pass
330
331 def ScanForTimeoutRequests(self):
332 for rid in self._requests.keys()[:]:
333 request_time, timeout, handler = self._requests[rid]
334 if self.Timestamp() - request_time > timeout:
335 handler(None)
336 del self._requests[rid]
337
338 def Listen(self):
339 try:
340 while True:
341 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
342
343 if self._sock in rds:
344 self._buf += self._sock.recv(_BUFSIZE)
345 self.ParseMessage()
346
347 if self.Timestamp() - self._last_ping > _PING_INTERVAL:
348 self.Ping()
349 self.ScanForTimeoutRequests()
350
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800351 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800352 self.Reset()
353 break
354 except socket.error:
355 raise RuntimeError('Connection dropped')
356 except PingTimeoutError:
357 raise RuntimeError('Connection timeout')
358 finally:
359 self._sock.close()
360
361 self._queue.put('resume')
362
363 if self._mode != Ghost.AGENT:
364 sys.exit(1)
365
366 def Register(self):
367 non_local = {}
368 for addr in self._overlord_addrs:
369 non_local['addr'] = addr
370 def registered(response):
371 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800372 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800373 raise RuntimeError('Register request timeout')
374 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
375 self._queue.put("pause", True)
376
377 try:
378 logging.info('Trying %s:%d ...', *addr)
379 self.Reset()
380 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
381 self._sock.settimeout(_PING_TIMEOUT)
382 self._sock.connect(addr)
383
384 logging.info('Connection established, registering...')
385 handler = {
386 Ghost.AGENT: registered,
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800387 Ghost.TERMINAL: self.SpawnPTYServer,
388 Ghost.SHELL: self.SpawnShellServer
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800389 }[self._mode]
390
391 # Machine ID may change if MAC address is used (USB-ethernet dongle
392 # plugged/unplugged)
393 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800394 self.SendRequest('register',
395 {'mode': self._mode, 'mid': self._machine_id,
396 'cid': self._client_id,
397 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800398 except socket.error:
399 pass
400 else:
401 self._sock.settimeout(None)
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800402 self._connected_addr = addr
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800403 self.Listen()
404
405 raise RuntimeError("Cannot connect to any server")
406
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800407 def Reconnect(self):
408 logging.info('Received reconnect request from RPC server, reconnecting...')
409 self._reset.set()
410
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800411 def StartLanDiscovery(self):
412 """Start to listen to LAN discovery packet at
413 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800414
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800415 def thread_func():
416 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
417 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
418 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800419 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800420 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
421 except socket.error as e:
422 logging.error("LAN discovery: %s, abort", e)
423 return
424
425 logging.info('LAN Discovery: started')
426 while True:
427 rd, _, _ = select.select([s], [], [], 1)
428
429 if s in rd:
430 data, source_addr = s.recvfrom(_BUFSIZE)
431 parts = data.split()
432 if parts[0] == 'OVERLORD':
433 ip, port = parts[1].split(':')
434 if not ip:
435 ip = source_addr[0]
436 self._queue.put((ip, int(port)), True)
437
438 try:
439 obj = self._queue.get(False)
440 except Queue.Empty:
441 pass
442 else:
443 if type(obj) is not str:
444 self._queue.put(obj)
445 elif obj == 'pause':
446 logging.info('LAN Discovery: paused')
447 while obj != 'resume':
448 obj = self._queue.get(True)
449 logging.info('LAN Discovery: resumed')
450
451 t = threading.Thread(target=thread_func)
452 t.daemon = True
453 t.start()
454
455 def StartRPCServer(self):
456 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
457 logRequests=False)
458 rpc_server.register_function(self.Reconnect, 'Reconnect')
459 t = threading.Thread(target=rpc_server.serve_forever)
460 t.daemon = True
461 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800462
463 def ScanGateway(self):
464 for addr in [(x, _OVERLORD_PORT) for x in self.GetGateWayIP()]:
465 if addr not in self._overlord_addrs:
466 self._overlord_addrs.append(addr)
467
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800468 def Start(self, lan_disc=False, rpc_server=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800469 logging.info('%s started', self.MODE_NAME[self._mode])
470 logging.info('MID: %s', self._machine_id)
471 logging.info('CID: %s', self._client_id)
472
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800473 if lan_disc:
474 self.StartLanDiscovery()
475
476 if rpc_server:
477 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800478
479 try:
480 while True:
481 try:
482 addr = self._queue.get(False)
483 except Queue.Empty:
484 pass
485 else:
486 if type(addr) == tuple and addr not in self._overlord_addrs:
487 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
488 self._overlord_addrs.append(addr)
489
490 try:
491 self.ScanGateway()
492 self.Register()
493 except Exception as e:
494 logging.info(str(e) + ', retrying in %ds' % _RETRY_INTERVAL)
495 time.sleep(_RETRY_INTERVAL)
496
497 self.Reset()
498 except KeyboardInterrupt:
499 logging.error('Received keyboard interrupt, quit')
500 sys.exit(0)
501
502
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800503def GhostRPCServer():
504 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
505
506
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800507def main():
508 logger = logging.getLogger()
509 logger.setLevel(logging.INFO)
510
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800511 parser = argparse.ArgumentParser()
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800512 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
513 default=None, help='use MID as machine ID')
514 parser.add_argument('--rand-mid', dest='mid', action='store_const',
515 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800516 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
517 default=True, help='disable LAN discovery')
518 parser.add_argument('--no-rpc-server', dest='rpc_server',
519 action='store_false', default=True,
520 help='disable RPC server')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800521 parser.add_argument("--prop-file", dest="prop_file", type=str, default=None,
522 help='file containing the JSON representation of client '
523 'properties')
524 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
525 nargs='*', help='overlord server address')
526 args = parser.parse_args()
527
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800528 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800529 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800530
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800531 g = Ghost(addrs, Ghost.AGENT, args.mid)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800532 if args.prop_file:
533 g.LoadPropertiesFromFile(args.prop_file)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800534 g.Start(args.lan_disc, args.rpc_server)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800535
536
537if __name__ == '__main__':
538 main()