blob: 6b3b02774d3c1b4da9cd7e8e93d0fcf9508609d6 [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
Wei-Ning Huang829e0c82015-05-26 14:37:23 +080014import re
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080015import select
Wei-Ning Huanga301f572015-06-03 17:34:21 +080016import signal
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080017import socket
18import subprocess
19import sys
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080020import struct
21import termios
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080022import threading
23import time
24import uuid
25
Wei-Ning Huang2132de32015-04-13 17:24:38 +080026import jsonrpclib
27from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
28
29
30_GHOST_RPC_PORT = 4499
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080031
32_OVERLORD_PORT = 4455
33_OVERLORD_LAN_DISCOVERY_PORT = 4456
34
35_BUFSIZE = 8192
36_RETRY_INTERVAL = 2
37_SEPARATOR = '\r\n'
38_PING_TIMEOUT = 3
39_PING_INTERVAL = 5
40_REQUEST_TIMEOUT_SECS = 60
41_SHELL = os.getenv('SHELL', '/bin/bash')
Wei-Ning Huang2132de32015-04-13 17:24:38 +080042_DEFAULT_BIND_ADDRESS = '0.0.0.0'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080043
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080044_CONTROL_START = 128
45_CONTROL_END = 129
46
Wei-Ning Huanga301f572015-06-03 17:34:21 +080047_BLOCK_SIZE = 4096
48
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080049RESPONSE_SUCCESS = 'success'
50RESPONSE_FAILED = 'failed'
51
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080052class PingTimeoutError(Exception):
53 pass
54
55
56class RequestError(Exception):
57 pass
58
59
60class Ghost(object):
61 """Ghost implements the client protocol of Overlord.
62
63 Ghost provide terminal/shell/logcat functionality and manages the client
64 side connectivity.
65 """
Wei-Ning Huanga301f572015-06-03 17:34:21 +080066 NONE, AGENT, TERMINAL, SHELL, LOGCAT, FILE = range(6)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080067
68 MODE_NAME = {
69 NONE: 'NONE',
70 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080071 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080072 SHELL: 'Shell',
Wei-Ning Huanga301f572015-06-03 17:34:21 +080073 LOGCAT: 'Logcat',
74 FILE: 'File'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080075 }
76
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +080077 RANDOM_MID = '##random_mid##'
78
Wei-Ning Huanga301f572015-06-03 17:34:21 +080079 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None, bid=None,
80 command=None, file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080081 """Constructor.
82
83 Args:
84 overlord_addrs: a list of possible address of overlord.
85 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +080086 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
87 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +080088 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080089 be set to the corresponding session id assigned by overlord.
Wei-Ning Huanga301f572015-06-03 17:34:21 +080090 bid: browser ID. Identifies the browser which started the session.
91 command: the command to execute when we are in SHELL mode.
92 file_op: a tuple (action, filepath). action is either 'download' or
93 'upload'.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080094 """
Wei-Ning Huanga301f572015-06-03 17:34:21 +080095 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080096 if mode == Ghost.SHELL:
97 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +080098 if mode == Ghost.FILE:
99 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800100
101 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800102 self._connected_addr = None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800103 self._mode = mode
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800104 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800105 self._sock = None
106 self._machine_id = self.GetMachineID()
107 self._client_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800108 self._browser_id = bid
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800109 self._properties = {}
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800110 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800111 self._file_op = file_op
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800112 self._buf = ''
113 self._requests = {}
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800114 self._reset = threading.Event()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800115 self._last_ping = 0
116 self._queue = Queue.Queue()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800117 self._download_queue = Queue.Queue()
118 self._session_map = {} # Stores the mapping between ttyname and browser_id
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800119
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800120 def LoadPropertiesFromFile(self, filename):
121 try:
122 with open(filename, 'r') as f:
123 self._properties = json.loads(f.read())
124 except Exception as e:
125 logging.exception('LoadPropertiesFromFile: ' + str(e))
126
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800127 def SpawnGhost(self, mode, sid=None, bid=None, command=None, file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800128 """Spawn a child ghost with specific mode.
129
130 Returns:
131 The spawned child process pid.
132 """
133 pid = os.fork()
134 if pid == 0:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800135 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid, bid,
136 command, file_op)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800137 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800138 sys.exit(0)
139 else:
140 return pid
141
142 def Timestamp(self):
143 return int(time.time())
144
145 def GetGateWayIP(self):
146 with open('/proc/net/route', 'r') as f:
147 lines = f.readlines()
148
149 ips = []
150 for line in lines:
151 parts = line.split('\t')
152 if parts[2] == '00000000':
153 continue
154
155 try:
156 h = parts[2].decode('hex')
157 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
158 except TypeError:
159 pass
160
161 return ips
162
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800163 def GetShopfloorIP(self):
164 try:
165 import factory_common # pylint: disable=W0612
166 from cros.factory.test import shopfloor
167
168 url = shopfloor.get_server_url()
169 match = re.match(r'^https?://(.*):.*$', url)
170 if match:
171 return [match.group(1)]
172 except Exception:
173 pass
174 return []
175
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800176 def GetMachineID(self):
177 """Generates machine-dependent ID string for a machine.
178 There are many ways to generate a machine ID:
179 1. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800180 2. factory device_id
181 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
182 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800183 We follow the listed order to generate machine ID, and fallback to the next
184 alternative if the previous doesn't work.
185 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800186 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800187 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800188 elif self._mid:
189 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800190
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800191 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800192 try:
193 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
194 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800195 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800196 stdout, _ = p.communicate()
197 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800198 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800199 return stdout.strip()
200 except Exception:
201 pass
202
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800203 # Try factory device id
204 try:
205 import factory_common # pylint: disable=W0612
206 from cros.factory.test import event_log
207 with open(event_log.DEVICE_ID_PATH) as f:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800208 return f.read().strip()
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800209 except Exception:
210 pass
211
212 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800213 try:
214 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
215 return f.read().strip()
216 except Exception:
217 pass
218
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800219 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800220 try:
221 macs = []
222 ifaces = sorted(os.listdir('/sys/class/net'))
223 for iface in ifaces:
224 if iface == 'lo':
225 continue
226
227 with open('/sys/class/net/%s/address' % iface, 'r') as f:
228 macs.append(f.read().strip())
229
230 return ';'.join(macs)
231 except Exception:
232 pass
233
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800234 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800235
236 def Reset(self):
237 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800238 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800239 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800240 self._last_ping = 0
241 self._requests = {}
242
243 def SendMessage(self, msg):
244 """Serialize the message and send it through the socket."""
245 self._sock.send(json.dumps(msg) + _SEPARATOR)
246
247 def SendRequest(self, name, args, handler=None,
248 timeout=_REQUEST_TIMEOUT_SECS):
249 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800250 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800251
252 rid = str(uuid.uuid4())
253 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
254 self._requests[rid] = [self.Timestamp(), timeout, handler]
255 self.SendMessage(msg)
256
257 def SendResponse(self, omsg, status, params=None):
258 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
259 self.SendMessage(msg)
260
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800261 def HandlePTYControl(self, fd, control_string):
262 msg = json.loads(control_string)
263 command = msg['command']
264 params = msg['params']
265 if command == 'resize':
266 # some error happened on websocket
267 if len(params) != 2:
268 return
269 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
270 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
271 else:
272 logging.warn('Invalid request command "%s"', command)
273
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800274 def SpawnPTYServer(self, _):
275 """Spawn a PTY server and forward I/O to the TCP socket."""
276 logging.info('SpawnPTYServer: started')
277
278 pid, fd = os.forkpty()
279 if pid == 0:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800280 # Register the mapping of browser_id and ttyname
281 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
282 try:
283 server = GhostRPCServer()
284 server.RegisterTTY(self._browser_id, ttyname)
285 except Exception:
286 # If ghost is launched without RPC server, the call will fail but we
287 # can ignore it.
288 pass
289
290 # The directory that contains the current running ghost script
291 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
292
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800293 env = os.environ.copy()
294 env['USER'] = os.getenv('USER', 'root')
295 env['HOME'] = os.getenv('HOME', '/root')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800296 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800297 os.chdir(env['HOME'])
298 os.execve(_SHELL, [_SHELL], env)
299 else:
300 try:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800301 control_state = None
302 control_string = ''
303 write_buffer = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800304 while True:
305 rd, _, _ = select.select([self._sock, fd], [], [])
306
307 if fd in rd:
308 self._sock.send(os.read(fd, _BUFSIZE))
309
310 if self._sock in rd:
311 ret = self._sock.recv(_BUFSIZE)
312 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800313 raise RuntimeError('socket closed')
314 while ret:
315 if control_state:
316 if chr(_CONTROL_END) in ret:
317 index = ret.index(chr(_CONTROL_END))
318 control_string += ret[:index]
319 self.HandlePTYControl(fd, control_string)
320 control_state = None
321 control_string = ''
322 ret = ret[index+1:]
323 else:
324 control_string += ret
325 ret = ''
326 else:
327 if chr(_CONTROL_START) in ret:
328 control_state = _CONTROL_START
329 index = ret.index(chr(_CONTROL_START))
330 write_buffer += ret[:index]
331 ret = ret[index+1:]
332 else:
333 write_buffer += ret
334 ret = ''
335 if write_buffer:
336 os.write(fd, write_buffer)
337 write_buffer = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800338 except (OSError, socket.error, RuntimeError):
339 self._sock.close()
340 logging.info('SpawnPTYServer: terminated')
341 sys.exit(0)
342
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800343 def SpawnShellServer(self, _):
344 """Spawn a shell server and forward input/output from/to the TCP socket."""
345 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800346
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800347 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800348 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
349 shell=True)
350
351 def make_non_block(fd):
352 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
353 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
354
355 make_non_block(p.stdout)
356 make_non_block(p.stderr)
357
358 try:
359 while True:
360 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800361 p.poll()
362
363 if p.returncode != None:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800364 raise RuntimeError('process complete')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800365
366 if p.stdout in rd:
367 self._sock.send(p.stdout.read(_BUFSIZE))
368
369 if p.stderr in rd:
370 self._sock.send(p.stderr.read(_BUFSIZE))
371
372 if self._sock in rd:
373 ret = self._sock.recv(_BUFSIZE)
374 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800375 raise RuntimeError('socket closed')
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800376 p.stdin.write(ret)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800377 except (OSError, socket.error, RuntimeError):
378 self._sock.close()
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800379 logging.info('SpawnShellServer: terminated')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800380 sys.exit(0)
381
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800382 def InitiateFileOperation(self, _):
383 if self._file_op[0] == 'download':
384 size = os.stat(self._file_op[1]).st_size
385 self.SendRequest('request_to_download',
386 {'bid': self._browser_id,
387 'filename': os.path.basename(self._file_op[1]),
388 'size': size})
389
390 def StartDownloadServer(self):
391 logging.info('StartDownloadServer: started')
392
393 try:
394 with open(self._file_op[1], 'rb') as f:
395 while True:
396 data = f.read(_BLOCK_SIZE)
397 if len(data) == 0:
398 break
399 self._sock.send(data)
400 except Exception as e:
401 logging.error('StartDownloadServer: %s', e)
402 finally:
403 self._sock.close()
404
405 logging.info('StartDownloadServer: terminated')
406 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800407
408 def Ping(self):
409 def timeout_handler(x):
410 if x is None:
411 raise PingTimeoutError
412
413 self._last_ping = self.Timestamp()
414 self.SendRequest('ping', {}, timeout_handler, 5)
415
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800416 def HandleRequest(self, msg):
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800417 if msg['name'] == 'terminal':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800418 self.SpawnGhost(self.TERMINAL, msg['params']['sid'],
419 bid=msg['params']['bid'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800420 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800421 elif msg['name'] == 'shell':
422 self.SpawnGhost(self.SHELL, msg['params']['sid'],
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800423 command=msg['params']['command'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800424 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800425 elif msg['name'] == 'file_download':
426 self.SpawnGhost(self.FILE, msg['params']['sid'],
427 file_op=('download', msg['params']['filename']))
428 self.SendResponse(msg, RESPONSE_SUCCESS)
429 elif msg['name'] == 'clear_to_download':
430 self.StartDownloadServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800431
432 def HandleResponse(self, response):
433 rid = str(response['rid'])
434 if rid in self._requests:
435 handler = self._requests[rid][2]
436 del self._requests[rid]
437 if callable(handler):
438 handler(response)
439 else:
440 print(response, self._requests.keys())
441 logging.warning('Recvied unsolicited response, ignored')
442
443 def ParseMessage(self):
444 msgs_json = self._buf.split(_SEPARATOR)
445 self._buf = msgs_json.pop()
446
447 for msg_json in msgs_json:
448 try:
449 msg = json.loads(msg_json)
450 except ValueError:
451 # Ignore mal-formed message.
452 continue
453
454 if 'name' in msg:
455 self.HandleRequest(msg)
456 elif 'response' in msg:
457 self.HandleResponse(msg)
458 else: # Ingnore mal-formed message.
459 pass
460
461 def ScanForTimeoutRequests(self):
462 for rid in self._requests.keys()[:]:
463 request_time, timeout, handler = self._requests[rid]
464 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800465 if callable(handler):
466 handler(None)
467 else:
468 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800469 del self._requests[rid]
470
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800471 def InitiateDownload(self):
472 ttyname, filename = self._download_queue.get()
473 bid = self._session_map[ttyname]
474 self.SpawnGhost(self.FILE, bid=bid, file_op=('download', filename))
475
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800476 def Listen(self):
477 try:
478 while True:
479 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
480
481 if self._sock in rds:
482 self._buf += self._sock.recv(_BUFSIZE)
483 self.ParseMessage()
484
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800485 if (self._mode == self.AGENT and
486 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800487 self.Ping()
488 self.ScanForTimeoutRequests()
489
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800490 if not self._download_queue.empty():
491 self.InitiateDownload()
492
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800493 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800494 self.Reset()
495 break
496 except socket.error:
497 raise RuntimeError('Connection dropped')
498 except PingTimeoutError:
499 raise RuntimeError('Connection timeout')
500 finally:
501 self._sock.close()
502
503 self._queue.put('resume')
504
505 if self._mode != Ghost.AGENT:
506 sys.exit(1)
507
508 def Register(self):
509 non_local = {}
510 for addr in self._overlord_addrs:
511 non_local['addr'] = addr
512 def registered(response):
513 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800514 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800515 raise RuntimeError('Register request timeout')
516 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800517 self._queue.put('pause', True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800518
519 try:
520 logging.info('Trying %s:%d ...', *addr)
521 self.Reset()
522 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
523 self._sock.settimeout(_PING_TIMEOUT)
524 self._sock.connect(addr)
525
526 logging.info('Connection established, registering...')
527 handler = {
528 Ghost.AGENT: registered,
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800529 Ghost.TERMINAL: self.SpawnPTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800530 Ghost.SHELL: self.SpawnShellServer,
531 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800532 }[self._mode]
533
534 # Machine ID may change if MAC address is used (USB-ethernet dongle
535 # plugged/unplugged)
536 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800537 self.SendRequest('register',
538 {'mode': self._mode, 'mid': self._machine_id,
539 'cid': self._client_id,
540 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800541 except socket.error:
542 pass
543 else:
544 self._sock.settimeout(None)
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800545 self._connected_addr = addr
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800546 self.Listen()
547
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800548 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800549
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800550 def Reconnect(self):
551 logging.info('Received reconnect request from RPC server, reconnecting...')
552 self._reset.set()
553
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800554 def AddToDownloadQueue(self, ttyname, filename):
555 self._download_queue.put((ttyname, filename))
556
557 def RegisterTTY(self, browser_id, ttyname):
558 self._session_map[ttyname] = browser_id
559
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800560 def StartLanDiscovery(self):
561 """Start to listen to LAN discovery packet at
562 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800563
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800564 def thread_func():
565 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
566 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
567 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800568 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800569 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
570 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800571 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800572 return
573
574 logging.info('LAN Discovery: started')
575 while True:
576 rd, _, _ = select.select([s], [], [], 1)
577
578 if s in rd:
579 data, source_addr = s.recvfrom(_BUFSIZE)
580 parts = data.split()
581 if parts[0] == 'OVERLORD':
582 ip, port = parts[1].split(':')
583 if not ip:
584 ip = source_addr[0]
585 self._queue.put((ip, int(port)), True)
586
587 try:
588 obj = self._queue.get(False)
589 except Queue.Empty:
590 pass
591 else:
592 if type(obj) is not str:
593 self._queue.put(obj)
594 elif obj == 'pause':
595 logging.info('LAN Discovery: paused')
596 while obj != 'resume':
597 obj = self._queue.get(True)
598 logging.info('LAN Discovery: resumed')
599
600 t = threading.Thread(target=thread_func)
601 t.daemon = True
602 t.start()
603
604 def StartRPCServer(self):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800605 logging.info("RPC Server: started")
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800606 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
607 logRequests=False)
608 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800609 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
610 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800611 t = threading.Thread(target=rpc_server.serve_forever)
612 t.daemon = True
613 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800614
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800615 def ScanServer(self):
616 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
617 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
618 if addr not in self._overlord_addrs:
619 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800620
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800621 def Start(self, lan_disc=False, rpc_server=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800622 logging.info('%s started', self.MODE_NAME[self._mode])
623 logging.info('MID: %s', self._machine_id)
624 logging.info('CID: %s', self._client_id)
625
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800626 # We don't care about child process's return code, not wait is needed.
627 signal.signal(signal.SIGCHLD, signal.SIG_IGN)
628
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800629 if lan_disc:
630 self.StartLanDiscovery()
631
632 if rpc_server:
633 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800634
635 try:
636 while True:
637 try:
638 addr = self._queue.get(False)
639 except Queue.Empty:
640 pass
641 else:
642 if type(addr) == tuple and addr not in self._overlord_addrs:
643 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
644 self._overlord_addrs.append(addr)
645
646 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800647 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800648 self.Register()
649 except Exception as e:
650 logging.info(str(e) + ', retrying in %ds' % _RETRY_INTERVAL)
651 time.sleep(_RETRY_INTERVAL)
652
653 self.Reset()
654 except KeyboardInterrupt:
655 logging.error('Received keyboard interrupt, quit')
656 sys.exit(0)
657
658
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800659def GhostRPCServer():
660 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
661
662
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800663def DownloadFile(filename):
664 filepath = os.path.abspath(filename)
665 if not os.path.exists(filepath):
666 logging.error("file `%s' does not exist", filename)
667 sys.exit(1)
668
669 # Check if we actually have permission to read the file
670 if not os.access(filepath, os.R_OK):
671 logging.error("can not open %s for reading", filepath)
672 sys.exit(1)
673
674 server = GhostRPCServer()
675 server.AddToDownloadQueue(os.ttyname(0), filepath)
676 sys.exit(0)
677
678
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800679def main():
680 logger = logging.getLogger()
681 logger.setLevel(logging.INFO)
682
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800683 parser = argparse.ArgumentParser()
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800684 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
685 default=None, help='use MID as machine ID')
686 parser.add_argument('--rand-mid', dest='mid', action='store_const',
687 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800688 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
689 default=True, help='disable LAN discovery')
690 parser.add_argument('--no-rpc-server', dest='rpc_server',
691 action='store_false', default=True,
692 help='disable RPC server')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800693 parser.add_argument('--prop-file', metavar='PROP_FILE', dest="prop_file",
694 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800695 help='file containing the JSON representation of client '
696 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800697 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
698 default=None, help='file to download')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800699 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
700 nargs='*', help='overlord server address')
701 args = parser.parse_args()
702
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800703 if args.download:
704 DownloadFile(args.download)
705
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800706 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800707 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800708
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800709 g = Ghost(addrs, Ghost.AGENT, args.mid)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800710 if args.prop_file:
711 g.LoadPropertiesFromFile(args.prop_file)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800712 g.Start(args.lan_disc, args.rpc_server)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800713
714
715if __name__ == '__main__':
716 main()