blob: 335ab74bedfc7559c2154d52618e740010f74c27 [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 Huangaed90452015-03-23 17:50:21 +080062 def __init__(self, overlord_addrs, mode=AGENT, rand_mid=False, sid=None,
63 command=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080064 """Constructor.
65
66 Args:
67 overlord_addrs: a list of possible address of overlord.
68 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangaed90452015-03-23 17:50:21 +080069 rand_mid: whether to use random machine ID or not
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080070 sid: session id. If the connection is requested by overlord, sid should
71 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080072 shell: the command to execute when we are in SHELL mode.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080073 """
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080074 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL]
75 if mode == Ghost.SHELL:
76 assert command is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080077
78 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +080079 self._connected_addr = None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080080 self._mode = mode
Wei-Ning Huangaed90452015-03-23 17:50:21 +080081 self._rand_mid = rand_mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080082 self._sock = None
83 self._machine_id = self.GetMachineID()
84 self._client_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +080085 self._properties = {}
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080086 self._shell_command = command
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080087 self._buf = ''
88 self._requests = {}
89 self._reset = False
90 self._last_ping = 0
91 self._queue = Queue.Queue()
92
Wei-Ning Huang7d029b12015-03-06 10:32:15 +080093 def LoadPropertiesFromFile(self, filename):
94 try:
95 with open(filename, 'r') as f:
96 self._properties = json.loads(f.read())
97 except Exception as e:
98 logging.exception('LoadPropertiesFromFile: ' + str(e))
99
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800100 def SpawnGhost(self, mode, sid, command=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800101 """Spawn a child ghost with specific mode.
102
103 Returns:
104 The spawned child process pid.
105 """
106 pid = os.fork()
107 if pid == 0:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800108 g = Ghost([self._connected_addr], mode, True, sid, command)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800109 g.Start(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800110 sys.exit(0)
111 else:
112 return pid
113
114 def Timestamp(self):
115 return int(time.time())
116
117 def GetGateWayIP(self):
118 with open('/proc/net/route', 'r') as f:
119 lines = f.readlines()
120
121 ips = []
122 for line in lines:
123 parts = line.split('\t')
124 if parts[2] == '00000000':
125 continue
126
127 try:
128 h = parts[2].decode('hex')
129 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
130 except TypeError:
131 pass
132
133 return ips
134
135 def GetMachineID(self):
136 """Generates machine-dependent ID string for a machine.
137 There are many ways to generate a machine ID:
138 1. factory device-data
139 2. /sys/class/dmi/id/product_uuid (only available on intel machines)
140 3. MAC address
141 We follow the listed order to generate machine ID, and fallback to the next
142 alternative if the previous doesn't work.
143 """
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800144 if self._rand_mid:
145 return str(uuid.uuid4())
146
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800147 try:
148 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
149 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800150 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800151 stdout, _ = p.communicate()
152 if stdout == '':
153 raise RuntimeError("empty mlb number")
154 return stdout.strip()
155 except Exception:
156 pass
157
158 try:
159 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
160 return f.read().strip()
161 except Exception:
162 pass
163
164 try:
165 macs = []
166 ifaces = sorted(os.listdir('/sys/class/net'))
167 for iface in ifaces:
168 if iface == 'lo':
169 continue
170
171 with open('/sys/class/net/%s/address' % iface, 'r') as f:
172 macs.append(f.read().strip())
173
174 return ';'.join(macs)
175 except Exception:
176 pass
177
178 raise RuntimeError("can't generate machine ID")
179
180 def Reset(self):
181 """Reset state and clear request handlers."""
182 self._reset = False
183 self._buf = ""
184 self._last_ping = 0
185 self._requests = {}
186
187 def SendMessage(self, msg):
188 """Serialize the message and send it through the socket."""
189 self._sock.send(json.dumps(msg) + _SEPARATOR)
190
191 def SendRequest(self, name, args, handler=None,
192 timeout=_REQUEST_TIMEOUT_SECS):
193 if handler and not callable(handler):
194 raise RequestError('Invalid requiest handler for msg "%s"' % name)
195
196 rid = str(uuid.uuid4())
197 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
198 self._requests[rid] = [self.Timestamp(), timeout, handler]
199 self.SendMessage(msg)
200
201 def SendResponse(self, omsg, status, params=None):
202 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
203 self.SendMessage(msg)
204
205 def SpawnPTYServer(self, _):
206 """Spawn a PTY server and forward I/O to the TCP socket."""
207 logging.info('SpawnPTYServer: started')
208
209 pid, fd = os.forkpty()
210 if pid == 0:
211 env = os.environ.copy()
212 env['USER'] = os.getenv('USER', 'root')
213 env['HOME'] = os.getenv('HOME', '/root')
214 os.chdir(env['HOME'])
215 os.execve(_SHELL, [_SHELL], env)
216 else:
217 try:
218 while True:
219 rd, _, _ = select.select([self._sock, fd], [], [])
220
221 if fd in rd:
222 self._sock.send(os.read(fd, _BUFSIZE))
223
224 if self._sock in rd:
225 ret = self._sock.recv(_BUFSIZE)
226 if len(ret) == 0:
227 raise RuntimeError("socket closed")
228 os.write(fd, ret)
229 except (OSError, socket.error, RuntimeError):
230 self._sock.close()
231 logging.info('SpawnPTYServer: terminated')
232 sys.exit(0)
233
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800234 def SpawnShellServer(self, _):
235 """Spawn a shell server and forward input/output from/to the TCP socket."""
236 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800237
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800238 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800239 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
240 shell=True)
241
242 def make_non_block(fd):
243 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
244 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
245
246 make_non_block(p.stdout)
247 make_non_block(p.stderr)
248
249 try:
250 while True:
251 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800252 p.poll()
253
254 if p.returncode != None:
255 raise RuntimeError("process complete")
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800256
257 if p.stdout in rd:
258 self._sock.send(p.stdout.read(_BUFSIZE))
259
260 if p.stderr in rd:
261 self._sock.send(p.stderr.read(_BUFSIZE))
262
263 if self._sock in rd:
264 ret = self._sock.recv(_BUFSIZE)
265 if len(ret) == 0:
266 raise RuntimeError("socket closed")
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800267 p.stdin.write(ret)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800268 except (OSError, socket.error, RuntimeError):
269 self._sock.close()
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800270 logging.info('SpawnShellServer: terminated')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800271 sys.exit(0)
272
273
274 def Ping(self):
275 def timeout_handler(x):
276 if x is None:
277 raise PingTimeoutError
278
279 self._last_ping = self.Timestamp()
280 self.SendRequest('ping', {}, timeout_handler, 5)
281
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800282 def HandleRequest(self, msg):
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800283 if msg['name'] == 'terminal':
284 self.SpawnGhost(self.TERMINAL, msg['params']['sid'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800285 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800286 elif msg['name'] == 'shell':
287 self.SpawnGhost(self.SHELL, msg['params']['sid'],
288 msg['params']['command'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800289 self.SendResponse(msg, RESPONSE_SUCCESS)
290
291 def HandleResponse(self, response):
292 rid = str(response['rid'])
293 if rid in self._requests:
294 handler = self._requests[rid][2]
295 del self._requests[rid]
296 if callable(handler):
297 handler(response)
298 else:
299 print(response, self._requests.keys())
300 logging.warning('Recvied unsolicited response, ignored')
301
302 def ParseMessage(self):
303 msgs_json = self._buf.split(_SEPARATOR)
304 self._buf = msgs_json.pop()
305
306 for msg_json in msgs_json:
307 try:
308 msg = json.loads(msg_json)
309 except ValueError:
310 # Ignore mal-formed message.
311 continue
312
313 if 'name' in msg:
314 self.HandleRequest(msg)
315 elif 'response' in msg:
316 self.HandleResponse(msg)
317 else: # Ingnore mal-formed message.
318 pass
319
320 def ScanForTimeoutRequests(self):
321 for rid in self._requests.keys()[:]:
322 request_time, timeout, handler = self._requests[rid]
323 if self.Timestamp() - request_time > timeout:
324 handler(None)
325 del self._requests[rid]
326
327 def Listen(self):
328 try:
329 while True:
330 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
331
332 if self._sock in rds:
333 self._buf += self._sock.recv(_BUFSIZE)
334 self.ParseMessage()
335
336 if self.Timestamp() - self._last_ping > _PING_INTERVAL:
337 self.Ping()
338 self.ScanForTimeoutRequests()
339
340 if self._reset:
341 self.Reset()
342 break
343 except socket.error:
344 raise RuntimeError('Connection dropped')
345 except PingTimeoutError:
346 raise RuntimeError('Connection timeout')
347 finally:
348 self._sock.close()
349
350 self._queue.put('resume')
351
352 if self._mode != Ghost.AGENT:
353 sys.exit(1)
354
355 def Register(self):
356 non_local = {}
357 for addr in self._overlord_addrs:
358 non_local['addr'] = addr
359 def registered(response):
360 if response is None:
361 self._reset = True
362 raise RuntimeError('Register request timeout')
363 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
364 self._queue.put("pause", True)
365
366 try:
367 logging.info('Trying %s:%d ...', *addr)
368 self.Reset()
369 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
370 self._sock.settimeout(_PING_TIMEOUT)
371 self._sock.connect(addr)
372
373 logging.info('Connection established, registering...')
374 handler = {
375 Ghost.AGENT: registered,
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800376 Ghost.TERMINAL: self.SpawnPTYServer,
377 Ghost.SHELL: self.SpawnShellServer
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800378 }[self._mode]
379
380 # Machine ID may change if MAC address is used (USB-ethernet dongle
381 # plugged/unplugged)
382 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800383 self.SendRequest('register',
384 {'mode': self._mode, 'mid': self._machine_id,
385 'cid': self._client_id,
386 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800387 except socket.error:
388 pass
389 else:
390 self._sock.settimeout(None)
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800391 self._connected_addr = addr
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800392 self.Listen()
393
394 raise RuntimeError("Cannot connect to any server")
395
396 def StartLanDiscovery(self):
397 """Start to listen to LAN discovery packet at
398 _OVERLORD_LAN_DISCOVERY_PORT."""
399 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
400 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
401 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
402 try:
403 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
404 except socket.error as e:
405 logging.error("LAN discovery: %s, abort", e)
406 return
407
408 logging.info('LAN Discovery: started')
409 while True:
410 rd, _, _ = select.select([s], [], [], 1)
411
412 if s in rd:
413 data, source_addr = s.recvfrom(_BUFSIZE)
414 parts = data.split()
415 if parts[0] == 'OVERLORD':
416 ip = source_addr[0]
417 port = int(parts[1].lstrip(':'))
418 addr = (ip, port)
419 self._queue.put(addr, True)
420
421 try:
422 obj = self._queue.get(False)
423 except Queue.Empty:
424 pass
425 else:
426 if type(obj) is not str:
427 self._queue.put(obj)
428 elif obj == 'pause':
429 logging.info('LAN Discovery: paused')
430 while True:
431 obj = self._queue.get(True)
432 if obj == 'resume':
433 logging.info('LAN Discovery: resumed')
434 break
435
436 def ScanGateway(self):
437 for addr in [(x, _OVERLORD_PORT) for x in self.GetGateWayIP()]:
438 if addr not in self._overlord_addrs:
439 self._overlord_addrs.append(addr)
440
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800441 def Start(self, no_lan_disc=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800442 logging.info('%s started', self.MODE_NAME[self._mode])
443 logging.info('MID: %s', self._machine_id)
444 logging.info('CID: %s', self._client_id)
445
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800446 if not no_lan_disc:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800447 t = threading.Thread(target=self.StartLanDiscovery)
448 t.daemon = True
449 t.start()
450
451 try:
452 while True:
453 try:
454 addr = self._queue.get(False)
455 except Queue.Empty:
456 pass
457 else:
458 if type(addr) == tuple and addr not in self._overlord_addrs:
459 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
460 self._overlord_addrs.append(addr)
461
462 try:
463 self.ScanGateway()
464 self.Register()
465 except Exception as e:
466 logging.info(str(e) + ', retrying in %ds' % _RETRY_INTERVAL)
467 time.sleep(_RETRY_INTERVAL)
468
469 self.Reset()
470 except KeyboardInterrupt:
471 logging.error('Received keyboard interrupt, quit')
472 sys.exit(0)
473
474
475def main():
476 logger = logging.getLogger()
477 logger.setLevel(logging.INFO)
478
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800479 parser = argparse.ArgumentParser()
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800480 parser.add_argument('--rand-mid', dest='rand_mid', action='store_true',
481 default=False, help='use random machine ID')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800482 parser.add_argument('--no-lan-disc', dest='no_lan_disc', action='store_true',
483 default=False, help='disable LAN discovery')
484 parser.add_argument("--prop-file", dest="prop_file", type=str, default=None,
485 help='file containing the JSON representation of client '
486 'properties')
487 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
488 nargs='*', help='overlord server address')
489 args = parser.parse_args()
490
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800491 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800492 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800493
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800494 g = Ghost(addrs, Ghost.AGENT, args.rand_mid)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800495 if args.prop_file:
496 g.LoadPropertiesFromFile(args.prop_file)
497 g.Start(args.no_lan_disc)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800498
499
500if __name__ == '__main__':
501 main()