blob: da72bd6ee00fe7b146d1ba34ed14049b64d5d19e [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 Huangd521f282015-08-07 05:28:04 +080083 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None,
84 terminal_sid=None, 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 Huangd521f282015-08-07 05:28:04 +080094 terminal_sid: the terminal session ID associate with this client. This is
95 use for file download.
Wei-Ning Huanga301f572015-06-03 17:34:21 +080096 command: the command to execute when we are in SHELL mode.
Wei-Ning Huange2981862015-08-03 15:03:08 +080097 file_op: a tuple (action, filepath, pid). action is either 'download' or
98 'upload'. pid is the pid of the target shell, used to determine where
99 the current working is and thus where to upload to.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800100 """
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800101 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800102 if mode == Ghost.SHELL:
103 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800104 if mode == Ghost.FILE:
105 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800106
107 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800108 self._connected_addr = None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800109 self._mode = mode
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800110 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800111 self._sock = None
112 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800113 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800114 self._terminal_session_id = terminal_sid
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800115 self._properties = {}
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800116 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800117 self._file_op = file_op
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800118 self._buf = ''
119 self._requests = {}
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800120 self._reset = threading.Event()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800121 self._last_ping = 0
122 self._queue = Queue.Queue()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800123 self._download_queue = Queue.Queue()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800124 self._ttyname_to_sid = {}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800125 self._terminal_sid_to_pid = {}
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800126
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800127 def SetIgnoreChild(self, status):
128 # Only ignore child for Agent since only it could spawn child Ghost.
129 if self._mode == Ghost.AGENT:
130 signal.signal(signal.SIGCHLD,
131 signal.SIG_IGN if status else signal.SIG_DFL)
132
133 def GetFileSha1(self, filename):
134 with open(filename, 'r') as f:
135 return hashlib.sha1(f.read()).hexdigest()
136
137 def Upgrade(self):
138 logging.info('Upgrade: initiating upgrade sequence...')
139
140 scriptpath = os.path.abspath(sys.argv[0])
141 url = 'http://%s:%d/upgrade/ghost.py' % (
142 self._connected_addr[0], _OVERLORD_HTTP_PORT)
143
144 # Download sha1sum for ghost.py for verification
145 try:
146 with contextlib.closing(urllib.urlopen(url + '.sha1')) as f:
147 if f.getcode() != 200:
148 raise RuntimeError('HTTP status %d' % f.getcode())
149 sha1sum = f.read().strip()
150 except Exception:
151 logging.error('Upgrade: failed to download sha1sum file, abort')
152 return
153
154 if self.GetFileSha1(scriptpath) == sha1sum:
155 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
156 return
157
158 # Download upgrade version of ghost.py
159 try:
160 with contextlib.closing(urllib.urlopen(url)) as f:
161 if f.getcode() != 200:
162 raise RuntimeError('HTTP status %d' % f.getcode())
163 data = f.read()
164 except Exception:
165 logging.error('Upgrade: failed to download upgrade, abort')
166 return
167
168 # Compare SHA1 sum
169 if hashlib.sha1(data).hexdigest() != sha1sum:
170 logging.error('Upgrade: sha1sum mismatch, abort')
171 return
172
173 python = os.readlink('/proc/self/exe')
174 try:
175 with open(scriptpath, 'w') as f:
176 f.write(data)
177 except Exception:
178 logging.error('Upgrade: failed to write upgrade onto disk, abort')
179 return
180
181 logging.info('Upgrade: restarting ghost...')
182 self.CloseSockets()
183 self.SetIgnoreChild(False)
184 os.execve(python, [python, scriptpath] + sys.argv[1:], os.environ)
185
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800186 def LoadPropertiesFromFile(self, filename):
187 try:
188 with open(filename, 'r') as f:
189 self._properties = json.loads(f.read())
190 except Exception as e:
191 logging.exception('LoadPropertiesFromFile: ' + str(e))
192
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800193 def CloseSockets(self):
194 # Close sockets opened by parent process, since we don't use it anymore.
195 for fd in os.listdir('/proc/self/fd/'):
196 try:
197 real_fd = os.readlink('/proc/self/fd/%s' % fd)
198 if real_fd.startswith('socket'):
199 os.close(int(fd))
200 except Exception:
201 pass
202
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800203 def SpawnGhost(self, mode, sid=None, terminal_sid=None, command=None,
204 file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800205 """Spawn a child ghost with specific mode.
206
207 Returns:
208 The spawned child process pid.
209 """
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800210 # Restore the default signal hanlder, so our child won't have problems.
211 self.SetIgnoreChild(False)
212
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800213 pid = os.fork()
214 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800215 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800216 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
217 terminal_sid, command, file_op)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800218 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800219 sys.exit(0)
220 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800221 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800222 return pid
223
224 def Timestamp(self):
225 return int(time.time())
226
227 def GetGateWayIP(self):
228 with open('/proc/net/route', 'r') as f:
229 lines = f.readlines()
230
231 ips = []
232 for line in lines:
233 parts = line.split('\t')
234 if parts[2] == '00000000':
235 continue
236
237 try:
238 h = parts[2].decode('hex')
239 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
240 except TypeError:
241 pass
242
243 return ips
244
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800245 def GetShopfloorIP(self):
246 try:
247 import factory_common # pylint: disable=W0612
248 from cros.factory.test import shopfloor
249
250 url = shopfloor.get_server_url()
251 match = re.match(r'^https?://(.*):.*$', url)
252 if match:
253 return [match.group(1)]
254 except Exception:
255 pass
256 return []
257
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800258 def GetMachineID(self):
259 """Generates machine-dependent ID string for a machine.
260 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800261 1. factory device_id
262 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800263 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
264 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800265 We follow the listed order to generate machine ID, and fallback to the next
266 alternative if the previous doesn't work.
267 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800268 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800269 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800270 elif self._mid:
271 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800272
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800273 # Try factory device id
274 try:
275 import factory_common # pylint: disable=W0612
276 from cros.factory.test import event_log
277 with open(event_log.DEVICE_ID_PATH) as f:
278 return f.read().strip()
279 except Exception:
280 pass
281
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800282 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800283 try:
284 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
285 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800286 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800287 stdout, _ = p.communicate()
288 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800289 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800290 return stdout.strip()
291 except Exception:
292 pass
293
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800294 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800295 try:
296 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
297 return f.read().strip()
298 except Exception:
299 pass
300
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800301 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800302 try:
303 macs = []
304 ifaces = sorted(os.listdir('/sys/class/net'))
305 for iface in ifaces:
306 if iface == 'lo':
307 continue
308
309 with open('/sys/class/net/%s/address' % iface, 'r') as f:
310 macs.append(f.read().strip())
311
312 return ';'.join(macs)
313 except Exception:
314 pass
315
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800316 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800317
318 def Reset(self):
319 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800320 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800321 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800322 self._last_ping = 0
323 self._requests = {}
324
325 def SendMessage(self, msg):
326 """Serialize the message and send it through the socket."""
327 self._sock.send(json.dumps(msg) + _SEPARATOR)
328
329 def SendRequest(self, name, args, handler=None,
330 timeout=_REQUEST_TIMEOUT_SECS):
331 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800332 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800333
334 rid = str(uuid.uuid4())
335 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800336 if timeout >= 0:
337 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800338 self.SendMessage(msg)
339
340 def SendResponse(self, omsg, status, params=None):
341 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
342 self.SendMessage(msg)
343
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800344 def HandlePTYControl(self, fd, control_string):
345 msg = json.loads(control_string)
346 command = msg['command']
347 params = msg['params']
348 if command == 'resize':
349 # some error happened on websocket
350 if len(params) != 2:
351 return
352 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
353 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
354 else:
355 logging.warn('Invalid request command "%s"', command)
356
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800357 def SpawnPTYServer(self, _):
358 """Spawn a PTY server and forward I/O to the TCP socket."""
359 logging.info('SpawnPTYServer: started')
360
361 pid, fd = os.forkpty()
362 if pid == 0:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800363 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
364 try:
365 server = GhostRPCServer()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800366 server.RegisterTTY(self._session_id, ttyname)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800367 server.RegisterSession(self._session_id, os.getpid())
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800368 except Exception:
369 # If ghost is launched without RPC server, the call will fail but we
370 # can ignore it.
371 pass
372
373 # The directory that contains the current running ghost script
374 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
375
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800376 env = os.environ.copy()
377 env['USER'] = os.getenv('USER', 'root')
378 env['HOME'] = os.getenv('HOME', '/root')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800379 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800380 os.chdir(env['HOME'])
381 os.execve(_SHELL, [_SHELL], env)
382 else:
383 try:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800384 control_state = None
385 control_string = ''
386 write_buffer = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800387 while True:
388 rd, _, _ = select.select([self._sock, fd], [], [])
389
390 if fd in rd:
391 self._sock.send(os.read(fd, _BUFSIZE))
392
393 if self._sock in rd:
394 ret = self._sock.recv(_BUFSIZE)
395 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800396 raise RuntimeError('socket closed')
397 while ret:
398 if control_state:
399 if chr(_CONTROL_END) in ret:
400 index = ret.index(chr(_CONTROL_END))
401 control_string += ret[:index]
402 self.HandlePTYControl(fd, control_string)
403 control_state = None
404 control_string = ''
405 ret = ret[index+1:]
406 else:
407 control_string += ret
408 ret = ''
409 else:
410 if chr(_CONTROL_START) in ret:
411 control_state = _CONTROL_START
412 index = ret.index(chr(_CONTROL_START))
413 write_buffer += ret[:index]
414 ret = ret[index+1:]
415 else:
416 write_buffer += ret
417 ret = ''
418 if write_buffer:
419 os.write(fd, write_buffer)
420 write_buffer = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800421 except (OSError, socket.error, RuntimeError):
422 self._sock.close()
423 logging.info('SpawnPTYServer: terminated')
424 sys.exit(0)
425
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800426 def SpawnShellServer(self, _):
427 """Spawn a shell server and forward input/output from/to the TCP socket."""
428 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800429
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800430 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800431 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
432 shell=True)
433
434 def make_non_block(fd):
435 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
436 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
437
438 make_non_block(p.stdout)
439 make_non_block(p.stderr)
440
441 try:
442 while True:
443 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800444 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 Huangf14c84e2015-08-03 15:03:08 +0800455
456 p.poll()
457 if p.returncode != None:
458 break
459 finally:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800460 self._sock.close()
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800461 logging.info('SpawnShellServer: terminated')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800462 sys.exit(0)
463
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800464 def InitiateFileOperation(self, _):
465 if self._file_op[0] == 'download':
466 size = os.stat(self._file_op[1]).st_size
467 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800468 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800469 'filename': os.path.basename(self._file_op[1]),
470 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800471 elif self._file_op[0] == 'upload':
472 self.SendRequest('clear_to_upload', {}, timeout=-1)
473 self.StartUploadServer()
474 else:
475 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800476
477 def StartDownloadServer(self):
478 logging.info('StartDownloadServer: started')
479
480 try:
481 with open(self._file_op[1], 'rb') as f:
482 while True:
483 data = f.read(_BLOCK_SIZE)
484 if len(data) == 0:
485 break
486 self._sock.send(data)
487 except Exception as e:
488 logging.error('StartDownloadServer: %s', e)
489 finally:
490 self._sock.close()
491
492 logging.info('StartDownloadServer: terminated')
493 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800494
Wei-Ning Huange2981862015-08-03 15:03:08 +0800495 def StartUploadServer(self):
496 logging.info('StartUploadServer: started')
497
498 try:
499 target_dir = os.getenv('HOME', '/tmp')
500
501 # Get the client's working dir, which is our target upload dir
502 if self._file_op[2]:
503 target_dir = os.readlink('/proc/%d/cwd' % self._file_op[2])
504
505 self._sock.setblocking(False)
506 with open(os.path.join(target_dir, self._file_op[1]), 'wb') as f:
507 while True:
508 rd, _, _ = select.select([self._sock], [], [])
509 if self._sock in rd:
510 buf = self._sock.recv(_BLOCK_SIZE)
511 if len(buf) == 0:
512 break
513 f.write(buf)
514 except socket.error as e:
515 logging.error('StartUploadServer: socket error: %s', e)
516 except Exception as e:
517 logging.error('StartUploadServer: %s', e)
518 finally:
519 self._sock.close()
520
521 logging.info('StartUploadServer: terminated')
522 sys.exit(0)
523
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800524 def Ping(self):
525 def timeout_handler(x):
526 if x is None:
527 raise PingTimeoutError
528
529 self._last_ping = self.Timestamp()
530 self.SendRequest('ping', {}, timeout_handler, 5)
531
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800532 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800533 command = msg['name']
534 params = msg['params']
535
536 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800537 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800538 elif command == 'terminal':
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800539 self.SpawnGhost(self.TERMINAL, params['sid'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800540 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800541 elif command == 'shell':
542 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800543 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800544 elif command == 'file_download':
545 self.SpawnGhost(self.FILE, params['sid'],
546 file_op=('download', params['filename'], None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800547 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800548 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800549 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800550 elif command == 'file_upload':
551 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
552 self.SpawnGhost(self.FILE, params['sid'],
553 file_op=('upload', params['filename'], pid))
554 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800555
556 def HandleResponse(self, response):
557 rid = str(response['rid'])
558 if rid in self._requests:
559 handler = self._requests[rid][2]
560 del self._requests[rid]
561 if callable(handler):
562 handler(response)
563 else:
564 print(response, self._requests.keys())
565 logging.warning('Recvied unsolicited response, ignored')
566
567 def ParseMessage(self):
568 msgs_json = self._buf.split(_SEPARATOR)
569 self._buf = msgs_json.pop()
570
571 for msg_json in msgs_json:
572 try:
573 msg = json.loads(msg_json)
574 except ValueError:
575 # Ignore mal-formed message.
576 continue
577
578 if 'name' in msg:
579 self.HandleRequest(msg)
580 elif 'response' in msg:
581 self.HandleResponse(msg)
582 else: # Ingnore mal-formed message.
583 pass
584
585 def ScanForTimeoutRequests(self):
586 for rid in self._requests.keys()[:]:
587 request_time, timeout, handler = self._requests[rid]
588 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800589 if callable(handler):
590 handler(None)
591 else:
592 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800593 del self._requests[rid]
594
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800595 def InitiateDownload(self):
596 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800597 sid = self._ttyname_to_sid[ttyname]
598 self.SpawnGhost(self.FILE, terminal_sid=sid,
599 file_op=('download', filename, None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800600
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800601 def Listen(self):
602 try:
603 while True:
604 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
605
606 if self._sock in rds:
607 self._buf += self._sock.recv(_BUFSIZE)
608 self.ParseMessage()
609
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800610 if (self._mode == self.AGENT and
611 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800612 self.Ping()
613 self.ScanForTimeoutRequests()
614
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800615 if not self._download_queue.empty():
616 self.InitiateDownload()
617
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800618 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800619 self.Reset()
620 break
621 except socket.error:
622 raise RuntimeError('Connection dropped')
623 except PingTimeoutError:
624 raise RuntimeError('Connection timeout')
625 finally:
626 self._sock.close()
627
628 self._queue.put('resume')
629
630 if self._mode != Ghost.AGENT:
631 sys.exit(1)
632
633 def Register(self):
634 non_local = {}
635 for addr in self._overlord_addrs:
636 non_local['addr'] = addr
637 def registered(response):
638 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800639 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800640 raise RuntimeError('Register request timeout')
641 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800642 self._queue.put('pause', True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800643
644 try:
645 logging.info('Trying %s:%d ...', *addr)
646 self.Reset()
647 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
648 self._sock.settimeout(_PING_TIMEOUT)
649 self._sock.connect(addr)
650
651 logging.info('Connection established, registering...')
652 handler = {
653 Ghost.AGENT: registered,
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800654 Ghost.TERMINAL: self.SpawnPTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800655 Ghost.SHELL: self.SpawnShellServer,
656 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800657 }[self._mode]
658
659 # Machine ID may change if MAC address is used (USB-ethernet dongle
660 # plugged/unplugged)
661 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800662 self.SendRequest('register',
663 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800664 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800665 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800666 except socket.error:
667 pass
668 else:
669 self._sock.settimeout(None)
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800670 self._connected_addr = addr
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800671 self.Listen()
672
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800673 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800674
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800675 def Reconnect(self):
676 logging.info('Received reconnect request from RPC server, reconnecting...')
677 self._reset.set()
678
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800679 def AddToDownloadQueue(self, ttyname, filename):
680 self._download_queue.put((ttyname, filename))
681
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800682 def RegisterTTY(self, session_id, ttyname):
683 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +0800684
685 def RegisterSession(self, session_id, process_id):
686 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800687
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800688 def StartLanDiscovery(self):
689 """Start to listen to LAN discovery packet at
690 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800691
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800692 def thread_func():
693 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
694 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
695 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800696 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800697 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
698 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800699 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800700 return
701
702 logging.info('LAN Discovery: started')
703 while True:
704 rd, _, _ = select.select([s], [], [], 1)
705
706 if s in rd:
707 data, source_addr = s.recvfrom(_BUFSIZE)
708 parts = data.split()
709 if parts[0] == 'OVERLORD':
710 ip, port = parts[1].split(':')
711 if not ip:
712 ip = source_addr[0]
713 self._queue.put((ip, int(port)), True)
714
715 try:
716 obj = self._queue.get(False)
717 except Queue.Empty:
718 pass
719 else:
720 if type(obj) is not str:
721 self._queue.put(obj)
722 elif obj == 'pause':
723 logging.info('LAN Discovery: paused')
724 while obj != 'resume':
725 obj = self._queue.get(True)
726 logging.info('LAN Discovery: resumed')
727
728 t = threading.Thread(target=thread_func)
729 t.daemon = True
730 t.start()
731
732 def StartRPCServer(self):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800733 logging.info("RPC Server: started")
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800734 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
735 logRequests=False)
736 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800737 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800738 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800739 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800740 t = threading.Thread(target=rpc_server.serve_forever)
741 t.daemon = True
742 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800743
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800744 def ScanServer(self):
745 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
746 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
747 if addr not in self._overlord_addrs:
748 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800749
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800750 def Start(self, lan_disc=False, rpc_server=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800751 logging.info('%s started', self.MODE_NAME[self._mode])
752 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800753 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800754
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800755 # We don't care about child process's return code, not wait is needed. This
756 # is used to prevent zombie process from lingering in the system.
757 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800758
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800759 if lan_disc:
760 self.StartLanDiscovery()
761
762 if rpc_server:
763 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800764
765 try:
766 while True:
767 try:
768 addr = self._queue.get(False)
769 except Queue.Empty:
770 pass
771 else:
772 if type(addr) == tuple and addr not in self._overlord_addrs:
773 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
774 self._overlord_addrs.append(addr)
775
776 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800777 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800778 self.Register()
779 except Exception as e:
780 logging.info(str(e) + ', retrying in %ds' % _RETRY_INTERVAL)
781 time.sleep(_RETRY_INTERVAL)
782
783 self.Reset()
784 except KeyboardInterrupt:
785 logging.error('Received keyboard interrupt, quit')
786 sys.exit(0)
787
788
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800789def GhostRPCServer():
790 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
791
792
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800793def DownloadFile(filename):
794 filepath = os.path.abspath(filename)
795 if not os.path.exists(filepath):
796 logging.error("file `%s' does not exist", filename)
797 sys.exit(1)
798
799 # Check if we actually have permission to read the file
800 if not os.access(filepath, os.R_OK):
801 logging.error("can not open %s for reading", filepath)
802 sys.exit(1)
803
804 server = GhostRPCServer()
805 server.AddToDownloadQueue(os.ttyname(0), filepath)
806 sys.exit(0)
807
808
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800809def main():
810 logger = logging.getLogger()
811 logger.setLevel(logging.INFO)
812
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800813 parser = argparse.ArgumentParser()
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800814 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
815 default=None, help='use MID as machine ID')
816 parser.add_argument('--rand-mid', dest='mid', action='store_const',
817 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800818 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
819 default=True, help='disable LAN discovery')
820 parser.add_argument('--no-rpc-server', dest='rpc_server',
821 action='store_false', default=True,
822 help='disable RPC server')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800823 parser.add_argument('--prop-file', metavar='PROP_FILE', dest="prop_file",
824 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800825 help='file containing the JSON representation of client '
826 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800827 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
828 default=None, help='file to download')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800829 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
830 nargs='*', help='overlord server address')
831 args = parser.parse_args()
832
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800833 if args.download:
834 DownloadFile(args.download)
835
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800836 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800837 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800838
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800839 g = Ghost(addrs, Ghost.AGENT, args.mid)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800840 if args.prop_file:
841 g.LoadPropertiesFromFile(args.prop_file)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800842 g.Start(args.lan_disc, args.rpc_server)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800843
844
845if __name__ == '__main__':
846 main()