blob: da30d976ae29ff8e983a9709e9635aa5954f5c89 [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
Wei-Ning Huangad330c52015-03-12 20:34:18 +080077 self._connected_addr = None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080078 self._mode = mode
79 self._sock = None
80 self._machine_id = self.GetMachineID()
81 self._client_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +080082 self._properties = {}
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080083 self._shell_command = command
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080084 self._buf = ''
85 self._requests = {}
86 self._reset = False
87 self._last_ping = 0
88 self._queue = Queue.Queue()
89
Wei-Ning Huang7d029b12015-03-06 10:32:15 +080090 def LoadPropertiesFromFile(self, filename):
91 try:
92 with open(filename, 'r') as f:
93 self._properties = json.loads(f.read())
94 except Exception as e:
95 logging.exception('LoadPropertiesFromFile: ' + str(e))
96
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080097 def SpawnGhost(self, mode, sid, command=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080098 """Spawn a child ghost with specific mode.
99
100 Returns:
101 The spawned child process pid.
102 """
103 pid = os.fork()
104 if pid == 0:
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800105 g = Ghost([self._connected_addr], mode, sid, command)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800106 g.Start(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800107 sys.exit(0)
108 else:
109 return pid
110
111 def Timestamp(self):
112 return int(time.time())
113
114 def GetGateWayIP(self):
115 with open('/proc/net/route', 'r') as f:
116 lines = f.readlines()
117
118 ips = []
119 for line in lines:
120 parts = line.split('\t')
121 if parts[2] == '00000000':
122 continue
123
124 try:
125 h = parts[2].decode('hex')
126 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
127 except TypeError:
128 pass
129
130 return ips
131
132 def GetMachineID(self):
133 """Generates machine-dependent ID string for a machine.
134 There are many ways to generate a machine ID:
135 1. factory device-data
136 2. /sys/class/dmi/id/product_uuid (only available on intel machines)
137 3. MAC address
138 We follow the listed order to generate machine ID, and fallback to the next
139 alternative if the previous doesn't work.
140 """
141 try:
142 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
143 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800144 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800145 stdout, _ = p.communicate()
146 if stdout == '':
147 raise RuntimeError("empty mlb number")
148 return stdout.strip()
149 except Exception:
150 pass
151
152 try:
153 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
154 return f.read().strip()
155 except Exception:
156 pass
157
158 try:
159 macs = []
160 ifaces = sorted(os.listdir('/sys/class/net'))
161 for iface in ifaces:
162 if iface == 'lo':
163 continue
164
165 with open('/sys/class/net/%s/address' % iface, 'r') as f:
166 macs.append(f.read().strip())
167
168 return ';'.join(macs)
169 except Exception:
170 pass
171
172 raise RuntimeError("can't generate machine ID")
173
174 def Reset(self):
175 """Reset state and clear request handlers."""
176 self._reset = False
177 self._buf = ""
178 self._last_ping = 0
179 self._requests = {}
180
181 def SendMessage(self, msg):
182 """Serialize the message and send it through the socket."""
183 self._sock.send(json.dumps(msg) + _SEPARATOR)
184
185 def SendRequest(self, name, args, handler=None,
186 timeout=_REQUEST_TIMEOUT_SECS):
187 if handler and not callable(handler):
188 raise RequestError('Invalid requiest handler for msg "%s"' % name)
189
190 rid = str(uuid.uuid4())
191 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
192 self._requests[rid] = [self.Timestamp(), timeout, handler]
193 self.SendMessage(msg)
194
195 def SendResponse(self, omsg, status, params=None):
196 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
197 self.SendMessage(msg)
198
199 def SpawnPTYServer(self, _):
200 """Spawn a PTY server and forward I/O to the TCP socket."""
201 logging.info('SpawnPTYServer: started')
202
203 pid, fd = os.forkpty()
204 if pid == 0:
205 env = os.environ.copy()
206 env['USER'] = os.getenv('USER', 'root')
207 env['HOME'] = os.getenv('HOME', '/root')
208 os.chdir(env['HOME'])
209 os.execve(_SHELL, [_SHELL], env)
210 else:
211 try:
212 while True:
213 rd, _, _ = select.select([self._sock, fd], [], [])
214
215 if fd in rd:
216 self._sock.send(os.read(fd, _BUFSIZE))
217
218 if self._sock in rd:
219 ret = self._sock.recv(_BUFSIZE)
220 if len(ret) == 0:
221 raise RuntimeError("socket closed")
222 os.write(fd, ret)
223 except (OSError, socket.error, RuntimeError):
224 self._sock.close()
225 logging.info('SpawnPTYServer: terminated')
226 sys.exit(0)
227
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800228 def SpawnShellServer(self, _):
229 """Spawn a shell server and forward input/output from/to the TCP socket."""
230 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800231
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800232 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800233 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
234 shell=True)
235
236 def make_non_block(fd):
237 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
238 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
239
240 make_non_block(p.stdout)
241 make_non_block(p.stderr)
242
243 try:
244 while True:
245 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800246 p.poll()
247
248 if p.returncode != None:
249 raise RuntimeError("process complete")
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800250
251 if p.stdout in rd:
252 self._sock.send(p.stdout.read(_BUFSIZE))
253
254 if p.stderr in rd:
255 self._sock.send(p.stderr.read(_BUFSIZE))
256
257 if self._sock in rd:
258 ret = self._sock.recv(_BUFSIZE)
259 if len(ret) == 0:
260 raise RuntimeError("socket closed")
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800261 p.stdin.write(ret)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800262 except (OSError, socket.error, RuntimeError):
263 self._sock.close()
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800264 logging.info('SpawnShellServer: terminated')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800265 sys.exit(0)
266
267
268 def Ping(self):
269 def timeout_handler(x):
270 if x is None:
271 raise PingTimeoutError
272
273 self._last_ping = self.Timestamp()
274 self.SendRequest('ping', {}, timeout_handler, 5)
275
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800276 def HandleRequest(self, msg):
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800277 if msg['name'] == 'terminal':
278 self.SpawnGhost(self.TERMINAL, msg['params']['sid'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800279 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800280 elif msg['name'] == 'shell':
281 self.SpawnGhost(self.SHELL, msg['params']['sid'],
282 msg['params']['command'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800283 self.SendResponse(msg, RESPONSE_SUCCESS)
284
285 def HandleResponse(self, response):
286 rid = str(response['rid'])
287 if rid in self._requests:
288 handler = self._requests[rid][2]
289 del self._requests[rid]
290 if callable(handler):
291 handler(response)
292 else:
293 print(response, self._requests.keys())
294 logging.warning('Recvied unsolicited response, ignored')
295
296 def ParseMessage(self):
297 msgs_json = self._buf.split(_SEPARATOR)
298 self._buf = msgs_json.pop()
299
300 for msg_json in msgs_json:
301 try:
302 msg = json.loads(msg_json)
303 except ValueError:
304 # Ignore mal-formed message.
305 continue
306
307 if 'name' in msg:
308 self.HandleRequest(msg)
309 elif 'response' in msg:
310 self.HandleResponse(msg)
311 else: # Ingnore mal-formed message.
312 pass
313
314 def ScanForTimeoutRequests(self):
315 for rid in self._requests.keys()[:]:
316 request_time, timeout, handler = self._requests[rid]
317 if self.Timestamp() - request_time > timeout:
318 handler(None)
319 del self._requests[rid]
320
321 def Listen(self):
322 try:
323 while True:
324 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
325
326 if self._sock in rds:
327 self._buf += self._sock.recv(_BUFSIZE)
328 self.ParseMessage()
329
330 if self.Timestamp() - self._last_ping > _PING_INTERVAL:
331 self.Ping()
332 self.ScanForTimeoutRequests()
333
334 if self._reset:
335 self.Reset()
336 break
337 except socket.error:
338 raise RuntimeError('Connection dropped')
339 except PingTimeoutError:
340 raise RuntimeError('Connection timeout')
341 finally:
342 self._sock.close()
343
344 self._queue.put('resume')
345
346 if self._mode != Ghost.AGENT:
347 sys.exit(1)
348
349 def Register(self):
350 non_local = {}
351 for addr in self._overlord_addrs:
352 non_local['addr'] = addr
353 def registered(response):
354 if response is None:
355 self._reset = True
356 raise RuntimeError('Register request timeout')
357 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
358 self._queue.put("pause", True)
359
360 try:
361 logging.info('Trying %s:%d ...', *addr)
362 self.Reset()
363 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
364 self._sock.settimeout(_PING_TIMEOUT)
365 self._sock.connect(addr)
366
367 logging.info('Connection established, registering...')
368 handler = {
369 Ghost.AGENT: registered,
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800370 Ghost.TERMINAL: self.SpawnPTYServer,
371 Ghost.SHELL: self.SpawnShellServer
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800372 }[self._mode]
373
374 # Machine ID may change if MAC address is used (USB-ethernet dongle
375 # plugged/unplugged)
376 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800377 self.SendRequest('register',
378 {'mode': self._mode, 'mid': self._machine_id,
379 'cid': self._client_id,
380 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800381 except socket.error:
382 pass
383 else:
384 self._sock.settimeout(None)
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800385 self._connected_addr = addr
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800386 self.Listen()
387
388 raise RuntimeError("Cannot connect to any server")
389
390 def StartLanDiscovery(self):
391 """Start to listen to LAN discovery packet at
392 _OVERLORD_LAN_DISCOVERY_PORT."""
393 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
394 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
395 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
396 try:
397 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
398 except socket.error as e:
399 logging.error("LAN discovery: %s, abort", e)
400 return
401
402 logging.info('LAN Discovery: started')
403 while True:
404 rd, _, _ = select.select([s], [], [], 1)
405
406 if s in rd:
407 data, source_addr = s.recvfrom(_BUFSIZE)
408 parts = data.split()
409 if parts[0] == 'OVERLORD':
410 ip = source_addr[0]
411 port = int(parts[1].lstrip(':'))
412 addr = (ip, port)
413 self._queue.put(addr, True)
414
415 try:
416 obj = self._queue.get(False)
417 except Queue.Empty:
418 pass
419 else:
420 if type(obj) is not str:
421 self._queue.put(obj)
422 elif obj == 'pause':
423 logging.info('LAN Discovery: paused')
424 while True:
425 obj = self._queue.get(True)
426 if obj == 'resume':
427 logging.info('LAN Discovery: resumed')
428 break
429
430 def ScanGateway(self):
431 for addr in [(x, _OVERLORD_PORT) for x in self.GetGateWayIP()]:
432 if addr not in self._overlord_addrs:
433 self._overlord_addrs.append(addr)
434
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800435 def Start(self, no_lan_disc=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800436 logging.info('%s started', self.MODE_NAME[self._mode])
437 logging.info('MID: %s', self._machine_id)
438 logging.info('CID: %s', self._client_id)
439
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800440 if not no_lan_disc:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800441 t = threading.Thread(target=self.StartLanDiscovery)
442 t.daemon = True
443 t.start()
444
445 try:
446 while True:
447 try:
448 addr = self._queue.get(False)
449 except Queue.Empty:
450 pass
451 else:
452 if type(addr) == tuple and addr not in self._overlord_addrs:
453 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
454 self._overlord_addrs.append(addr)
455
456 try:
457 self.ScanGateway()
458 self.Register()
459 except Exception as e:
460 logging.info(str(e) + ', retrying in %ds' % _RETRY_INTERVAL)
461 time.sleep(_RETRY_INTERVAL)
462
463 self.Reset()
464 except KeyboardInterrupt:
465 logging.error('Received keyboard interrupt, quit')
466 sys.exit(0)
467
468
469def main():
470 logger = logging.getLogger()
471 logger.setLevel(logging.INFO)
472
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800473 parser = argparse.ArgumentParser()
474 parser.add_argument('--no-lan-disc', dest='no_lan_disc', action='store_true',
475 default=False, help='disable LAN discovery')
476 parser.add_argument("--prop-file", dest="prop_file", type=str, default=None,
477 help='file containing the JSON representation of client '
478 'properties')
479 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
480 nargs='*', help='overlord server address')
481 args = parser.parse_args()
482
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800483 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800484 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800485
486 g = Ghost(addrs)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800487 if args.prop_file:
488 g.LoadPropertiesFromFile(args.prop_file)
489 g.Start(args.no_lan_disc)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800490
491
492if __name__ == '__main__':
493 main()