blob: 83b2d8348ed65ca3fafd1c7c62b0dcb8b33fc40d [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
Joel Kitching22b89042015-08-06 18:23:29 +080026import traceback
Wei-Ning Huang39169902015-09-19 06:00:23 +080027import tty
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080028import urllib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080029import uuid
30
Wei-Ning Huang2132de32015-04-13 17:24:38 +080031import jsonrpclib
32from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
33
34
35_GHOST_RPC_PORT = 4499
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080036
37_OVERLORD_PORT = 4455
38_OVERLORD_LAN_DISCOVERY_PORT = 4456
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080039_OVERLORD_HTTP_PORT = 9000
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080040
41_BUFSIZE = 8192
42_RETRY_INTERVAL = 2
43_SEPARATOR = '\r\n'
44_PING_TIMEOUT = 3
45_PING_INTERVAL = 5
46_REQUEST_TIMEOUT_SECS = 60
47_SHELL = os.getenv('SHELL', '/bin/bash')
Wei-Ning Huang2132de32015-04-13 17:24:38 +080048_DEFAULT_BIND_ADDRESS = '0.0.0.0'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080049
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080050_CONTROL_START = 128
51_CONTROL_END = 129
52
Wei-Ning Huanga301f572015-06-03 17:34:21 +080053_BLOCK_SIZE = 4096
54
Wei-Ning Huang7ec55342015-09-17 08:46:06 +080055SUCCESS = 'success'
56FAILED = 'failed'
57DISCONNECTED = 'disconnected'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080058
Joel Kitching22b89042015-08-06 18:23:29 +080059
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080060class PingTimeoutError(Exception):
61 pass
62
63
64class RequestError(Exception):
65 pass
66
67
68class Ghost(object):
69 """Ghost implements the client protocol of Overlord.
70
71 Ghost provide terminal/shell/logcat functionality and manages the client
72 side connectivity.
73 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +080074 NONE, AGENT, TERMINAL, SHELL, LOGCAT, FILE, FORWARD = range(7)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080075
76 MODE_NAME = {
77 NONE: 'NONE',
78 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080079 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080080 SHELL: 'Shell',
Wei-Ning Huanga301f572015-06-03 17:34:21 +080081 LOGCAT: 'Logcat',
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +080082 FILE: 'File',
83 FORWARD: 'Forward'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080084 }
85
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +080086 RANDOM_MID = '##random_mid##'
87
Wei-Ning Huangd521f282015-08-07 05:28:04 +080088 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +080089 prop_file=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +080090 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080091 """Constructor.
92
93 Args:
94 overlord_addrs: a list of possible address of overlord.
95 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +080096 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
97 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +080098 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080099 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800100 prop_file: properties file filename.
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800101 terminal_sid: the terminal session ID associate with this client. This is
102 use for file download.
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800103 tty_device: the terminal device to open, if tty_device is None, as pseudo
104 terminal will be opened instead.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800105 command: the command to execute when we are in SHELL mode.
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800106 file_op: a tuple (action, filepath, perm). action is either 'download' or
107 'upload'. perm is the permission to set for the file.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800108 port: port number to forward.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800109 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800110 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE,
111 Ghost.FORWARD]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800112 if mode == Ghost.SHELL:
113 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800114 if mode == Ghost.FILE:
115 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800116
117 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800118 self._connected_addr = None
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800119 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800120 self._sock = None
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800121 self._mode = mode
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800122 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800123 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800124 self._terminal_session_id = terminal_sid
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800125 self._ttyname_to_sid = {}
126 self._terminal_sid_to_pid = {}
127 self._prop_file = prop_file
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800128 self._properties = {}
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800129 self._register_status = DISCONNECTED
130 self._reset = threading.Event()
131
132 # RPC
133 self._buf = '' # Read buffer
134 self._requests = {}
135 self._queue = Queue.Queue()
136
137 # Protocol specific
138 self._last_ping = 0
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800139 self._tty_device = tty_device
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800140 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800141 self._file_op = file_op
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800142 self._download_queue = Queue.Queue()
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800143 self._port = port
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800144
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800145 def SetIgnoreChild(self, status):
146 # Only ignore child for Agent since only it could spawn child Ghost.
147 if self._mode == Ghost.AGENT:
148 signal.signal(signal.SIGCHLD,
149 signal.SIG_IGN if status else signal.SIG_DFL)
150
151 def GetFileSha1(self, filename):
152 with open(filename, 'r') as f:
153 return hashlib.sha1(f.read()).hexdigest()
154
Wei-Ning Huang58833882015-09-16 16:52:37 +0800155 def UseSSL(self):
156 """Determine if SSL is enabled on the Overlord server."""
157 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
158 try:
159 sock.connect((self._connected_addr[0], _OVERLORD_HTTP_PORT))
160 sock.send('GET\r\n')
161
162 data = sock.recv(16)
163 return 'HTTP' not in data
164 except Exception:
165 return False # For whatever reason above failed, assume HTTP
166
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800167 def Upgrade(self):
168 logging.info('Upgrade: initiating upgrade sequence...')
169
170 scriptpath = os.path.abspath(sys.argv[0])
Wei-Ning Huang03f9f762015-09-16 21:51:35 +0800171 url = 'http%s://%s:%d/upgrade/ghost.py' % (
172 's' if self.UseSSL() else '', self._connected_addr[0],
173 _OVERLORD_HTTP_PORT)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800174
175 # Download sha1sum for ghost.py for verification
176 try:
177 with contextlib.closing(urllib.urlopen(url + '.sha1')) as f:
178 if f.getcode() != 200:
179 raise RuntimeError('HTTP status %d' % f.getcode())
180 sha1sum = f.read().strip()
181 except Exception:
182 logging.error('Upgrade: failed to download sha1sum file, abort')
183 return
184
185 if self.GetFileSha1(scriptpath) == sha1sum:
186 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
187 return
188
189 # Download upgrade version of ghost.py
190 try:
191 with contextlib.closing(urllib.urlopen(url)) as f:
192 if f.getcode() != 200:
193 raise RuntimeError('HTTP status %d' % f.getcode())
194 data = f.read()
195 except Exception:
196 logging.error('Upgrade: failed to download upgrade, abort')
197 return
198
199 # Compare SHA1 sum
200 if hashlib.sha1(data).hexdigest() != sha1sum:
201 logging.error('Upgrade: sha1sum mismatch, abort')
202 return
203
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800204 try:
205 with open(scriptpath, 'w') as f:
206 f.write(data)
207 except Exception:
208 logging.error('Upgrade: failed to write upgrade onto disk, abort')
209 return
210
211 logging.info('Upgrade: restarting ghost...')
212 self.CloseSockets()
213 self.SetIgnoreChild(False)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800214 os.execve(scriptpath, [scriptpath] + sys.argv[1:], os.environ)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800215
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800216 def LoadProperties(self):
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800217 try:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800218 if self._prop_file:
219 with open(self._prop_file, 'r') as f:
220 self._properties = json.loads(f.read())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800221 except Exception as e:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800222 logging.exception('LoadProperties: ' + str(e))
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800223
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800224 def CloseSockets(self):
225 # Close sockets opened by parent process, since we don't use it anymore.
226 for fd in os.listdir('/proc/self/fd/'):
227 try:
228 real_fd = os.readlink('/proc/self/fd/%s' % fd)
229 if real_fd.startswith('socket'):
230 os.close(int(fd))
231 except Exception:
232 pass
233
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800234 def SpawnGhost(self, mode, sid=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800235 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800236 """Spawn a child ghost with specific mode.
237
238 Returns:
239 The spawned child process pid.
240 """
Joel Kitching22b89042015-08-06 18:23:29 +0800241 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800242 self.SetIgnoreChild(False)
243
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800244 pid = os.fork()
245 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800246 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800247 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800248 terminal_sid=terminal_sid, tty_device=tty_device,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800249 command=command, file_op=file_op, port=port)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800250 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800251 sys.exit(0)
252 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800253 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800254 return pid
255
256 def Timestamp(self):
257 return int(time.time())
258
259 def GetGateWayIP(self):
260 with open('/proc/net/route', 'r') as f:
261 lines = f.readlines()
262
263 ips = []
264 for line in lines:
265 parts = line.split('\t')
266 if parts[2] == '00000000':
267 continue
268
269 try:
270 h = parts[2].decode('hex')
271 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
272 except TypeError:
273 pass
274
275 return ips
276
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800277 def GetShopfloorIP(self):
278 try:
279 import factory_common # pylint: disable=W0612
280 from cros.factory.test import shopfloor
281
282 url = shopfloor.get_server_url()
283 match = re.match(r'^https?://(.*):.*$', url)
284 if match:
285 return [match.group(1)]
286 except Exception:
287 pass
288 return []
289
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800290 def GetMachineID(self):
291 """Generates machine-dependent ID string for a machine.
292 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800293 1. factory device_id
294 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800295 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
296 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800297 We follow the listed order to generate machine ID, and fallback to the next
298 alternative if the previous doesn't work.
299 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800300 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800301 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800302 elif self._mid:
303 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800304
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800305 # Try factory device id
306 try:
307 import factory_common # pylint: disable=W0612
308 from cros.factory.test import event_log
309 with open(event_log.DEVICE_ID_PATH) as f:
310 return f.read().strip()
311 except Exception:
312 pass
313
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800314 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800315 try:
316 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
317 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800318 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800319 stdout, _ = p.communicate()
320 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800321 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800322 return stdout.strip()
323 except Exception:
324 pass
325
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800326 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800327 try:
328 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
329 return f.read().strip()
330 except Exception:
331 pass
332
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800333 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800334 try:
335 macs = []
336 ifaces = sorted(os.listdir('/sys/class/net'))
337 for iface in ifaces:
338 if iface == 'lo':
339 continue
340
341 with open('/sys/class/net/%s/address' % iface, 'r') as f:
342 macs.append(f.read().strip())
343
344 return ';'.join(macs)
345 except Exception:
346 pass
347
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800348 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800349
350 def Reset(self):
351 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800352 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800353 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800354 self._last_ping = 0
355 self._requests = {}
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800356 self.LoadProperties()
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800357 self._register_status = DISCONNECTED
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800358
359 def SendMessage(self, msg):
360 """Serialize the message and send it through the socket."""
361 self._sock.send(json.dumps(msg) + _SEPARATOR)
362
363 def SendRequest(self, name, args, handler=None,
364 timeout=_REQUEST_TIMEOUT_SECS):
365 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800366 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800367
368 rid = str(uuid.uuid4())
369 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800370 if timeout >= 0:
371 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800372 self.SendMessage(msg)
373
374 def SendResponse(self, omsg, status, params=None):
375 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
376 self.SendMessage(msg)
377
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800378 def HandleTTYControl(self, fd, control_string):
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800379 msg = json.loads(control_string)
380 command = msg['command']
381 params = msg['params']
382 if command == 'resize':
383 # some error happened on websocket
384 if len(params) != 2:
385 return
386 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
387 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
388 else:
389 logging.warn('Invalid request command "%s"', command)
390
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800391 def SpawnTTYServer(self, _):
392 """Spawn a TTY server and forward I/O to the TCP socket."""
393 logging.info('SpawnTTYServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800394
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800395 try:
396 if self._tty_device is None:
397 pid, fd = os.forkpty()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800398
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800399 if pid == 0:
400 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
401 try:
402 server = GhostRPCServer()
403 server.RegisterTTY(self._session_id, ttyname)
404 server.RegisterSession(self._session_id, os.getpid())
405 except Exception:
406 # If ghost is launched without RPC server, the call will fail but we
407 # can ignore it.
408 pass
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800409
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800410 # The directory that contains the current running ghost script
411 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800412
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800413 env = os.environ.copy()
414 env['USER'] = os.getenv('USER', 'root')
415 env['HOME'] = os.getenv('HOME', '/root')
416 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
417 os.chdir(env['HOME'])
418 os.execve(_SHELL, [_SHELL], env)
419 else:
420 fd = os.open(self._tty_device, os.O_RDWR)
Wei-Ning Huang39169902015-09-19 06:00:23 +0800421 tty.setraw(fd)
422 attr = termios.tcgetattr(fd)
423 attr[0] &= ~(termios.IXON | termios.IXOFF)
424 attr[2] |= termios.CLOCAL
425 attr[2] &= ~termios.CRTSCTS
426 attr[4] = termios.B115200
427 attr[5] = termios.B115200
428 termios.tcsetattr(fd, termios.TCSANOW, attr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800429
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800430 control_state = None
431 control_string = ''
432 write_buffer = ''
433 while True:
434 rd, _, _ = select.select([self._sock, fd], [], [])
435
436 if fd in rd:
437 self._sock.send(os.read(fd, _BUFSIZE))
438
439 if self._sock in rd:
440 ret = self._sock.recv(_BUFSIZE)
441 if len(ret) == 0:
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800442 raise RuntimeError('connection terminated')
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800443 while ret:
444 if control_state:
445 if chr(_CONTROL_END) in ret:
446 index = ret.index(chr(_CONTROL_END))
447 control_string += ret[:index]
448 self.HandleTTYControl(fd, control_string)
449 control_state = None
450 control_string = ''
451 ret = ret[index+1:]
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800452 else:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800453 control_string += ret
454 ret = ''
455 else:
456 if chr(_CONTROL_START) in ret:
457 control_state = _CONTROL_START
458 index = ret.index(chr(_CONTROL_START))
459 write_buffer += ret[:index]
460 ret = ret[index+1:]
461 else:
462 write_buffer += ret
463 ret = ''
464 if write_buffer:
465 os.write(fd, write_buffer)
466 write_buffer = ''
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800467 except Exception as e:
468 logging.error('SpawnTTYServer: %s', e)
469 finally:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800470 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800471
472 logging.info('SpawnTTYServer: terminated')
473 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800474
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800475 def SpawnShellServer(self, _):
476 """Spawn a shell server and forward input/output from/to the TCP socket."""
477 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800478
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800479 # Add ghost executable to PATH
480 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
481 env = os.environ.copy()
482 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
483
484 # Execute shell command from HOME directory
485 os.chdir(os.getenv('HOME', '/tmp'))
486
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800487 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800488 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800489 shell=True, env=env)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800490
491 def make_non_block(fd):
492 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
493 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
494
495 make_non_block(p.stdout)
496 make_non_block(p.stderr)
497
498 try:
499 while True:
500 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800501 if p.stdout in rd:
502 self._sock.send(p.stdout.read(_BUFSIZE))
503
504 if p.stderr in rd:
505 self._sock.send(p.stderr.read(_BUFSIZE))
506
507 if self._sock in rd:
508 ret = self._sock.recv(_BUFSIZE)
509 if len(ret) == 0:
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800510 raise RuntimeError('connection terminated')
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800511 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800512 p.poll()
513 if p.returncode != None:
514 break
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800515 except Exception as e:
516 logging.error('SpawnShellServer: %s', e)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800517 finally:
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800518 # Send SIGTERM to process, then wait for 1 second. Send another SIGKILL
519 # to make sure the process is terminated.
520 try:
521 p.terminate()
522 time.sleep(1)
523 p.kill()
524 except Exception:
525 pass
526
527 p.wait()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800528 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800529
530 logging.info('SpawnShellServer: terminated')
531 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800532
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800533 def InitiateFileOperation(self, _):
534 if self._file_op[0] == 'download':
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800535 try:
536 size = os.stat(self._file_op[1]).st_size
537 except OSError as e:
538 logging.error('InitiateFileOperation: download: %s', e)
539 sys.exit(1)
540
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800541 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800542 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800543 'filename': os.path.basename(self._file_op[1]),
544 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800545 elif self._file_op[0] == 'upload':
546 self.SendRequest('clear_to_upload', {}, timeout=-1)
547 self.StartUploadServer()
548 else:
549 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800550
551 def StartDownloadServer(self):
552 logging.info('StartDownloadServer: started')
553
554 try:
555 with open(self._file_op[1], 'rb') as f:
556 while True:
557 data = f.read(_BLOCK_SIZE)
558 if len(data) == 0:
559 break
560 self._sock.send(data)
561 except Exception as e:
562 logging.error('StartDownloadServer: %s', e)
563 finally:
564 self._sock.close()
565
566 logging.info('StartDownloadServer: terminated')
567 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800568
Wei-Ning Huange2981862015-08-03 15:03:08 +0800569 def StartUploadServer(self):
570 logging.info('StartUploadServer: started')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800571 try:
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800572 filepath = self._file_op[1]
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800573 dirname = os.path.dirname(filepath)
574 if not os.path.exists(dirname):
575 try:
576 os.makedirs(dirname)
577 except Exception:
578 pass
Wei-Ning Huange2981862015-08-03 15:03:08 +0800579
580 self._sock.setblocking(False)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800581 with open(filepath, 'wb') as f:
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800582 if self._file_op[2]:
583 os.fchmod(f.fileno(), self._file_op[2])
584
Wei-Ning Huange2981862015-08-03 15:03:08 +0800585 while True:
586 rd, _, _ = select.select([self._sock], [], [])
587 if self._sock in rd:
588 buf = self._sock.recv(_BLOCK_SIZE)
589 if len(buf) == 0:
590 break
591 f.write(buf)
592 except socket.error as e:
593 logging.error('StartUploadServer: socket error: %s', e)
594 except Exception as e:
595 logging.error('StartUploadServer: %s', e)
596 finally:
597 self._sock.close()
598
599 logging.info('StartUploadServer: terminated')
600 sys.exit(0)
601
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800602 def SpawnPortForwardServer(self, _):
603 """Spawn a port forwarding server and forward I/O to the TCP socket."""
604 logging.info('SpawnPortForwardServer: started')
605
606 src_sock = None
607 try:
608 src_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
609 src_sock.connect(('localhost', self._port))
610 src_sock.setblocking(False)
611
612 # Pass the leftovers of the previous buffer
613 if self._buf:
614 src_sock.send(self._buf)
615 self._buf = ''
616
617 while True:
618 rd, _, _ = select.select([self._sock, src_sock], [], [])
619
620 if self._sock in rd:
621 data = self._sock.recv(_BUFSIZE)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800622 if len(data) == 0:
623 raise RuntimeError('connection terminated')
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800624 src_sock.send(data)
625
626 if src_sock in rd:
627 data = src_sock.recv(_BUFSIZE)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800628 if len(data) == 0:
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800629 break
630 self._sock.send(data)
631 except Exception as e:
632 logging.error('SpawnPortForwardServer: %s', e)
633 finally:
634 if src_sock:
635 src_sock.close()
636 self._sock.close()
637
638 logging.info('SpawnPortForwardServer: terminated')
639 sys.exit(0)
640
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800641 def Ping(self):
642 def timeout_handler(x):
643 if x is None:
644 raise PingTimeoutError
645
646 self._last_ping = self.Timestamp()
647 self.SendRequest('ping', {}, timeout_handler, 5)
648
Wei-Ning Huangae923642015-09-24 14:08:09 +0800649 def HandleFileDownloadRequest(self, msg):
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800650 params = msg['params']
Wei-Ning Huangae923642015-09-24 14:08:09 +0800651 filepath = params['filename']
652 if not os.path.isabs(filepath):
653 filepath = os.path.join(os.getenv('HOME', '/tmp'), filepath)
654
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800655 try:
Wei-Ning Huang11c35022015-10-21 16:52:32 +0800656 with open(filepath, 'r') as _:
Wei-Ning Huang46a3fc92015-10-06 02:35:27 +0800657 pass
658 except Exception as e:
Wei-Ning Huangae923642015-09-24 14:08:09 +0800659 return self.SendResponse(msg, str(e))
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800660
661 self.SpawnGhost(self.FILE, params['sid'],
Wei-Ning Huangae923642015-09-24 14:08:09 +0800662 file_op=('download', filepath))
663 self.SendResponse(msg, SUCCESS)
664
665 def HandleFileUploadRequest(self, msg):
666 params = msg['params']
667
668 # Resolve upload filepath
669 filename = params['filename']
670 dest_path = filename
671
672 # If dest is specified, use it first
673 dest_path = params.get('dest', '')
674 if dest_path:
675 if not os.path.isabs(dest_path):
676 dest_path = os.path.join(os.getenv('HOME', '/tmp'), dest_path)
677
678 if os.path.isdir(dest_path):
679 dest_path = os.path.join(dest_path, filename)
680 else:
681 target_dir = os.getenv('HOME', '/tmp')
682
683 # Terminal session ID found, upload to it's current working directory
684 if params.has_key('terminal_sid'):
685 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
686 if pid:
687 target_dir = os.readlink('/proc/%d/cwd' % pid)
688
689 dest_path = os.path.join(target_dir, filename)
690
691 try:
692 os.makedirs(os.path.dirname(dest_path))
693 except Exception:
694 pass
695
696 try:
697 with open(dest_path, 'w') as _:
698 pass
699 except Exception as e:
700 return self.SendResponse(msg, str(e))
701
Wei-Ning Huangd6f69762015-10-01 21:02:07 +0800702 # If not check_only, spawn FILE mode ghost agent to handle upload
703 if not params.get('check_only', False):
704 self.SpawnGhost(self.FILE, params['sid'],
705 file_op=('upload', dest_path, params.get('perm', None)))
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800706 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800707
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800708 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800709 command = msg['name']
710 params = msg['params']
711
712 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800713 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800714 elif command == 'terminal':
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800715 self.SpawnGhost(self.TERMINAL, params['sid'],
716 tty_device=params['tty_device'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800717 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800718 elif command == 'shell':
719 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800720 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800721 elif command == 'file_download':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800722 self.HandleFileDownloadRequest(msg)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800723 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800724 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800725 elif command == 'file_upload':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800726 self.HandleFileUploadRequest(msg)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800727 elif command == 'forward':
728 self.SpawnGhost(self.FORWARD, params['sid'], port=params['port'])
729 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800730
731 def HandleResponse(self, response):
732 rid = str(response['rid'])
733 if rid in self._requests:
734 handler = self._requests[rid][2]
735 del self._requests[rid]
736 if callable(handler):
737 handler(response)
738 else:
Joel Kitching22b89042015-08-06 18:23:29 +0800739 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800740
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800741 def ParseMessage(self, single=True):
742 if single:
743 index = self._buf.index(_SEPARATOR)
744 msgs_json = [self._buf[:index]]
745 self._buf = self._buf[index + 2:]
746 else:
747 msgs_json = self._buf.split(_SEPARATOR)
748 self._buf = msgs_json.pop()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800749
750 for msg_json in msgs_json:
751 try:
752 msg = json.loads(msg_json)
753 except ValueError:
754 # Ignore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800755 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800756 continue
757
758 if 'name' in msg:
759 self.HandleRequest(msg)
760 elif 'response' in msg:
761 self.HandleResponse(msg)
762 else: # Ingnore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800763 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800764
765 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800766 """Scans for pending requests which have timed out.
767
768 If any timed-out requests are discovered, their handler is called with the
769 special response value of None.
770 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800771 for rid in self._requests.keys()[:]:
772 request_time, timeout, handler = self._requests[rid]
773 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800774 if callable(handler):
775 handler(None)
776 else:
777 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800778 del self._requests[rid]
779
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800780 def InitiateDownload(self):
781 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800782 sid = self._ttyname_to_sid[ttyname]
783 self.SpawnGhost(self.FILE, terminal_sid=sid,
Wei-Ning Huangae923642015-09-24 14:08:09 +0800784 file_op=('download', filename))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800785
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800786 def Listen(self):
787 try:
788 while True:
789 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
790
791 if self._sock in rds:
792 self._buf += self._sock.recv(_BUFSIZE)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800793 self.ParseMessage(self._register_status != SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800794
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800795 if (self._mode == self.AGENT and
796 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800797 self.Ping()
798 self.ScanForTimeoutRequests()
799
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800800 if not self._download_queue.empty():
801 self.InitiateDownload()
802
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800803 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800804 self.Reset()
805 break
806 except socket.error:
807 raise RuntimeError('Connection dropped')
808 except PingTimeoutError:
809 raise RuntimeError('Connection timeout')
810 finally:
811 self._sock.close()
812
813 self._queue.put('resume')
814
815 if self._mode != Ghost.AGENT:
816 sys.exit(1)
817
818 def Register(self):
819 non_local = {}
820 for addr in self._overlord_addrs:
821 non_local['addr'] = addr
822 def registered(response):
823 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800824 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800825 raise RuntimeError('Register request timeout')
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800826
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800827 self._register_status = response['response']
828 if response['response'] != SUCCESS:
829 self._reset.set()
830 raise RuntimeError('Reigster: ' + response['response'])
831 else:
832 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
833 self._connected_addr = non_local['addr']
834 self.Upgrade() # Check for upgrade
835 self._queue.put('pause', True)
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800836
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800837 try:
838 logging.info('Trying %s:%d ...', *addr)
839 self.Reset()
840 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
841 self._sock.settimeout(_PING_TIMEOUT)
842 self._sock.connect(addr)
843
844 logging.info('Connection established, registering...')
845 handler = {
846 Ghost.AGENT: registered,
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800847 Ghost.TERMINAL: self.SpawnTTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800848 Ghost.SHELL: self.SpawnShellServer,
849 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800850 Ghost.FORWARD: self.SpawnPortForwardServer,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800851 }[self._mode]
852
853 # Machine ID may change if MAC address is used (USB-ethernet dongle
854 # plugged/unplugged)
855 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800856 self.SendRequest('register',
857 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800858 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800859 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800860 except socket.error:
861 pass
862 else:
863 self._sock.settimeout(None)
864 self.Listen()
865
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800866 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800867
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800868 def Reconnect(self):
869 logging.info('Received reconnect request from RPC server, reconnecting...')
870 self._reset.set()
871
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800872 def GetStatus(self):
873 return self._register_status
874
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800875 def AddToDownloadQueue(self, ttyname, filename):
876 self._download_queue.put((ttyname, filename))
877
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800878 def RegisterTTY(self, session_id, ttyname):
879 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +0800880
881 def RegisterSession(self, session_id, process_id):
882 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800883
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800884 def StartLanDiscovery(self):
885 """Start to listen to LAN discovery packet at
886 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800887
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800888 def thread_func():
889 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
890 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
891 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800892 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800893 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
894 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800895 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800896 return
897
898 logging.info('LAN Discovery: started')
899 while True:
900 rd, _, _ = select.select([s], [], [], 1)
901
902 if s in rd:
903 data, source_addr = s.recvfrom(_BUFSIZE)
904 parts = data.split()
905 if parts[0] == 'OVERLORD':
906 ip, port = parts[1].split(':')
907 if not ip:
908 ip = source_addr[0]
909 self._queue.put((ip, int(port)), True)
910
911 try:
912 obj = self._queue.get(False)
913 except Queue.Empty:
914 pass
915 else:
916 if type(obj) is not str:
917 self._queue.put(obj)
918 elif obj == 'pause':
919 logging.info('LAN Discovery: paused')
920 while obj != 'resume':
921 obj = self._queue.get(True)
922 logging.info('LAN Discovery: resumed')
923
924 t = threading.Thread(target=thread_func)
925 t.daemon = True
926 t.start()
927
928 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800929 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800930 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
931 logRequests=False)
932 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800933 rpc_server.register_function(self.GetStatus, 'GetStatus')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800934 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800935 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800936 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800937 t = threading.Thread(target=rpc_server.serve_forever)
938 t.daemon = True
939 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800940
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800941 def ScanServer(self):
942 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
943 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
944 if addr not in self._overlord_addrs:
945 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800946
Wei-Ning Huang11c35022015-10-21 16:52:32 +0800947 def Start(self, lan_disc=False, rpc_server=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800948 logging.info('%s started', self.MODE_NAME[self._mode])
949 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800950 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800951
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800952 # We don't care about child process's return code, not wait is needed. This
953 # is used to prevent zombie process from lingering in the system.
954 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800955
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800956 if lan_disc:
957 self.StartLanDiscovery()
958
959 if rpc_server:
960 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800961
962 try:
963 while True:
964 try:
965 addr = self._queue.get(False)
966 except Queue.Empty:
967 pass
968 else:
969 if type(addr) == tuple and addr not in self._overlord_addrs:
970 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
971 self._overlord_addrs.append(addr)
972
973 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800974 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800975 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +0800976 # Don't show stack trace for RuntimeError, which we use in this file for
977 # plausible and expected errors (such as can't connect to server).
978 except RuntimeError as e:
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800979 logging.info('%s, retrying in %ds', e.message, _RETRY_INTERVAL)
Joel Kitching22b89042015-08-06 18:23:29 +0800980 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800981 except Exception as e:
Joel Kitching22b89042015-08-06 18:23:29 +0800982 _, _, exc_traceback = sys.exc_info()
983 traceback.print_tb(exc_traceback)
984 logging.info('%s: %s, retrying in %ds',
985 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800986 time.sleep(_RETRY_INTERVAL)
987
988 self.Reset()
989 except KeyboardInterrupt:
990 logging.error('Received keyboard interrupt, quit')
991 sys.exit(0)
992
993
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800994def GhostRPCServer():
Wei-Ning Huang8037c182015-09-19 04:41:50 +0800995 """Returns handler to Ghost's JSON RPC server."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800996 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
997
998
Wei-Ning Huang8037c182015-09-19 04:41:50 +0800999def ForkToBackground():
1000 """Fork process to run in background."""
1001 pid = os.fork()
1002 if pid != 0:
1003 logging.info('Ghost(%d) running in background.', pid)
1004 sys.exit(0)
1005
1006
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001007def DownloadFile(filename):
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001008 """Initiate a client-initiated file download."""
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001009 filepath = os.path.abspath(filename)
1010 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001011 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001012 sys.exit(1)
1013
1014 # Check if we actually have permission to read the file
1015 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001016 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001017 sys.exit(1)
1018
1019 server = GhostRPCServer()
1020 server.AddToDownloadQueue(os.ttyname(0), filepath)
1021 sys.exit(0)
1022
1023
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001024def main():
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +08001025 # Setup logging format
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001026 logger = logging.getLogger()
1027 logger.setLevel(logging.INFO)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +08001028 handler = logging.StreamHandler()
1029 formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S')
1030 handler.setFormatter(formatter)
1031 logger.addHandler(handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001032
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001033 parser = argparse.ArgumentParser()
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001034 parser.add_argument('--fork', dest='fork', action='store_true', default=False,
1035 help='fork procecess to run in background')
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001036 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1037 default=None, help='use MID as machine ID')
1038 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1039 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001040 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1041 default=True, help='disable LAN discovery')
1042 parser.add_argument('--no-rpc-server', dest='rpc_server',
1043 action='store_false', default=True,
1044 help='disable RPC server')
Joel Kitching22b89042015-08-06 18:23:29 +08001045 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001046 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001047 help='file containing the JSON representation of client '
1048 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001049 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1050 default=None, help='file to download')
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001051 parser.add_argument('--reset', dest='reset', default=False,
1052 action='store_true',
1053 help='reset ghost and reload all configs')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001054 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1055 nargs='*', help='overlord server address')
1056 args = parser.parse_args()
1057
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001058 if args.fork:
1059 ForkToBackground()
1060
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001061 if args.reset:
1062 GhostRPCServer().Reconnect()
1063 sys.exit()
1064
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001065 if args.download:
1066 DownloadFile(args.download)
1067
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001068 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001069 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001070
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001071 g = Ghost(addrs, Ghost.AGENT, args.mid, prop_file=args.prop_file)
Wei-Ning Huang11c35022015-10-21 16:52:32 +08001072 g.Start(args.lan_disc, args.rpc_server)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001073
1074
1075if __name__ == '__main__':
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001076 main()