blob: ad4b5bebced2e49a3bfdb0989508d624e946dc0b [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 Huangb05cde32015-08-01 09:48:41 +08009import contextlib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080010import fcntl
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080011import hashlib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080012import json
13import logging
14import os
15import Queue
Wei-Ning Huang829e0c82015-05-26 14:37:23 +080016import re
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080017import select
Wei-Ning Huanga301f572015-06-03 17:34:21 +080018import signal
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080019import socket
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080020import struct
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080021import subprocess
22import sys
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080023import termios
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080024import threading
25import time
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080026import urllib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080027import uuid
28
Wei-Ning Huang2132de32015-04-13 17:24:38 +080029import jsonrpclib
30from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
31
32
33_GHOST_RPC_PORT = 4499
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080034
35_OVERLORD_PORT = 4455
36_OVERLORD_LAN_DISCOVERY_PORT = 4456
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080037_OVERLORD_HTTP_PORT = 9000
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080038
39_BUFSIZE = 8192
40_RETRY_INTERVAL = 2
41_SEPARATOR = '\r\n'
42_PING_TIMEOUT = 3
43_PING_INTERVAL = 5
44_REQUEST_TIMEOUT_SECS = 60
45_SHELL = os.getenv('SHELL', '/bin/bash')
Wei-Ning Huang2132de32015-04-13 17:24:38 +080046_DEFAULT_BIND_ADDRESS = '0.0.0.0'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080047
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080048_CONTROL_START = 128
49_CONTROL_END = 129
50
Wei-Ning Huanga301f572015-06-03 17:34:21 +080051_BLOCK_SIZE = 4096
52
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080053RESPONSE_SUCCESS = 'success'
54RESPONSE_FAILED = 'failed'
55
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080056class PingTimeoutError(Exception):
57 pass
58
59
60class RequestError(Exception):
61 pass
62
63
64class Ghost(object):
65 """Ghost implements the client protocol of Overlord.
66
67 Ghost provide terminal/shell/logcat functionality and manages the client
68 side connectivity.
69 """
Wei-Ning Huanga301f572015-06-03 17:34:21 +080070 NONE, AGENT, TERMINAL, SHELL, LOGCAT, FILE = range(6)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080071
72 MODE_NAME = {
73 NONE: 'NONE',
74 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080075 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080076 SHELL: 'Shell',
Wei-Ning Huanga301f572015-06-03 17:34:21 +080077 LOGCAT: 'Logcat',
78 FILE: 'File'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080079 }
80
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +080081 RANDOM_MID = '##random_mid##'
82
Wei-Ning Huanga301f572015-06-03 17:34:21 +080083 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None, bid=None,
84 command=None, file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080085 """Constructor.
86
87 Args:
88 overlord_addrs: a list of possible address of overlord.
89 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +080090 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
91 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +080092 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080093 be set to the corresponding session id assigned by overlord.
Wei-Ning Huanga301f572015-06-03 17:34:21 +080094 bid: browser ID. Identifies the browser which started the session.
95 command: the command to execute when we are in SHELL mode.
96 file_op: a tuple (action, filepath). action is either 'download' or
97 'upload'.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080098 """
Wei-Ning Huanga301f572015-06-03 17:34:21 +080099 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800100 if mode == Ghost.SHELL:
101 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800102 if mode == Ghost.FILE:
103 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800104
105 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800106 self._connected_addr = None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800107 self._mode = mode
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800108 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800109 self._sock = None
110 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800111 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800112 self._browser_id = bid
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800113 self._properties = {}
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800114 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800115 self._file_op = file_op
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800116 self._buf = ''
117 self._requests = {}
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800118 self._reset = threading.Event()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800119 self._last_ping = 0
120 self._queue = Queue.Queue()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800121 self._download_queue = Queue.Queue()
122 self._session_map = {} # Stores the mapping between ttyname and browser_id
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800123
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800124 def SetIgnoreChild(self, status):
125 # Only ignore child for Agent since only it could spawn child Ghost.
126 if self._mode == Ghost.AGENT:
127 signal.signal(signal.SIGCHLD,
128 signal.SIG_IGN if status else signal.SIG_DFL)
129
130 def GetFileSha1(self, filename):
131 with open(filename, 'r') as f:
132 return hashlib.sha1(f.read()).hexdigest()
133
134 def Upgrade(self):
135 logging.info('Upgrade: initiating upgrade sequence...')
136
137 scriptpath = os.path.abspath(sys.argv[0])
138 url = 'http://%s:%d/upgrade/ghost.py' % (
139 self._connected_addr[0], _OVERLORD_HTTP_PORT)
140
141 # Download sha1sum for ghost.py for verification
142 try:
143 with contextlib.closing(urllib.urlopen(url + '.sha1')) as f:
144 if f.getcode() != 200:
145 raise RuntimeError('HTTP status %d' % f.getcode())
146 sha1sum = f.read().strip()
147 except Exception:
148 logging.error('Upgrade: failed to download sha1sum file, abort')
149 return
150
151 if self.GetFileSha1(scriptpath) == sha1sum:
152 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
153 return
154
155 # Download upgrade version of ghost.py
156 try:
157 with contextlib.closing(urllib.urlopen(url)) as f:
158 if f.getcode() != 200:
159 raise RuntimeError('HTTP status %d' % f.getcode())
160 data = f.read()
161 except Exception:
162 logging.error('Upgrade: failed to download upgrade, abort')
163 return
164
165 # Compare SHA1 sum
166 if hashlib.sha1(data).hexdigest() != sha1sum:
167 logging.error('Upgrade: sha1sum mismatch, abort')
168 return
169
170 python = os.readlink('/proc/self/exe')
171 try:
172 with open(scriptpath, 'w') as f:
173 f.write(data)
174 except Exception:
175 logging.error('Upgrade: failed to write upgrade onto disk, abort')
176 return
177
178 logging.info('Upgrade: restarting ghost...')
179 self.CloseSockets()
180 self.SetIgnoreChild(False)
181 os.execve(python, [python, scriptpath] + sys.argv[1:], os.environ)
182
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800183 def LoadPropertiesFromFile(self, filename):
184 try:
185 with open(filename, 'r') as f:
186 self._properties = json.loads(f.read())
187 except Exception as e:
188 logging.exception('LoadPropertiesFromFile: ' + str(e))
189
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800190 def CloseSockets(self):
191 # Close sockets opened by parent process, since we don't use it anymore.
192 for fd in os.listdir('/proc/self/fd/'):
193 try:
194 real_fd = os.readlink('/proc/self/fd/%s' % fd)
195 if real_fd.startswith('socket'):
196 os.close(int(fd))
197 except Exception:
198 pass
199
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800200 def SpawnGhost(self, mode, sid=None, bid=None, command=None, file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800201 """Spawn a child ghost with specific mode.
202
203 Returns:
204 The spawned child process pid.
205 """
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800206 # Restore the default signal hanlder, so our child won't have problems.
207 self.SetIgnoreChild(False)
208
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800209 pid = os.fork()
210 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800211 self.CloseSockets()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800212 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid, bid,
213 command, file_op)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800214 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800215 sys.exit(0)
216 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800217 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800218 return pid
219
220 def Timestamp(self):
221 return int(time.time())
222
223 def GetGateWayIP(self):
224 with open('/proc/net/route', 'r') as f:
225 lines = f.readlines()
226
227 ips = []
228 for line in lines:
229 parts = line.split('\t')
230 if parts[2] == '00000000':
231 continue
232
233 try:
234 h = parts[2].decode('hex')
235 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
236 except TypeError:
237 pass
238
239 return ips
240
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800241 def GetShopfloorIP(self):
242 try:
243 import factory_common # pylint: disable=W0612
244 from cros.factory.test import shopfloor
245
246 url = shopfloor.get_server_url()
247 match = re.match(r'^https?://(.*):.*$', url)
248 if match:
249 return [match.group(1)]
250 except Exception:
251 pass
252 return []
253
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800254 def GetMachineID(self):
255 """Generates machine-dependent ID string for a machine.
256 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800257 1. factory device_id
258 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800259 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
260 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800261 We follow the listed order to generate machine ID, and fallback to the next
262 alternative if the previous doesn't work.
263 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800264 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800265 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800266 elif self._mid:
267 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800268
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800269 # Try factory device id
270 try:
271 import factory_common # pylint: disable=W0612
272 from cros.factory.test import event_log
273 with open(event_log.DEVICE_ID_PATH) as f:
274 return f.read().strip()
275 except Exception:
276 pass
277
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800278 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800279 try:
280 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
281 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800282 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800283 stdout, _ = p.communicate()
284 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800285 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800286 return stdout.strip()
287 except Exception:
288 pass
289
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800290 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800291 try:
292 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
293 return f.read().strip()
294 except Exception:
295 pass
296
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800297 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800298 try:
299 macs = []
300 ifaces = sorted(os.listdir('/sys/class/net'))
301 for iface in ifaces:
302 if iface == 'lo':
303 continue
304
305 with open('/sys/class/net/%s/address' % iface, 'r') as f:
306 macs.append(f.read().strip())
307
308 return ';'.join(macs)
309 except Exception:
310 pass
311
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800312 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800313
314 def Reset(self):
315 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800316 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800317 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800318 self._last_ping = 0
319 self._requests = {}
320
321 def SendMessage(self, msg):
322 """Serialize the message and send it through the socket."""
323 self._sock.send(json.dumps(msg) + _SEPARATOR)
324
325 def SendRequest(self, name, args, handler=None,
326 timeout=_REQUEST_TIMEOUT_SECS):
327 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800328 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800329
330 rid = str(uuid.uuid4())
331 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
332 self._requests[rid] = [self.Timestamp(), timeout, handler]
333 self.SendMessage(msg)
334
335 def SendResponse(self, omsg, status, params=None):
336 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
337 self.SendMessage(msg)
338
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800339 def HandlePTYControl(self, fd, control_string):
340 msg = json.loads(control_string)
341 command = msg['command']
342 params = msg['params']
343 if command == 'resize':
344 # some error happened on websocket
345 if len(params) != 2:
346 return
347 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
348 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
349 else:
350 logging.warn('Invalid request command "%s"', command)
351
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800352 def SpawnPTYServer(self, _):
353 """Spawn a PTY server and forward I/O to the TCP socket."""
354 logging.info('SpawnPTYServer: started')
355
356 pid, fd = os.forkpty()
357 if pid == 0:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800358 # Register the mapping of browser_id and ttyname
359 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
360 try:
361 server = GhostRPCServer()
362 server.RegisterTTY(self._browser_id, ttyname)
363 except Exception:
364 # If ghost is launched without RPC server, the call will fail but we
365 # can ignore it.
366 pass
367
368 # The directory that contains the current running ghost script
369 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
370
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800371 env = os.environ.copy()
372 env['USER'] = os.getenv('USER', 'root')
373 env['HOME'] = os.getenv('HOME', '/root')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800374 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800375 os.chdir(env['HOME'])
376 os.execve(_SHELL, [_SHELL], env)
377 else:
378 try:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800379 control_state = None
380 control_string = ''
381 write_buffer = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800382 while True:
383 rd, _, _ = select.select([self._sock, fd], [], [])
384
385 if fd in rd:
386 self._sock.send(os.read(fd, _BUFSIZE))
387
388 if self._sock in rd:
389 ret = self._sock.recv(_BUFSIZE)
390 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800391 raise RuntimeError('socket closed')
392 while ret:
393 if control_state:
394 if chr(_CONTROL_END) in ret:
395 index = ret.index(chr(_CONTROL_END))
396 control_string += ret[:index]
397 self.HandlePTYControl(fd, control_string)
398 control_state = None
399 control_string = ''
400 ret = ret[index+1:]
401 else:
402 control_string += ret
403 ret = ''
404 else:
405 if chr(_CONTROL_START) in ret:
406 control_state = _CONTROL_START
407 index = ret.index(chr(_CONTROL_START))
408 write_buffer += ret[:index]
409 ret = ret[index+1:]
410 else:
411 write_buffer += ret
412 ret = ''
413 if write_buffer:
414 os.write(fd, write_buffer)
415 write_buffer = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800416 except (OSError, socket.error, RuntimeError):
417 self._sock.close()
418 logging.info('SpawnPTYServer: terminated')
419 sys.exit(0)
420
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800421 def SpawnShellServer(self, _):
422 """Spawn a shell server and forward input/output from/to the TCP socket."""
423 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800424
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800425 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800426 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
427 shell=True)
428
429 def make_non_block(fd):
430 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
431 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
432
433 make_non_block(p.stdout)
434 make_non_block(p.stderr)
435
436 try:
437 while True:
438 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800439 p.poll()
440
441 if p.returncode != None:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800442 raise RuntimeError('process complete')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800443
444 if p.stdout in rd:
445 self._sock.send(p.stdout.read(_BUFSIZE))
446
447 if p.stderr in rd:
448 self._sock.send(p.stderr.read(_BUFSIZE))
449
450 if self._sock in rd:
451 ret = self._sock.recv(_BUFSIZE)
452 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800453 raise RuntimeError('socket closed')
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800454 p.stdin.write(ret)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800455 except (OSError, socket.error, RuntimeError):
456 self._sock.close()
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800457 logging.info('SpawnShellServer: terminated')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800458 sys.exit(0)
459
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800460 def InitiateFileOperation(self, _):
461 if self._file_op[0] == 'download':
462 size = os.stat(self._file_op[1]).st_size
463 self.SendRequest('request_to_download',
464 {'bid': self._browser_id,
465 'filename': os.path.basename(self._file_op[1]),
466 'size': size})
467
468 def StartDownloadServer(self):
469 logging.info('StartDownloadServer: started')
470
471 try:
472 with open(self._file_op[1], 'rb') as f:
473 while True:
474 data = f.read(_BLOCK_SIZE)
475 if len(data) == 0:
476 break
477 self._sock.send(data)
478 except Exception as e:
479 logging.error('StartDownloadServer: %s', e)
480 finally:
481 self._sock.close()
482
483 logging.info('StartDownloadServer: terminated')
484 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800485
486 def Ping(self):
487 def timeout_handler(x):
488 if x is None:
489 raise PingTimeoutError
490
491 self._last_ping = self.Timestamp()
492 self.SendRequest('ping', {}, timeout_handler, 5)
493
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800494 def HandleRequest(self, msg):
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800495 if msg['name'] == 'upgrade':
496 self.Upgrade()
497 elif msg['name'] == 'terminal':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800498 self.SpawnGhost(self.TERMINAL, msg['params']['sid'],
499 bid=msg['params']['bid'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800500 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800501 elif msg['name'] == 'shell':
502 self.SpawnGhost(self.SHELL, msg['params']['sid'],
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800503 command=msg['params']['command'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800504 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800505 elif msg['name'] == 'file_download':
506 self.SpawnGhost(self.FILE, msg['params']['sid'],
507 file_op=('download', msg['params']['filename']))
508 self.SendResponse(msg, RESPONSE_SUCCESS)
509 elif msg['name'] == 'clear_to_download':
510 self.StartDownloadServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800511
512 def HandleResponse(self, response):
513 rid = str(response['rid'])
514 if rid in self._requests:
515 handler = self._requests[rid][2]
516 del self._requests[rid]
517 if callable(handler):
518 handler(response)
519 else:
520 print(response, self._requests.keys())
521 logging.warning('Recvied unsolicited response, ignored')
522
523 def ParseMessage(self):
524 msgs_json = self._buf.split(_SEPARATOR)
525 self._buf = msgs_json.pop()
526
527 for msg_json in msgs_json:
528 try:
529 msg = json.loads(msg_json)
530 except ValueError:
531 # Ignore mal-formed message.
532 continue
533
534 if 'name' in msg:
535 self.HandleRequest(msg)
536 elif 'response' in msg:
537 self.HandleResponse(msg)
538 else: # Ingnore mal-formed message.
539 pass
540
541 def ScanForTimeoutRequests(self):
542 for rid in self._requests.keys()[:]:
543 request_time, timeout, handler = self._requests[rid]
544 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800545 if callable(handler):
546 handler(None)
547 else:
548 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800549 del self._requests[rid]
550
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800551 def InitiateDownload(self):
552 ttyname, filename = self._download_queue.get()
553 bid = self._session_map[ttyname]
554 self.SpawnGhost(self.FILE, bid=bid, file_op=('download', filename))
555
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800556 def Listen(self):
557 try:
558 while True:
559 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
560
561 if self._sock in rds:
562 self._buf += self._sock.recv(_BUFSIZE)
563 self.ParseMessage()
564
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800565 if (self._mode == self.AGENT and
566 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800567 self.Ping()
568 self.ScanForTimeoutRequests()
569
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800570 if not self._download_queue.empty():
571 self.InitiateDownload()
572
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800573 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800574 self.Reset()
575 break
576 except socket.error:
577 raise RuntimeError('Connection dropped')
578 except PingTimeoutError:
579 raise RuntimeError('Connection timeout')
580 finally:
581 self._sock.close()
582
583 self._queue.put('resume')
584
585 if self._mode != Ghost.AGENT:
586 sys.exit(1)
587
588 def Register(self):
589 non_local = {}
590 for addr in self._overlord_addrs:
591 non_local['addr'] = addr
592 def registered(response):
593 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800594 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800595 raise RuntimeError('Register request timeout')
596 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800597 self._queue.put('pause', True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800598
599 try:
600 logging.info('Trying %s:%d ...', *addr)
601 self.Reset()
602 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
603 self._sock.settimeout(_PING_TIMEOUT)
604 self._sock.connect(addr)
605
606 logging.info('Connection established, registering...')
607 handler = {
608 Ghost.AGENT: registered,
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800609 Ghost.TERMINAL: self.SpawnPTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800610 Ghost.SHELL: self.SpawnShellServer,
611 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800612 }[self._mode]
613
614 # Machine ID may change if MAC address is used (USB-ethernet dongle
615 # plugged/unplugged)
616 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800617 self.SendRequest('register',
618 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800619 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800620 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800621 except socket.error:
622 pass
623 else:
624 self._sock.settimeout(None)
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800625 self._connected_addr = addr
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800626 self.Listen()
627
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800628 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800629
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800630 def Reconnect(self):
631 logging.info('Received reconnect request from RPC server, reconnecting...')
632 self._reset.set()
633
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800634 def AddToDownloadQueue(self, ttyname, filename):
635 self._download_queue.put((ttyname, filename))
636
637 def RegisterTTY(self, browser_id, ttyname):
638 self._session_map[ttyname] = browser_id
639
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800640 def StartLanDiscovery(self):
641 """Start to listen to LAN discovery packet at
642 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800643
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800644 def thread_func():
645 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
646 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
647 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800648 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800649 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
650 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800651 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800652 return
653
654 logging.info('LAN Discovery: started')
655 while True:
656 rd, _, _ = select.select([s], [], [], 1)
657
658 if s in rd:
659 data, source_addr = s.recvfrom(_BUFSIZE)
660 parts = data.split()
661 if parts[0] == 'OVERLORD':
662 ip, port = parts[1].split(':')
663 if not ip:
664 ip = source_addr[0]
665 self._queue.put((ip, int(port)), True)
666
667 try:
668 obj = self._queue.get(False)
669 except Queue.Empty:
670 pass
671 else:
672 if type(obj) is not str:
673 self._queue.put(obj)
674 elif obj == 'pause':
675 logging.info('LAN Discovery: paused')
676 while obj != 'resume':
677 obj = self._queue.get(True)
678 logging.info('LAN Discovery: resumed')
679
680 t = threading.Thread(target=thread_func)
681 t.daemon = True
682 t.start()
683
684 def StartRPCServer(self):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800685 logging.info("RPC Server: started")
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800686 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
687 logRequests=False)
688 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800689 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
690 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800691 t = threading.Thread(target=rpc_server.serve_forever)
692 t.daemon = True
693 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800694
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800695 def ScanServer(self):
696 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
697 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
698 if addr not in self._overlord_addrs:
699 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800700
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800701 def Start(self, lan_disc=False, rpc_server=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800702 logging.info('%s started', self.MODE_NAME[self._mode])
703 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800704 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800705
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800706 # We don't care about child process's return code, not wait is needed. This
707 # is used to prevent zombie process from lingering in the system.
708 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800709
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800710 if lan_disc:
711 self.StartLanDiscovery()
712
713 if rpc_server:
714 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800715
716 try:
717 while True:
718 try:
719 addr = self._queue.get(False)
720 except Queue.Empty:
721 pass
722 else:
723 if type(addr) == tuple and addr not in self._overlord_addrs:
724 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
725 self._overlord_addrs.append(addr)
726
727 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800728 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800729 self.Register()
730 except Exception as e:
731 logging.info(str(e) + ', retrying in %ds' % _RETRY_INTERVAL)
732 time.sleep(_RETRY_INTERVAL)
733
734 self.Reset()
735 except KeyboardInterrupt:
736 logging.error('Received keyboard interrupt, quit')
737 sys.exit(0)
738
739
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800740def GhostRPCServer():
741 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
742
743
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800744def DownloadFile(filename):
745 filepath = os.path.abspath(filename)
746 if not os.path.exists(filepath):
747 logging.error("file `%s' does not exist", filename)
748 sys.exit(1)
749
750 # Check if we actually have permission to read the file
751 if not os.access(filepath, os.R_OK):
752 logging.error("can not open %s for reading", filepath)
753 sys.exit(1)
754
755 server = GhostRPCServer()
756 server.AddToDownloadQueue(os.ttyname(0), filepath)
757 sys.exit(0)
758
759
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800760def main():
761 logger = logging.getLogger()
762 logger.setLevel(logging.INFO)
763
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800764 parser = argparse.ArgumentParser()
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800765 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
766 default=None, help='use MID as machine ID')
767 parser.add_argument('--rand-mid', dest='mid', action='store_const',
768 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800769 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
770 default=True, help='disable LAN discovery')
771 parser.add_argument('--no-rpc-server', dest='rpc_server',
772 action='store_false', default=True,
773 help='disable RPC server')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800774 parser.add_argument('--prop-file', metavar='PROP_FILE', dest="prop_file",
775 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800776 help='file containing the JSON representation of client '
777 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800778 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
779 default=None, help='file to download')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800780 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
781 nargs='*', help='overlord server address')
782 args = parser.parse_args()
783
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800784 if args.download:
785 DownloadFile(args.download)
786
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800787 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800788 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800789
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800790 g = Ghost(addrs, Ghost.AGENT, args.mid)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800791 if args.prop_file:
792 g.LoadPropertiesFromFile(args.prop_file)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800793 g.Start(args.lan_disc, args.rpc_server)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800794
795
796if __name__ == '__main__':
797 main()