blob: 652755deaefc8cd29219e9a4f32d782c5fd36e82 [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 Huange0def6a2015-11-05 15:41:24 +080028import urllib2
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
Wei-Ning Huange0def6a2015-11-05 15:41:24 +080054_CONNECT_TIMEOUT = 3
Wei-Ning Huanga301f572015-06-03 17:34:21 +080055
Wei-Ning Huang7ec55342015-09-17 08:46:06 +080056SUCCESS = 'success'
57FAILED = 'failed'
58DISCONNECTED = 'disconnected'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080059
Joel Kitching22b89042015-08-06 18:23:29 +080060
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080061class PingTimeoutError(Exception):
62 pass
63
64
65class RequestError(Exception):
66 pass
67
68
69class Ghost(object):
70 """Ghost implements the client protocol of Overlord.
71
72 Ghost provide terminal/shell/logcat functionality and manages the client
73 side connectivity.
74 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +080075 NONE, AGENT, TERMINAL, SHELL, LOGCAT, FILE, FORWARD = range(7)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080076
77 MODE_NAME = {
78 NONE: 'NONE',
79 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080080 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080081 SHELL: 'Shell',
Wei-Ning Huanga301f572015-06-03 17:34:21 +080082 LOGCAT: 'Logcat',
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +080083 FILE: 'File',
84 FORWARD: 'Forward'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080085 }
86
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +080087 RANDOM_MID = '##random_mid##'
88
Wei-Ning Huangd521f282015-08-07 05:28:04 +080089 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +080090 prop_file=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +080091 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080092 """Constructor.
93
94 Args:
95 overlord_addrs: a list of possible address of overlord.
96 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +080097 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
98 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +080099 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800100 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800101 prop_file: properties file filename.
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800102 terminal_sid: the terminal session ID associate with this client. This is
103 use for file download.
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800104 tty_device: the terminal device to open, if tty_device is None, as pseudo
105 terminal will be opened instead.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800106 command: the command to execute when we are in SHELL mode.
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800107 file_op: a tuple (action, filepath, perm). action is either 'download' or
108 'upload'. perm is the permission to set for the file.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800109 port: port number to forward.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800110 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800111 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE,
112 Ghost.FORWARD]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800113 if mode == Ghost.SHELL:
114 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800115 if mode == Ghost.FILE:
116 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800117
118 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800119 self._connected_addr = None
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800120 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800121 self._sock = None
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800122 self._mode = mode
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800123 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800124 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800125 self._terminal_session_id = terminal_sid
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800126 self._ttyname_to_sid = {}
127 self._terminal_sid_to_pid = {}
128 self._prop_file = prop_file
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800129 self._properties = {}
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800130 self._register_status = DISCONNECTED
131 self._reset = threading.Event()
132
133 # RPC
134 self._buf = '' # Read buffer
135 self._requests = {}
136 self._queue = Queue.Queue()
137
138 # Protocol specific
139 self._last_ping = 0
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800140 self._tty_device = tty_device
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800141 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800142 self._file_op = file_op
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800143 self._download_queue = Queue.Queue()
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800144 self._port = port
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800145
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800146 def SetIgnoreChild(self, status):
147 # Only ignore child for Agent since only it could spawn child Ghost.
148 if self._mode == Ghost.AGENT:
149 signal.signal(signal.SIGCHLD,
150 signal.SIG_IGN if status else signal.SIG_DFL)
151
152 def GetFileSha1(self, filename):
153 with open(filename, 'r') as f:
154 return hashlib.sha1(f.read()).hexdigest()
155
Wei-Ning Huang58833882015-09-16 16:52:37 +0800156 def UseSSL(self):
157 """Determine if SSL is enabled on the Overlord server."""
158 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
159 try:
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800160 sock.settimeout(_CONNECT_TIMEOUT)
Wei-Ning Huang58833882015-09-16 16:52:37 +0800161 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:
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800179 with contextlib.closing(
180 urllib2.urlopen(url + '.sha1', timeout=_CONNECT_TIMEOUT)) as f:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800181 if f.getcode() != 200:
182 raise RuntimeError('HTTP status %d' % f.getcode())
183 sha1sum = f.read().strip()
184 except Exception:
185 logging.error('Upgrade: failed to download sha1sum file, abort')
186 return
187
188 if self.GetFileSha1(scriptpath) == sha1sum:
189 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
190 return
191
192 # Download upgrade version of ghost.py
193 try:
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800194 with contextlib.closing(
195 urllib2.urlopen(url, timeout=_CONNECT_TIMEOUT)) as f:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800196 if f.getcode() != 200:
197 raise RuntimeError('HTTP status %d' % f.getcode())
198 data = f.read()
199 except Exception:
200 logging.error('Upgrade: failed to download upgrade, abort')
201 return
202
203 # Compare SHA1 sum
204 if hashlib.sha1(data).hexdigest() != sha1sum:
205 logging.error('Upgrade: sha1sum mismatch, abort')
206 return
207
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800208 try:
209 with open(scriptpath, 'w') as f:
210 f.write(data)
211 except Exception:
212 logging.error('Upgrade: failed to write upgrade onto disk, abort')
213 return
214
215 logging.info('Upgrade: restarting ghost...')
216 self.CloseSockets()
217 self.SetIgnoreChild(False)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800218 os.execve(scriptpath, [scriptpath] + sys.argv[1:], os.environ)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800219
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800220 def LoadProperties(self):
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800221 try:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800222 if self._prop_file:
223 with open(self._prop_file, 'r') as f:
224 self._properties = json.loads(f.read())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800225 except Exception as e:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800226 logging.exception('LoadProperties: ' + str(e))
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800227
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800228 def CloseSockets(self):
229 # Close sockets opened by parent process, since we don't use it anymore.
230 for fd in os.listdir('/proc/self/fd/'):
231 try:
232 real_fd = os.readlink('/proc/self/fd/%s' % fd)
233 if real_fd.startswith('socket'):
234 os.close(int(fd))
235 except Exception:
236 pass
237
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800238 def SpawnGhost(self, mode, sid=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800239 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800240 """Spawn a child ghost with specific mode.
241
242 Returns:
243 The spawned child process pid.
244 """
Joel Kitching22b89042015-08-06 18:23:29 +0800245 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800246 self.SetIgnoreChild(False)
247
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800248 pid = os.fork()
249 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800250 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800251 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800252 terminal_sid=terminal_sid, tty_device=tty_device,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800253 command=command, file_op=file_op, port=port)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800254 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800255 sys.exit(0)
256 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800257 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800258 return pid
259
260 def Timestamp(self):
261 return int(time.time())
262
263 def GetGateWayIP(self):
264 with open('/proc/net/route', 'r') as f:
265 lines = f.readlines()
266
267 ips = []
268 for line in lines:
269 parts = line.split('\t')
270 if parts[2] == '00000000':
271 continue
272
273 try:
274 h = parts[2].decode('hex')
275 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
276 except TypeError:
277 pass
278
279 return ips
280
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800281 def GetShopfloorIP(self):
282 try:
283 import factory_common # pylint: disable=W0612
284 from cros.factory.test import shopfloor
285
286 url = shopfloor.get_server_url()
287 match = re.match(r'^https?://(.*):.*$', url)
288 if match:
289 return [match.group(1)]
290 except Exception:
291 pass
292 return []
293
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800294 def GetMachineID(self):
295 """Generates machine-dependent ID string for a machine.
296 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800297 1. factory device_id
298 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800299 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
300 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800301 We follow the listed order to generate machine ID, and fallback to the next
302 alternative if the previous doesn't work.
303 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800304 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800305 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800306 elif self._mid:
307 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800308
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800309 # Try factory device id
310 try:
311 import factory_common # pylint: disable=W0612
312 from cros.factory.test import event_log
313 with open(event_log.DEVICE_ID_PATH) as f:
314 return f.read().strip()
315 except Exception:
316 pass
317
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800318 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800319 try:
320 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
321 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800322 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800323 stdout, _ = p.communicate()
324 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800325 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800326 return stdout.strip()
327 except Exception:
328 pass
329
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800330 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800331 try:
332 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
333 return f.read().strip()
334 except Exception:
335 pass
336
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800337 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800338 try:
339 macs = []
340 ifaces = sorted(os.listdir('/sys/class/net'))
341 for iface in ifaces:
342 if iface == 'lo':
343 continue
344
345 with open('/sys/class/net/%s/address' % iface, 'r') as f:
346 macs.append(f.read().strip())
347
348 return ';'.join(macs)
349 except Exception:
350 pass
351
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800352 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800353
354 def Reset(self):
355 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800356 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800357 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800358 self._last_ping = 0
359 self._requests = {}
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800360 self.LoadProperties()
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800361 self._register_status = DISCONNECTED
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800362
363 def SendMessage(self, msg):
364 """Serialize the message and send it through the socket."""
365 self._sock.send(json.dumps(msg) + _SEPARATOR)
366
367 def SendRequest(self, name, args, handler=None,
368 timeout=_REQUEST_TIMEOUT_SECS):
369 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800370 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800371
372 rid = str(uuid.uuid4())
373 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800374 if timeout >= 0:
375 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800376 self.SendMessage(msg)
377
378 def SendResponse(self, omsg, status, params=None):
379 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
380 self.SendMessage(msg)
381
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800382 def HandleTTYControl(self, fd, control_string):
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800383 msg = json.loads(control_string)
384 command = msg['command']
385 params = msg['params']
386 if command == 'resize':
387 # some error happened on websocket
388 if len(params) != 2:
389 return
390 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
391 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
392 else:
393 logging.warn('Invalid request command "%s"', command)
394
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800395 def SpawnTTYServer(self, _):
396 """Spawn a TTY server and forward I/O to the TCP socket."""
397 logging.info('SpawnTTYServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800398
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800399 try:
400 if self._tty_device is None:
401 pid, fd = os.forkpty()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800402
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800403 if pid == 0:
404 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
405 try:
406 server = GhostRPCServer()
407 server.RegisterTTY(self._session_id, ttyname)
408 server.RegisterSession(self._session_id, os.getpid())
409 except Exception:
410 # If ghost is launched without RPC server, the call will fail but we
411 # can ignore it.
412 pass
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800413
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800414 # The directory that contains the current running ghost script
415 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800416
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800417 env = os.environ.copy()
418 env['USER'] = os.getenv('USER', 'root')
419 env['HOME'] = os.getenv('HOME', '/root')
420 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
421 os.chdir(env['HOME'])
422 os.execve(_SHELL, [_SHELL], env)
423 else:
424 fd = os.open(self._tty_device, os.O_RDWR)
Wei-Ning Huang39169902015-09-19 06:00:23 +0800425 tty.setraw(fd)
426 attr = termios.tcgetattr(fd)
427 attr[0] &= ~(termios.IXON | termios.IXOFF)
428 attr[2] |= termios.CLOCAL
429 attr[2] &= ~termios.CRTSCTS
430 attr[4] = termios.B115200
431 attr[5] = termios.B115200
432 termios.tcsetattr(fd, termios.TCSANOW, attr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800433
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800434 control_state = None
435 control_string = ''
436 write_buffer = ''
437 while True:
438 rd, _, _ = select.select([self._sock, fd], [], [])
439
440 if fd in rd:
441 self._sock.send(os.read(fd, _BUFSIZE))
442
443 if self._sock in rd:
444 ret = self._sock.recv(_BUFSIZE)
445 if len(ret) == 0:
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800446 raise RuntimeError('connection terminated')
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800447 while ret:
448 if control_state:
449 if chr(_CONTROL_END) in ret:
450 index = ret.index(chr(_CONTROL_END))
451 control_string += ret[:index]
452 self.HandleTTYControl(fd, control_string)
453 control_state = None
454 control_string = ''
455 ret = ret[index+1:]
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800456 else:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800457 control_string += ret
458 ret = ''
459 else:
460 if chr(_CONTROL_START) in ret:
461 control_state = _CONTROL_START
462 index = ret.index(chr(_CONTROL_START))
463 write_buffer += ret[:index]
464 ret = ret[index+1:]
465 else:
466 write_buffer += ret
467 ret = ''
468 if write_buffer:
469 os.write(fd, write_buffer)
470 write_buffer = ''
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800471 except Exception as e:
472 logging.error('SpawnTTYServer: %s', e)
473 finally:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800474 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800475
476 logging.info('SpawnTTYServer: terminated')
477 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800478
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800479 def SpawnShellServer(self, _):
480 """Spawn a shell server and forward input/output from/to the TCP socket."""
481 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800482
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800483 # Add ghost executable to PATH
484 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
485 env = os.environ.copy()
486 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
487
488 # Execute shell command from HOME directory
489 os.chdir(os.getenv('HOME', '/tmp'))
490
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800491 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800492 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800493 shell=True, env=env)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800494
495 def make_non_block(fd):
496 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
497 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
498
499 make_non_block(p.stdout)
500 make_non_block(p.stderr)
501
502 try:
503 while True:
504 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800505 if p.stdout in rd:
506 self._sock.send(p.stdout.read(_BUFSIZE))
507
508 if p.stderr in rd:
509 self._sock.send(p.stderr.read(_BUFSIZE))
510
511 if self._sock in rd:
512 ret = self._sock.recv(_BUFSIZE)
513 if len(ret) == 0:
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800514 raise RuntimeError('connection terminated')
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800515 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800516 p.poll()
517 if p.returncode != None:
518 break
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800519 except Exception as e:
520 logging.error('SpawnShellServer: %s', e)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800521 finally:
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800522 # Send SIGTERM to process, then wait for 1 second. Send another SIGKILL
523 # to make sure the process is terminated.
524 try:
525 p.terminate()
526 time.sleep(1)
527 p.kill()
528 except Exception:
529 pass
530
531 p.wait()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800532 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800533
534 logging.info('SpawnShellServer: terminated')
535 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800536
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800537 def InitiateFileOperation(self, _):
538 if self._file_op[0] == 'download':
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800539 try:
540 size = os.stat(self._file_op[1]).st_size
541 except OSError as e:
542 logging.error('InitiateFileOperation: download: %s', e)
543 sys.exit(1)
544
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800545 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800546 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800547 'filename': os.path.basename(self._file_op[1]),
548 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800549 elif self._file_op[0] == 'upload':
550 self.SendRequest('clear_to_upload', {}, timeout=-1)
551 self.StartUploadServer()
552 else:
553 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800554
555 def StartDownloadServer(self):
556 logging.info('StartDownloadServer: started')
557
558 try:
559 with open(self._file_op[1], 'rb') as f:
560 while True:
561 data = f.read(_BLOCK_SIZE)
562 if len(data) == 0:
563 break
564 self._sock.send(data)
565 except Exception as e:
566 logging.error('StartDownloadServer: %s', e)
567 finally:
568 self._sock.close()
569
570 logging.info('StartDownloadServer: terminated')
571 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800572
Wei-Ning Huange2981862015-08-03 15:03:08 +0800573 def StartUploadServer(self):
574 logging.info('StartUploadServer: started')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800575 try:
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800576 filepath = self._file_op[1]
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800577 dirname = os.path.dirname(filepath)
578 if not os.path.exists(dirname):
579 try:
580 os.makedirs(dirname)
581 except Exception:
582 pass
Wei-Ning Huange2981862015-08-03 15:03:08 +0800583
584 self._sock.setblocking(False)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800585 with open(filepath, 'wb') as f:
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800586 if self._file_op[2]:
587 os.fchmod(f.fileno(), self._file_op[2])
588
Wei-Ning Huange2981862015-08-03 15:03:08 +0800589 while True:
590 rd, _, _ = select.select([self._sock], [], [])
591 if self._sock in rd:
592 buf = self._sock.recv(_BLOCK_SIZE)
593 if len(buf) == 0:
594 break
595 f.write(buf)
596 except socket.error as e:
597 logging.error('StartUploadServer: socket error: %s', e)
598 except Exception as e:
599 logging.error('StartUploadServer: %s', e)
600 finally:
601 self._sock.close()
602
603 logging.info('StartUploadServer: terminated')
604 sys.exit(0)
605
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800606 def SpawnPortForwardServer(self, _):
607 """Spawn a port forwarding server and forward I/O to the TCP socket."""
608 logging.info('SpawnPortForwardServer: started')
609
610 src_sock = None
611 try:
612 src_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800613 src_sock.settimeout(_CONNECT_TIMEOUT)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800614 src_sock.connect(('localhost', self._port))
615 src_sock.setblocking(False)
616
617 # Pass the leftovers of the previous buffer
618 if self._buf:
619 src_sock.send(self._buf)
620 self._buf = ''
621
622 while True:
623 rd, _, _ = select.select([self._sock, src_sock], [], [])
624
625 if self._sock in rd:
626 data = self._sock.recv(_BUFSIZE)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800627 if len(data) == 0:
628 raise RuntimeError('connection terminated')
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800629 src_sock.send(data)
630
631 if src_sock in rd:
632 data = src_sock.recv(_BUFSIZE)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800633 if len(data) == 0:
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800634 break
635 self._sock.send(data)
636 except Exception as e:
637 logging.error('SpawnPortForwardServer: %s', e)
638 finally:
639 if src_sock:
640 src_sock.close()
641 self._sock.close()
642
643 logging.info('SpawnPortForwardServer: terminated')
644 sys.exit(0)
645
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800646 def Ping(self):
647 def timeout_handler(x):
648 if x is None:
649 raise PingTimeoutError
650
651 self._last_ping = self.Timestamp()
652 self.SendRequest('ping', {}, timeout_handler, 5)
653
Wei-Ning Huangae923642015-09-24 14:08:09 +0800654 def HandleFileDownloadRequest(self, msg):
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800655 params = msg['params']
Wei-Ning Huangae923642015-09-24 14:08:09 +0800656 filepath = params['filename']
657 if not os.path.isabs(filepath):
658 filepath = os.path.join(os.getenv('HOME', '/tmp'), filepath)
659
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800660 try:
Wei-Ning Huang11c35022015-10-21 16:52:32 +0800661 with open(filepath, 'r') as _:
Wei-Ning Huang46a3fc92015-10-06 02:35:27 +0800662 pass
663 except Exception as e:
Wei-Ning Huangae923642015-09-24 14:08:09 +0800664 return self.SendResponse(msg, str(e))
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800665
666 self.SpawnGhost(self.FILE, params['sid'],
Wei-Ning Huangae923642015-09-24 14:08:09 +0800667 file_op=('download', filepath))
668 self.SendResponse(msg, SUCCESS)
669
670 def HandleFileUploadRequest(self, msg):
671 params = msg['params']
672
673 # Resolve upload filepath
674 filename = params['filename']
675 dest_path = filename
676
677 # If dest is specified, use it first
678 dest_path = params.get('dest', '')
679 if dest_path:
680 if not os.path.isabs(dest_path):
681 dest_path = os.path.join(os.getenv('HOME', '/tmp'), dest_path)
682
683 if os.path.isdir(dest_path):
684 dest_path = os.path.join(dest_path, filename)
685 else:
686 target_dir = os.getenv('HOME', '/tmp')
687
688 # Terminal session ID found, upload to it's current working directory
689 if params.has_key('terminal_sid'):
690 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
691 if pid:
692 target_dir = os.readlink('/proc/%d/cwd' % pid)
693
694 dest_path = os.path.join(target_dir, filename)
695
696 try:
697 os.makedirs(os.path.dirname(dest_path))
698 except Exception:
699 pass
700
701 try:
702 with open(dest_path, 'w') as _:
703 pass
704 except Exception as e:
705 return self.SendResponse(msg, str(e))
706
Wei-Ning Huangd6f69762015-10-01 21:02:07 +0800707 # If not check_only, spawn FILE mode ghost agent to handle upload
708 if not params.get('check_only', False):
709 self.SpawnGhost(self.FILE, params['sid'],
710 file_op=('upload', dest_path, params.get('perm', None)))
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800711 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800712
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800713 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800714 command = msg['name']
715 params = msg['params']
716
717 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800718 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800719 elif command == 'terminal':
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800720 self.SpawnGhost(self.TERMINAL, params['sid'],
721 tty_device=params['tty_device'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800722 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800723 elif command == 'shell':
724 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800725 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800726 elif command == 'file_download':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800727 self.HandleFileDownloadRequest(msg)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800728 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800729 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800730 elif command == 'file_upload':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800731 self.HandleFileUploadRequest(msg)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800732 elif command == 'forward':
733 self.SpawnGhost(self.FORWARD, params['sid'], port=params['port'])
734 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800735
736 def HandleResponse(self, response):
737 rid = str(response['rid'])
738 if rid in self._requests:
739 handler = self._requests[rid][2]
740 del self._requests[rid]
741 if callable(handler):
742 handler(response)
743 else:
Joel Kitching22b89042015-08-06 18:23:29 +0800744 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800745
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800746 def ParseMessage(self, single=True):
747 if single:
748 index = self._buf.index(_SEPARATOR)
749 msgs_json = [self._buf[:index]]
750 self._buf = self._buf[index + 2:]
751 else:
752 msgs_json = self._buf.split(_SEPARATOR)
753 self._buf = msgs_json.pop()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800754
755 for msg_json in msgs_json:
756 try:
757 msg = json.loads(msg_json)
758 except ValueError:
759 # Ignore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800760 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800761 continue
762
763 if 'name' in msg:
764 self.HandleRequest(msg)
765 elif 'response' in msg:
766 self.HandleResponse(msg)
767 else: # Ingnore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800768 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800769
770 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800771 """Scans for pending requests which have timed out.
772
773 If any timed-out requests are discovered, their handler is called with the
774 special response value of None.
775 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800776 for rid in self._requests.keys()[:]:
777 request_time, timeout, handler = self._requests[rid]
778 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800779 if callable(handler):
780 handler(None)
781 else:
782 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800783 del self._requests[rid]
784
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800785 def InitiateDownload(self):
786 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800787 sid = self._ttyname_to_sid[ttyname]
788 self.SpawnGhost(self.FILE, terminal_sid=sid,
Wei-Ning Huangae923642015-09-24 14:08:09 +0800789 file_op=('download', filename))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800790
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800791 def Listen(self):
792 try:
793 while True:
794 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
795
796 if self._sock in rds:
797 self._buf += self._sock.recv(_BUFSIZE)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800798 self.ParseMessage(self._register_status != SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800799
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800800 if (self._mode == self.AGENT and
801 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800802 self.Ping()
803 self.ScanForTimeoutRequests()
804
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800805 if not self._download_queue.empty():
806 self.InitiateDownload()
807
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800808 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800809 self.Reset()
810 break
811 except socket.error:
812 raise RuntimeError('Connection dropped')
813 except PingTimeoutError:
814 raise RuntimeError('Connection timeout')
815 finally:
816 self._sock.close()
817
818 self._queue.put('resume')
819
820 if self._mode != Ghost.AGENT:
821 sys.exit(1)
822
823 def Register(self):
824 non_local = {}
825 for addr in self._overlord_addrs:
826 non_local['addr'] = addr
827 def registered(response):
828 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800829 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800830 raise RuntimeError('Register request timeout')
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800831
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800832 self._register_status = response['response']
833 if response['response'] != SUCCESS:
834 self._reset.set()
835 raise RuntimeError('Reigster: ' + response['response'])
836 else:
837 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
838 self._connected_addr = non_local['addr']
839 self.Upgrade() # Check for upgrade
840 self._queue.put('pause', True)
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800841
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800842 try:
843 logging.info('Trying %s:%d ...', *addr)
844 self.Reset()
845 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800846 self._sock.settimeout(_CONNECT_TIMEOUT)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800847 self._sock.connect(addr)
848
849 logging.info('Connection established, registering...')
850 handler = {
851 Ghost.AGENT: registered,
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800852 Ghost.TERMINAL: self.SpawnTTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800853 Ghost.SHELL: self.SpawnShellServer,
854 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800855 Ghost.FORWARD: self.SpawnPortForwardServer,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800856 }[self._mode]
857
858 # Machine ID may change if MAC address is used (USB-ethernet dongle
859 # plugged/unplugged)
860 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800861 self.SendRequest('register',
862 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800863 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800864 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800865 except socket.error:
866 pass
867 else:
868 self._sock.settimeout(None)
869 self.Listen()
870
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800871 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800872
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800873 def Reconnect(self):
874 logging.info('Received reconnect request from RPC server, reconnecting...')
875 self._reset.set()
876
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800877 def GetStatus(self):
878 return self._register_status
879
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800880 def AddToDownloadQueue(self, ttyname, filename):
881 self._download_queue.put((ttyname, filename))
882
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800883 def RegisterTTY(self, session_id, ttyname):
884 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +0800885
886 def RegisterSession(self, session_id, process_id):
887 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800888
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800889 def StartLanDiscovery(self):
890 """Start to listen to LAN discovery packet at
891 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800892
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800893 def thread_func():
894 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
895 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
896 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800897 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800898 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
899 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800900 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800901 return
902
903 logging.info('LAN Discovery: started')
904 while True:
905 rd, _, _ = select.select([s], [], [], 1)
906
907 if s in rd:
908 data, source_addr = s.recvfrom(_BUFSIZE)
909 parts = data.split()
910 if parts[0] == 'OVERLORD':
911 ip, port = parts[1].split(':')
912 if not ip:
913 ip = source_addr[0]
914 self._queue.put((ip, int(port)), True)
915
916 try:
917 obj = self._queue.get(False)
918 except Queue.Empty:
919 pass
920 else:
921 if type(obj) is not str:
922 self._queue.put(obj)
923 elif obj == 'pause':
924 logging.info('LAN Discovery: paused')
925 while obj != 'resume':
926 obj = self._queue.get(True)
927 logging.info('LAN Discovery: resumed')
928
929 t = threading.Thread(target=thread_func)
930 t.daemon = True
931 t.start()
932
933 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800934 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800935 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
936 logRequests=False)
937 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800938 rpc_server.register_function(self.GetStatus, 'GetStatus')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800939 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800940 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800941 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800942 t = threading.Thread(target=rpc_server.serve_forever)
943 t.daemon = True
944 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800945
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800946 def ScanServer(self):
947 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
948 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
949 if addr not in self._overlord_addrs:
950 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800951
Wei-Ning Huang11c35022015-10-21 16:52:32 +0800952 def Start(self, lan_disc=False, rpc_server=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800953 logging.info('%s started', self.MODE_NAME[self._mode])
954 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800955 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800956
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800957 # We don't care about child process's return code, not wait is needed. This
958 # is used to prevent zombie process from lingering in the system.
959 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800960
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800961 if lan_disc:
962 self.StartLanDiscovery()
963
964 if rpc_server:
965 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800966
967 try:
968 while True:
969 try:
970 addr = self._queue.get(False)
971 except Queue.Empty:
972 pass
973 else:
974 if type(addr) == tuple and addr not in self._overlord_addrs:
975 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
976 self._overlord_addrs.append(addr)
977
978 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800979 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800980 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +0800981 # Don't show stack trace for RuntimeError, which we use in this file for
982 # plausible and expected errors (such as can't connect to server).
983 except RuntimeError as e:
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800984 logging.info('%s, retrying in %ds', e.message, _RETRY_INTERVAL)
Joel Kitching22b89042015-08-06 18:23:29 +0800985 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800986 except Exception as e:
Joel Kitching22b89042015-08-06 18:23:29 +0800987 _, _, exc_traceback = sys.exc_info()
988 traceback.print_tb(exc_traceback)
989 logging.info('%s: %s, retrying in %ds',
990 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800991 time.sleep(_RETRY_INTERVAL)
992
993 self.Reset()
994 except KeyboardInterrupt:
995 logging.error('Received keyboard interrupt, quit')
996 sys.exit(0)
997
998
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800999def GhostRPCServer():
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001000 """Returns handler to Ghost's JSON RPC server."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001001 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1002
1003
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001004def ForkToBackground():
1005 """Fork process to run in background."""
1006 pid = os.fork()
1007 if pid != 0:
1008 logging.info('Ghost(%d) running in background.', pid)
1009 sys.exit(0)
1010
1011
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001012def DownloadFile(filename):
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001013 """Initiate a client-initiated file download."""
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001014 filepath = os.path.abspath(filename)
1015 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001016 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001017 sys.exit(1)
1018
1019 # Check if we actually have permission to read the file
1020 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001021 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001022 sys.exit(1)
1023
1024 server = GhostRPCServer()
1025 server.AddToDownloadQueue(os.ttyname(0), filepath)
1026 sys.exit(0)
1027
1028
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001029def main():
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +08001030 # Setup logging format
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001031 logger = logging.getLogger()
1032 logger.setLevel(logging.INFO)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +08001033 handler = logging.StreamHandler()
1034 formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S')
1035 handler.setFormatter(formatter)
1036 logger.addHandler(handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001037
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001038 parser = argparse.ArgumentParser()
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001039 parser.add_argument('--fork', dest='fork', action='store_true', default=False,
1040 help='fork procecess to run in background')
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001041 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1042 default=None, help='use MID as machine ID')
1043 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1044 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001045 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1046 default=True, help='disable LAN discovery')
1047 parser.add_argument('--no-rpc-server', dest='rpc_server',
1048 action='store_false', default=True,
1049 help='disable RPC server')
Joel Kitching22b89042015-08-06 18:23:29 +08001050 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001051 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001052 help='file containing the JSON representation of client '
1053 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001054 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1055 default=None, help='file to download')
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001056 parser.add_argument('--reset', dest='reset', default=False,
1057 action='store_true',
1058 help='reset ghost and reload all configs')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001059 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1060 nargs='*', help='overlord server address')
1061 args = parser.parse_args()
1062
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001063 if args.fork:
1064 ForkToBackground()
1065
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001066 if args.reset:
1067 GhostRPCServer().Reconnect()
1068 sys.exit()
1069
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001070 if args.download:
1071 DownloadFile(args.download)
1072
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001073 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001074 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001075
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001076 g = Ghost(addrs, Ghost.AGENT, args.mid, prop_file=args.prop_file)
Wei-Ning Huang11c35022015-10-21 16:52:32 +08001077 g.Start(args.lan_disc, args.rpc_server)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001078
1079
1080if __name__ == '__main__':
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001081 main()