blob: 65e9d0bb48ab8b9ab44c818878dcc06e26a9df8d [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
Joel Kitching22b89042015-08-06 18:23:29 +08008from __future__ import print_function
9
Wei-Ning Huang7d029b12015-03-06 10:32:15 +080010import argparse
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080011import contextlib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080012import fcntl
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080013import hashlib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080014import json
15import logging
16import os
17import Queue
Wei-Ning Huang829e0c82015-05-26 14:37:23 +080018import re
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080019import select
Wei-Ning Huanga301f572015-06-03 17:34:21 +080020import signal
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080021import socket
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080022import struct
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080023import subprocess
24import sys
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080025import termios
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080026import threading
27import time
Joel Kitching22b89042015-08-06 18:23:29 +080028import traceback
Wei-Ning Huang39169902015-09-19 06:00:23 +080029import tty
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080030import urllib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080031import uuid
32
Wei-Ning Huang2132de32015-04-13 17:24:38 +080033import jsonrpclib
34from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
35
36
37_GHOST_RPC_PORT = 4499
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080038
39_OVERLORD_PORT = 4455
40_OVERLORD_LAN_DISCOVERY_PORT = 4456
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080041_OVERLORD_HTTP_PORT = 9000
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080042
43_BUFSIZE = 8192
44_RETRY_INTERVAL = 2
45_SEPARATOR = '\r\n'
46_PING_TIMEOUT = 3
47_PING_INTERVAL = 5
48_REQUEST_TIMEOUT_SECS = 60
49_SHELL = os.getenv('SHELL', '/bin/bash')
Wei-Ning Huang2132de32015-04-13 17:24:38 +080050_DEFAULT_BIND_ADDRESS = '0.0.0.0'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080051
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080052_CONTROL_START = 128
53_CONTROL_END = 129
54
Wei-Ning Huanga301f572015-06-03 17:34:21 +080055_BLOCK_SIZE = 4096
56
Wei-Ning Huang7ec55342015-09-17 08:46:06 +080057SUCCESS = 'success'
58FAILED = 'failed'
59DISCONNECTED = 'disconnected'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080060
Joel Kitching22b89042015-08-06 18:23:29 +080061
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080062class PingTimeoutError(Exception):
63 pass
64
65
66class RequestError(Exception):
67 pass
68
69
70class Ghost(object):
71 """Ghost implements the client protocol of Overlord.
72
73 Ghost provide terminal/shell/logcat functionality and manages the client
74 side connectivity.
75 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +080076 NONE, AGENT, TERMINAL, SHELL, LOGCAT, FILE, FORWARD = range(7)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080077
78 MODE_NAME = {
79 NONE: 'NONE',
80 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080081 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080082 SHELL: 'Shell',
Wei-Ning Huanga301f572015-06-03 17:34:21 +080083 LOGCAT: 'Logcat',
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +080084 FILE: 'File',
85 FORWARD: 'Forward'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080086 }
87
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +080088 RANDOM_MID = '##random_mid##'
89
Wei-Ning Huangd521f282015-08-07 05:28:04 +080090 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +080091 prop_file=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +080092 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080093 """Constructor.
94
95 Args:
96 overlord_addrs: a list of possible address of overlord.
97 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +080098 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
99 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800100 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800101 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800102 prop_file: properties file filename.
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800103 terminal_sid: the terminal session ID associate with this client. This is
104 use for file download.
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800105 tty_device: the terminal device to open, if tty_device is None, as pseudo
106 terminal will be opened instead.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800107 command: the command to execute when we are in SHELL mode.
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800108 file_op: a tuple (action, filepath, perm). action is either 'download' or
109 'upload'. perm is the permission to set for the file.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800110 port: port number to forward.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800111 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800112 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE,
113 Ghost.FORWARD]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800114 if mode == Ghost.SHELL:
115 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800116 if mode == Ghost.FILE:
117 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800118
119 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800120 self._connected_addr = None
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800121 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800122 self._sock = None
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800123 self._mode = mode
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800124 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800125 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800126 self._terminal_session_id = terminal_sid
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800127 self._ttyname_to_sid = {}
128 self._terminal_sid_to_pid = {}
129 self._prop_file = prop_file
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800130 self._properties = {}
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800131 self._register_status = DISCONNECTED
132 self._reset = threading.Event()
133
134 # RPC
135 self._buf = '' # Read buffer
136 self._requests = {}
137 self._queue = Queue.Queue()
138
139 # Protocol specific
140 self._last_ping = 0
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800141 self._tty_device = tty_device
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800142 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800143 self._file_op = file_op
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800144 self._download_queue = Queue.Queue()
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800145 self._port = port
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800146
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800147 def SetIgnoreChild(self, status):
148 # Only ignore child for Agent since only it could spawn child Ghost.
149 if self._mode == Ghost.AGENT:
150 signal.signal(signal.SIGCHLD,
151 signal.SIG_IGN if status else signal.SIG_DFL)
152
153 def GetFileSha1(self, filename):
154 with open(filename, 'r') as f:
155 return hashlib.sha1(f.read()).hexdigest()
156
Wei-Ning Huang58833882015-09-16 16:52:37 +0800157 def UseSSL(self):
158 """Determine if SSL is enabled on the Overlord server."""
159 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
160 try:
161 sock.connect((self._connected_addr[0], _OVERLORD_HTTP_PORT))
162 sock.send('GET\r\n')
163
164 data = sock.recv(16)
165 return 'HTTP' not in data
166 except Exception:
167 return False # For whatever reason above failed, assume HTTP
168
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800169 def Upgrade(self):
170 logging.info('Upgrade: initiating upgrade sequence...')
171
172 scriptpath = os.path.abspath(sys.argv[0])
Wei-Ning Huang03f9f762015-09-16 21:51:35 +0800173 url = 'http%s://%s:%d/upgrade/ghost.py' % (
174 's' if self.UseSSL() else '', self._connected_addr[0],
175 _OVERLORD_HTTP_PORT)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800176
177 # Download sha1sum for ghost.py for verification
178 try:
179 with contextlib.closing(urllib.urlopen(url + '.sha1')) as f:
180 if f.getcode() != 200:
181 raise RuntimeError('HTTP status %d' % f.getcode())
182 sha1sum = f.read().strip()
183 except Exception:
184 logging.error('Upgrade: failed to download sha1sum file, abort')
185 return
186
187 if self.GetFileSha1(scriptpath) == sha1sum:
188 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
189 return
190
191 # Download upgrade version of ghost.py
192 try:
193 with contextlib.closing(urllib.urlopen(url)) as f:
194 if f.getcode() != 200:
195 raise RuntimeError('HTTP status %d' % f.getcode())
196 data = f.read()
197 except Exception:
198 logging.error('Upgrade: failed to download upgrade, abort')
199 return
200
201 # Compare SHA1 sum
202 if hashlib.sha1(data).hexdigest() != sha1sum:
203 logging.error('Upgrade: sha1sum mismatch, abort')
204 return
205
206 python = os.readlink('/proc/self/exe')
207 try:
208 with open(scriptpath, 'w') as f:
209 f.write(data)
210 except Exception:
211 logging.error('Upgrade: failed to write upgrade onto disk, abort')
212 return
213
214 logging.info('Upgrade: restarting ghost...')
215 self.CloseSockets()
216 self.SetIgnoreChild(False)
217 os.execve(python, [python, scriptpath] + sys.argv[1:], os.environ)
218
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800219 def LoadProperties(self):
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800220 try:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800221 if self._prop_file:
222 with open(self._prop_file, 'r') as f:
223 self._properties = json.loads(f.read())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800224 except Exception as e:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800225 logging.exception('LoadProperties: ' + str(e))
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800226
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800227 def CloseSockets(self):
228 # Close sockets opened by parent process, since we don't use it anymore.
229 for fd in os.listdir('/proc/self/fd/'):
230 try:
231 real_fd = os.readlink('/proc/self/fd/%s' % fd)
232 if real_fd.startswith('socket'):
233 os.close(int(fd))
234 except Exception:
235 pass
236
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800237 def SpawnGhost(self, mode, sid=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800238 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800239 """Spawn a child ghost with specific mode.
240
241 Returns:
242 The spawned child process pid.
243 """
Joel Kitching22b89042015-08-06 18:23:29 +0800244 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800245 self.SetIgnoreChild(False)
246
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800247 pid = os.fork()
248 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800249 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800250 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800251 terminal_sid=terminal_sid, tty_device=tty_device,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800252 command=command, file_op=file_op, port=port)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800253 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800254 sys.exit(0)
255 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800256 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800257 return pid
258
259 def Timestamp(self):
260 return int(time.time())
261
262 def GetGateWayIP(self):
263 with open('/proc/net/route', 'r') as f:
264 lines = f.readlines()
265
266 ips = []
267 for line in lines:
268 parts = line.split('\t')
269 if parts[2] == '00000000':
270 continue
271
272 try:
273 h = parts[2].decode('hex')
274 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
275 except TypeError:
276 pass
277
278 return ips
279
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800280 def GetShopfloorIP(self):
281 try:
282 import factory_common # pylint: disable=W0612
283 from cros.factory.test import shopfloor
284
285 url = shopfloor.get_server_url()
286 match = re.match(r'^https?://(.*):.*$', url)
287 if match:
288 return [match.group(1)]
289 except Exception:
290 pass
291 return []
292
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800293 def GetMachineID(self):
294 """Generates machine-dependent ID string for a machine.
295 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800296 1. factory device_id
297 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800298 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
299 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800300 We follow the listed order to generate machine ID, and fallback to the next
301 alternative if the previous doesn't work.
302 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800303 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800304 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800305 elif self._mid:
306 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800307
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800308 # Try factory device id
309 try:
310 import factory_common # pylint: disable=W0612
311 from cros.factory.test import event_log
312 with open(event_log.DEVICE_ID_PATH) as f:
313 return f.read().strip()
314 except Exception:
315 pass
316
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800317 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800318 try:
319 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
320 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800321 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800322 stdout, _ = p.communicate()
323 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800324 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800325 return stdout.strip()
326 except Exception:
327 pass
328
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800329 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800330 try:
331 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
332 return f.read().strip()
333 except Exception:
334 pass
335
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800336 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800337 try:
338 macs = []
339 ifaces = sorted(os.listdir('/sys/class/net'))
340 for iface in ifaces:
341 if iface == 'lo':
342 continue
343
344 with open('/sys/class/net/%s/address' % iface, 'r') as f:
345 macs.append(f.read().strip())
346
347 return ';'.join(macs)
348 except Exception:
349 pass
350
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800351 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800352
353 def Reset(self):
354 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800355 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800356 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800357 self._last_ping = 0
358 self._requests = {}
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800359 self.LoadProperties()
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800360 self._register_status = DISCONNECTED
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800361
362 def SendMessage(self, msg):
363 """Serialize the message and send it through the socket."""
364 self._sock.send(json.dumps(msg) + _SEPARATOR)
365
366 def SendRequest(self, name, args, handler=None,
367 timeout=_REQUEST_TIMEOUT_SECS):
368 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800369 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800370
371 rid = str(uuid.uuid4())
372 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800373 if timeout >= 0:
374 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800375 self.SendMessage(msg)
376
377 def SendResponse(self, omsg, status, params=None):
378 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
379 self.SendMessage(msg)
380
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800381 def HandleTTYControl(self, fd, control_string):
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800382 msg = json.loads(control_string)
383 command = msg['command']
384 params = msg['params']
385 if command == 'resize':
386 # some error happened on websocket
387 if len(params) != 2:
388 return
389 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
390 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
391 else:
392 logging.warn('Invalid request command "%s"', command)
393
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800394 def SpawnTTYServer(self, _):
395 """Spawn a TTY server and forward I/O to the TCP socket."""
396 logging.info('SpawnTTYServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800397
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800398 try:
399 if self._tty_device is None:
400 pid, fd = os.forkpty()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800401
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800402 if pid == 0:
403 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
404 try:
405 server = GhostRPCServer()
406 server.RegisterTTY(self._session_id, ttyname)
407 server.RegisterSession(self._session_id, os.getpid())
408 except Exception:
409 # If ghost is launched without RPC server, the call will fail but we
410 # can ignore it.
411 pass
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800412
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800413 # The directory that contains the current running ghost script
414 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800415
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800416 env = os.environ.copy()
417 env['USER'] = os.getenv('USER', 'root')
418 env['HOME'] = os.getenv('HOME', '/root')
419 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
420 os.chdir(env['HOME'])
421 os.execve(_SHELL, [_SHELL], env)
422 else:
423 fd = os.open(self._tty_device, os.O_RDWR)
Wei-Ning Huang39169902015-09-19 06:00:23 +0800424 tty.setraw(fd)
425 attr = termios.tcgetattr(fd)
426 attr[0] &= ~(termios.IXON | termios.IXOFF)
427 attr[2] |= termios.CLOCAL
428 attr[2] &= ~termios.CRTSCTS
429 attr[4] = termios.B115200
430 attr[5] = termios.B115200
431 termios.tcsetattr(fd, termios.TCSANOW, attr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800432
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800433 control_state = None
434 control_string = ''
435 write_buffer = ''
436 while True:
437 rd, _, _ = select.select([self._sock, fd], [], [])
438
439 if fd in rd:
440 self._sock.send(os.read(fd, _BUFSIZE))
441
442 if self._sock in rd:
443 ret = self._sock.recv(_BUFSIZE)
444 if len(ret) == 0:
445 raise RuntimeError('socket closed')
446 while ret:
447 if control_state:
448 if chr(_CONTROL_END) in ret:
449 index = ret.index(chr(_CONTROL_END))
450 control_string += ret[:index]
451 self.HandleTTYControl(fd, control_string)
452 control_state = None
453 control_string = ''
454 ret = ret[index+1:]
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800455 else:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800456 control_string += ret
457 ret = ''
458 else:
459 if chr(_CONTROL_START) in ret:
460 control_state = _CONTROL_START
461 index = ret.index(chr(_CONTROL_START))
462 write_buffer += ret[:index]
463 ret = ret[index+1:]
464 else:
465 write_buffer += ret
466 ret = ''
467 if write_buffer:
468 os.write(fd, write_buffer)
469 write_buffer = ''
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800470 except Exception as e:
471 logging.error('SpawnTTYServer: %s', e)
472 finally:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800473 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800474
475 logging.info('SpawnTTYServer: terminated')
476 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800477
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800478 def SpawnShellServer(self, _):
479 """Spawn a shell server and forward input/output from/to the TCP socket."""
480 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800481
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800482 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800483 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
484 shell=True)
485
486 def make_non_block(fd):
487 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
488 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
489
490 make_non_block(p.stdout)
491 make_non_block(p.stderr)
492
493 try:
494 while True:
495 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800496 if p.stdout in rd:
497 self._sock.send(p.stdout.read(_BUFSIZE))
498
499 if p.stderr in rd:
500 self._sock.send(p.stderr.read(_BUFSIZE))
501
502 if self._sock in rd:
503 ret = self._sock.recv(_BUFSIZE)
504 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800505 raise RuntimeError('socket closed')
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800506 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800507 p.poll()
508 if p.returncode != None:
509 break
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800510 except Exception as e:
511 logging.error('SpawnShellServer: %s', e)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800512 finally:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800513 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800514
515 logging.info('SpawnShellServer: terminated')
516 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800517
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800518 def InitiateFileOperation(self, _):
519 if self._file_op[0] == 'download':
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800520 try:
521 size = os.stat(self._file_op[1]).st_size
522 except OSError as e:
523 logging.error('InitiateFileOperation: download: %s', e)
524 sys.exit(1)
525
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800526 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800527 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800528 'filename': os.path.basename(self._file_op[1]),
529 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800530 elif self._file_op[0] == 'upload':
531 self.SendRequest('clear_to_upload', {}, timeout=-1)
532 self.StartUploadServer()
533 else:
534 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800535
536 def StartDownloadServer(self):
537 logging.info('StartDownloadServer: started')
538
539 try:
540 with open(self._file_op[1], 'rb') as f:
541 while True:
542 data = f.read(_BLOCK_SIZE)
543 if len(data) == 0:
544 break
545 self._sock.send(data)
546 except Exception as e:
547 logging.error('StartDownloadServer: %s', e)
548 finally:
549 self._sock.close()
550
551 logging.info('StartDownloadServer: terminated')
552 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800553
Wei-Ning Huange2981862015-08-03 15:03:08 +0800554 def StartUploadServer(self):
555 logging.info('StartUploadServer: started')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800556 try:
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800557 filepath = self._file_op[1]
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800558 dirname = os.path.dirname(filepath)
559 if not os.path.exists(dirname):
560 try:
561 os.makedirs(dirname)
562 except Exception:
563 pass
Wei-Ning Huange2981862015-08-03 15:03:08 +0800564
565 self._sock.setblocking(False)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800566 with open(filepath, 'wb') as f:
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800567 if self._file_op[2]:
568 os.fchmod(f.fileno(), self._file_op[2])
569
Wei-Ning Huange2981862015-08-03 15:03:08 +0800570 while True:
571 rd, _, _ = select.select([self._sock], [], [])
572 if self._sock in rd:
573 buf = self._sock.recv(_BLOCK_SIZE)
574 if len(buf) == 0:
575 break
576 f.write(buf)
577 except socket.error as e:
578 logging.error('StartUploadServer: socket error: %s', e)
579 except Exception as e:
580 logging.error('StartUploadServer: %s', e)
581 finally:
582 self._sock.close()
583
584 logging.info('StartUploadServer: terminated')
585 sys.exit(0)
586
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800587 def SpawnPortForwardServer(self, _):
588 """Spawn a port forwarding server and forward I/O to the TCP socket."""
589 logging.info('SpawnPortForwardServer: started')
590
591 src_sock = None
592 try:
593 src_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
594 src_sock.connect(('localhost', self._port))
595 src_sock.setblocking(False)
596
597 # Pass the leftovers of the previous buffer
598 if self._buf:
599 src_sock.send(self._buf)
600 self._buf = ''
601
602 while True:
603 rd, _, _ = select.select([self._sock, src_sock], [], [])
604
605 if self._sock in rd:
606 data = self._sock.recv(_BUFSIZE)
607 if not data:
608 break
609 src_sock.send(data)
610
611 if src_sock in rd:
612 data = src_sock.recv(_BUFSIZE)
613 if not data:
614 break
615 self._sock.send(data)
616 except Exception as e:
617 logging.error('SpawnPortForwardServer: %s', e)
618 finally:
619 if src_sock:
620 src_sock.close()
621 self._sock.close()
622
623 logging.info('SpawnPortForwardServer: terminated')
624 sys.exit(0)
625
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800626 def Ping(self):
627 def timeout_handler(x):
628 if x is None:
629 raise PingTimeoutError
630
631 self._last_ping = self.Timestamp()
632 self.SendRequest('ping', {}, timeout_handler, 5)
633
Wei-Ning Huangae923642015-09-24 14:08:09 +0800634 def HandleFileDownloadRequest(self, msg):
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800635 params = msg['params']
Wei-Ning Huangae923642015-09-24 14:08:09 +0800636 filepath = params['filename']
637 if not os.path.isabs(filepath):
638 filepath = os.path.join(os.getenv('HOME', '/tmp'), filepath)
639
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800640 try:
Wei-Ning Huang11c35022015-10-21 16:52:32 +0800641 with open(filepath, 'r') as _:
Wei-Ning Huang46a3fc92015-10-06 02:35:27 +0800642 pass
643 except Exception as e:
Wei-Ning Huangae923642015-09-24 14:08:09 +0800644 return self.SendResponse(msg, str(e))
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800645
646 self.SpawnGhost(self.FILE, params['sid'],
Wei-Ning Huangae923642015-09-24 14:08:09 +0800647 file_op=('download', filepath))
648 self.SendResponse(msg, SUCCESS)
649
650 def HandleFileUploadRequest(self, msg):
651 params = msg['params']
652
653 # Resolve upload filepath
654 filename = params['filename']
655 dest_path = filename
656
657 # If dest is specified, use it first
658 dest_path = params.get('dest', '')
659 if dest_path:
660 if not os.path.isabs(dest_path):
661 dest_path = os.path.join(os.getenv('HOME', '/tmp'), dest_path)
662
663 if os.path.isdir(dest_path):
664 dest_path = os.path.join(dest_path, filename)
665 else:
666 target_dir = os.getenv('HOME', '/tmp')
667
668 # Terminal session ID found, upload to it's current working directory
669 if params.has_key('terminal_sid'):
670 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
671 if pid:
672 target_dir = os.readlink('/proc/%d/cwd' % pid)
673
674 dest_path = os.path.join(target_dir, filename)
675
676 try:
677 os.makedirs(os.path.dirname(dest_path))
678 except Exception:
679 pass
680
681 try:
682 with open(dest_path, 'w') as _:
683 pass
684 except Exception as e:
685 return self.SendResponse(msg, str(e))
686
Wei-Ning Huangd6f69762015-10-01 21:02:07 +0800687 # If not check_only, spawn FILE mode ghost agent to handle upload
688 if not params.get('check_only', False):
689 self.SpawnGhost(self.FILE, params['sid'],
690 file_op=('upload', dest_path, params.get('perm', None)))
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800691 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800692
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800693 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800694 command = msg['name']
695 params = msg['params']
696
697 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800698 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800699 elif command == 'terminal':
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800700 self.SpawnGhost(self.TERMINAL, params['sid'],
701 tty_device=params['tty_device'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800702 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800703 elif command == 'shell':
704 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800705 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800706 elif command == 'file_download':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800707 self.HandleFileDownloadRequest(msg)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800708 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800709 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800710 elif command == 'file_upload':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800711 self.HandleFileUploadRequest(msg)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800712 elif command == 'forward':
713 self.SpawnGhost(self.FORWARD, params['sid'], port=params['port'])
714 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800715
716 def HandleResponse(self, response):
717 rid = str(response['rid'])
718 if rid in self._requests:
719 handler = self._requests[rid][2]
720 del self._requests[rid]
721 if callable(handler):
722 handler(response)
723 else:
724 print(response, self._requests.keys())
Joel Kitching22b89042015-08-06 18:23:29 +0800725 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800726
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800727 def ParseMessage(self, single=True):
728 if single:
729 index = self._buf.index(_SEPARATOR)
730 msgs_json = [self._buf[:index]]
731 self._buf = self._buf[index + 2:]
732 else:
733 msgs_json = self._buf.split(_SEPARATOR)
734 self._buf = msgs_json.pop()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800735
736 for msg_json in msgs_json:
737 try:
738 msg = json.loads(msg_json)
739 except ValueError:
740 # Ignore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800741 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800742 continue
743
744 if 'name' in msg:
745 self.HandleRequest(msg)
746 elif 'response' in msg:
747 self.HandleResponse(msg)
748 else: # Ingnore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800749 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800750
751 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800752 """Scans for pending requests which have timed out.
753
754 If any timed-out requests are discovered, their handler is called with the
755 special response value of None.
756 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800757 for rid in self._requests.keys()[:]:
758 request_time, timeout, handler = self._requests[rid]
759 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800760 if callable(handler):
761 handler(None)
762 else:
763 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800764 del self._requests[rid]
765
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800766 def InitiateDownload(self):
767 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800768 sid = self._ttyname_to_sid[ttyname]
769 self.SpawnGhost(self.FILE, terminal_sid=sid,
Wei-Ning Huangae923642015-09-24 14:08:09 +0800770 file_op=('download', filename))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800771
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800772 def Listen(self):
773 try:
774 while True:
775 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
776
777 if self._sock in rds:
778 self._buf += self._sock.recv(_BUFSIZE)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800779 self.ParseMessage(self._register_status != SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800780
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800781 if (self._mode == self.AGENT and
782 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800783 self.Ping()
784 self.ScanForTimeoutRequests()
785
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800786 if not self._download_queue.empty():
787 self.InitiateDownload()
788
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800789 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800790 self.Reset()
791 break
792 except socket.error:
793 raise RuntimeError('Connection dropped')
794 except PingTimeoutError:
795 raise RuntimeError('Connection timeout')
796 finally:
797 self._sock.close()
798
799 self._queue.put('resume')
800
801 if self._mode != Ghost.AGENT:
802 sys.exit(1)
803
804 def Register(self):
805 non_local = {}
806 for addr in self._overlord_addrs:
807 non_local['addr'] = addr
808 def registered(response):
809 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800810 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800811 raise RuntimeError('Register request timeout')
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800812
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800813 self._register_status = response['response']
814 if response['response'] != SUCCESS:
815 self._reset.set()
816 raise RuntimeError('Reigster: ' + response['response'])
817 else:
818 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
819 self._connected_addr = non_local['addr']
820 self.Upgrade() # Check for upgrade
821 self._queue.put('pause', True)
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800822
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800823 try:
824 logging.info('Trying %s:%d ...', *addr)
825 self.Reset()
826 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
827 self._sock.settimeout(_PING_TIMEOUT)
828 self._sock.connect(addr)
829
830 logging.info('Connection established, registering...')
831 handler = {
832 Ghost.AGENT: registered,
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800833 Ghost.TERMINAL: self.SpawnTTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800834 Ghost.SHELL: self.SpawnShellServer,
835 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800836 Ghost.FORWARD: self.SpawnPortForwardServer,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800837 }[self._mode]
838
839 # Machine ID may change if MAC address is used (USB-ethernet dongle
840 # plugged/unplugged)
841 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800842 self.SendRequest('register',
843 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800844 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800845 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800846 except socket.error:
847 pass
848 else:
849 self._sock.settimeout(None)
850 self.Listen()
851
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800852 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800853
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800854 def Reconnect(self):
855 logging.info('Received reconnect request from RPC server, reconnecting...')
856 self._reset.set()
857
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800858 def GetStatus(self):
859 return self._register_status
860
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800861 def AddToDownloadQueue(self, ttyname, filename):
862 self._download_queue.put((ttyname, filename))
863
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800864 def RegisterTTY(self, session_id, ttyname):
865 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +0800866
867 def RegisterSession(self, session_id, process_id):
868 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800869
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800870 def StartLanDiscovery(self):
871 """Start to listen to LAN discovery packet at
872 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800873
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800874 def thread_func():
875 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
876 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
877 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800878 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800879 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
880 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800881 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800882 return
883
884 logging.info('LAN Discovery: started')
885 while True:
886 rd, _, _ = select.select([s], [], [], 1)
887
888 if s in rd:
889 data, source_addr = s.recvfrom(_BUFSIZE)
890 parts = data.split()
891 if parts[0] == 'OVERLORD':
892 ip, port = parts[1].split(':')
893 if not ip:
894 ip = source_addr[0]
895 self._queue.put((ip, int(port)), True)
896
897 try:
898 obj = self._queue.get(False)
899 except Queue.Empty:
900 pass
901 else:
902 if type(obj) is not str:
903 self._queue.put(obj)
904 elif obj == 'pause':
905 logging.info('LAN Discovery: paused')
906 while obj != 'resume':
907 obj = self._queue.get(True)
908 logging.info('LAN Discovery: resumed')
909
910 t = threading.Thread(target=thread_func)
911 t.daemon = True
912 t.start()
913
914 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800915 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800916 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
917 logRequests=False)
918 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800919 rpc_server.register_function(self.GetStatus, 'GetStatus')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800920 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800921 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800922 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800923 t = threading.Thread(target=rpc_server.serve_forever)
924 t.daemon = True
925 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800926
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800927 def ScanServer(self):
928 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
929 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
930 if addr not in self._overlord_addrs:
931 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800932
Wei-Ning Huang11c35022015-10-21 16:52:32 +0800933 def Start(self, lan_disc=False, rpc_server=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800934 logging.info('%s started', self.MODE_NAME[self._mode])
935 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800936 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800937
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800938 # We don't care about child process's return code, not wait is needed. This
939 # is used to prevent zombie process from lingering in the system.
940 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800941
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800942 if lan_disc:
943 self.StartLanDiscovery()
944
945 if rpc_server:
946 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800947
948 try:
949 while True:
950 try:
951 addr = self._queue.get(False)
952 except Queue.Empty:
953 pass
954 else:
955 if type(addr) == tuple and addr not in self._overlord_addrs:
956 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
957 self._overlord_addrs.append(addr)
958
959 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800960 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800961 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +0800962 # Don't show stack trace for RuntimeError, which we use in this file for
963 # plausible and expected errors (such as can't connect to server).
964 except RuntimeError as e:
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800965 logging.info('%s, retrying in %ds', e.message, _RETRY_INTERVAL)
Joel Kitching22b89042015-08-06 18:23:29 +0800966 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800967 except Exception as e:
Joel Kitching22b89042015-08-06 18:23:29 +0800968 _, _, exc_traceback = sys.exc_info()
969 traceback.print_tb(exc_traceback)
970 logging.info('%s: %s, retrying in %ds',
971 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800972 time.sleep(_RETRY_INTERVAL)
973
974 self.Reset()
975 except KeyboardInterrupt:
976 logging.error('Received keyboard interrupt, quit')
977 sys.exit(0)
978
979
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800980def GhostRPCServer():
Wei-Ning Huang8037c182015-09-19 04:41:50 +0800981 """Returns handler to Ghost's JSON RPC server."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800982 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
983
984
Wei-Ning Huang8037c182015-09-19 04:41:50 +0800985def ForkToBackground():
986 """Fork process to run in background."""
987 pid = os.fork()
988 if pid != 0:
989 logging.info('Ghost(%d) running in background.', pid)
990 sys.exit(0)
991
992
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800993def DownloadFile(filename):
Wei-Ning Huang8037c182015-09-19 04:41:50 +0800994 """Initiate a client-initiated file download."""
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800995 filepath = os.path.abspath(filename)
996 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +0800997 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800998 sys.exit(1)
999
1000 # Check if we actually have permission to read the file
1001 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001002 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001003 sys.exit(1)
1004
1005 server = GhostRPCServer()
1006 server.AddToDownloadQueue(os.ttyname(0), filepath)
1007 sys.exit(0)
1008
1009
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001010def main():
1011 logger = logging.getLogger()
1012 logger.setLevel(logging.INFO)
1013
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001014 parser = argparse.ArgumentParser()
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001015 parser.add_argument('--fork', dest='fork', action='store_true', default=False,
1016 help='fork procecess to run in background')
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001017 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1018 default=None, help='use MID as machine ID')
1019 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1020 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001021 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1022 default=True, help='disable LAN discovery')
1023 parser.add_argument('--no-rpc-server', dest='rpc_server',
1024 action='store_false', default=True,
1025 help='disable RPC server')
Joel Kitching22b89042015-08-06 18:23:29 +08001026 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001027 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001028 help='file containing the JSON representation of client '
1029 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001030 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1031 default=None, help='file to download')
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001032 parser.add_argument('--reset', dest='reset', default=False,
1033 action='store_true',
1034 help='reset ghost and reload all configs')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001035 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1036 nargs='*', help='overlord server address')
1037 args = parser.parse_args()
1038
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001039 if args.fork:
1040 ForkToBackground()
1041
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001042 if args.reset:
1043 GhostRPCServer().Reconnect()
1044 sys.exit()
1045
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001046 if args.download:
1047 DownloadFile(args.download)
1048
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001049 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001050 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001051
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001052 g = Ghost(addrs, Ghost.AGENT, args.mid, prop_file=args.prop_file)
Wei-Ning Huang11c35022015-10-21 16:52:32 +08001053 g.Start(args.lan_disc, args.rpc_server)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001054
1055
1056if __name__ == '__main__':
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001057 main()