blob: 8f7de484514832050a5ab253617ba5f80d8bbea3 [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 Huang9083b7c2016-01-26 16:44:11 +080056# Stream control
57_STDIN_CLOSED = '##STDIN_CLOSED##'
58
Wei-Ning Huang7ec55342015-09-17 08:46:06 +080059SUCCESS = 'success'
60FAILED = 'failed'
61DISCONNECTED = 'disconnected'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080062
Joel Kitching22b89042015-08-06 18:23:29 +080063
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080064class PingTimeoutError(Exception):
65 pass
66
67
68class RequestError(Exception):
69 pass
70
71
Wei-Ning Huanga28cd232016-01-27 15:04:41 +080072class BufferedSocket(socket.socket):
73 """A buffered socket that supports unrecv.
74
75 Allow putting back data back to the socket for the next recv() call.
76 """
77 def __init__(self, *args, **kwargs):
78 super(BufferedSocket, self).__init__(*args, **kwargs)
79 self._buf = ''
80
81 def Recv(self, bufsize, flags=0):
82 if self._buf:
83 if len(self._buf) >= bufsize:
84 ret = self._buf[:bufsize]
85 self._buf = self._buf[bufsize:]
86 return ret
87 else:
88 ret = self._buf
89 self._buf = ''
90 return ret + super(BufferedSocket, self).recv(bufsize - len(ret), flags)
91 else:
92 return super(BufferedSocket, self).recv(bufsize, flags)
93
94 def UnRecv(self, buf):
95 self._buf = buf + self._buf
96
97 def Send(self, *args, **kwargs):
98 return super(BufferedSocket, self).send(*args, **kwargs)
99
100 def RecvBuf(self):
101 """Only recive from buffer."""
102 ret = self._buf
103 self._buf = ''
104 return ret
105
106
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800107class Ghost(object):
108 """Ghost implements the client protocol of Overlord.
109
110 Ghost provide terminal/shell/logcat functionality and manages the client
111 side connectivity.
112 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800113 NONE, AGENT, TERMINAL, SHELL, LOGCAT, FILE, FORWARD = range(7)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800114
115 MODE_NAME = {
116 NONE: 'NONE',
117 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800118 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800119 SHELL: 'Shell',
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800120 LOGCAT: 'Logcat',
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800121 FILE: 'File',
122 FORWARD: 'Forward'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800123 }
124
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800125 RANDOM_MID = '##random_mid##'
126
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800127 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800128 prop_file=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800129 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800130 """Constructor.
131
132 Args:
133 overlord_addrs: a list of possible address of overlord.
134 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800135 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
136 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800137 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800138 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800139 prop_file: properties file filename.
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800140 terminal_sid: the terminal session ID associate with this client. This is
141 use for file download.
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800142 tty_device: the terminal device to open, if tty_device is None, as pseudo
143 terminal will be opened instead.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800144 command: the command to execute when we are in SHELL mode.
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800145 file_op: a tuple (action, filepath, perm). action is either 'download' or
146 'upload'. perm is the permission to set for the file.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800147 port: port number to forward.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800148 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800149 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE,
150 Ghost.FORWARD]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800151 if mode == Ghost.SHELL:
152 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800153 if mode == Ghost.FILE:
154 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800155
156 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800157 self._connected_addr = None
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800158 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800159 self._sock = None
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800160 self._mode = mode
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800161 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800162 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800163 self._terminal_session_id = terminal_sid
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800164 self._ttyname_to_sid = {}
165 self._terminal_sid_to_pid = {}
166 self._prop_file = prop_file
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800167 self._properties = {}
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800168 self._register_status = DISCONNECTED
169 self._reset = threading.Event()
170
171 # RPC
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800172 self._requests = {}
173 self._queue = Queue.Queue()
174
175 # Protocol specific
176 self._last_ping = 0
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800177 self._tty_device = tty_device
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800178 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800179 self._file_op = file_op
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800180 self._download_queue = Queue.Queue()
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800181 self._port = port
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800182
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800183 def SetIgnoreChild(self, status):
184 # Only ignore child for Agent since only it could spawn child Ghost.
185 if self._mode == Ghost.AGENT:
186 signal.signal(signal.SIGCHLD,
187 signal.SIG_IGN if status else signal.SIG_DFL)
188
189 def GetFileSha1(self, filename):
190 with open(filename, 'r') as f:
191 return hashlib.sha1(f.read()).hexdigest()
192
Wei-Ning Huang58833882015-09-16 16:52:37 +0800193 def UseSSL(self):
194 """Determine if SSL is enabled on the Overlord server."""
195 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
196 try:
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800197 sock.settimeout(_CONNECT_TIMEOUT)
Wei-Ning Huang58833882015-09-16 16:52:37 +0800198 sock.connect((self._connected_addr[0], _OVERLORD_HTTP_PORT))
199 sock.send('GET\r\n')
200
201 data = sock.recv(16)
202 return 'HTTP' not in data
203 except Exception:
204 return False # For whatever reason above failed, assume HTTP
205
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800206 def Upgrade(self):
207 logging.info('Upgrade: initiating upgrade sequence...')
208
209 scriptpath = os.path.abspath(sys.argv[0])
Wei-Ning Huang03f9f762015-09-16 21:51:35 +0800210 url = 'http%s://%s:%d/upgrade/ghost.py' % (
211 's' if self.UseSSL() else '', self._connected_addr[0],
212 _OVERLORD_HTTP_PORT)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800213
214 # Download sha1sum for ghost.py for verification
215 try:
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800216 with contextlib.closing(
217 urllib2.urlopen(url + '.sha1', timeout=_CONNECT_TIMEOUT)) as f:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800218 if f.getcode() != 200:
219 raise RuntimeError('HTTP status %d' % f.getcode())
220 sha1sum = f.read().strip()
221 except Exception:
222 logging.error('Upgrade: failed to download sha1sum file, abort')
223 return
224
225 if self.GetFileSha1(scriptpath) == sha1sum:
226 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
227 return
228
229 # Download upgrade version of ghost.py
230 try:
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800231 with contextlib.closing(
232 urllib2.urlopen(url, timeout=_CONNECT_TIMEOUT)) as f:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800233 if f.getcode() != 200:
234 raise RuntimeError('HTTP status %d' % f.getcode())
235 data = f.read()
236 except Exception:
237 logging.error('Upgrade: failed to download upgrade, abort')
238 return
239
240 # Compare SHA1 sum
241 if hashlib.sha1(data).hexdigest() != sha1sum:
242 logging.error('Upgrade: sha1sum mismatch, abort')
243 return
244
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800245 try:
246 with open(scriptpath, 'w') as f:
247 f.write(data)
248 except Exception:
249 logging.error('Upgrade: failed to write upgrade onto disk, abort')
250 return
251
252 logging.info('Upgrade: restarting ghost...')
253 self.CloseSockets()
254 self.SetIgnoreChild(False)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800255 os.execve(scriptpath, [scriptpath] + sys.argv[1:], os.environ)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800256
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800257 def LoadProperties(self):
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800258 try:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800259 if self._prop_file:
260 with open(self._prop_file, 'r') as f:
261 self._properties = json.loads(f.read())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800262 except Exception as e:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800263 logging.exception('LoadProperties: ' + str(e))
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800264
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800265 def CloseSockets(self):
266 # Close sockets opened by parent process, since we don't use it anymore.
267 for fd in os.listdir('/proc/self/fd/'):
268 try:
269 real_fd = os.readlink('/proc/self/fd/%s' % fd)
270 if real_fd.startswith('socket'):
271 os.close(int(fd))
272 except Exception:
273 pass
274
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800275 def SpawnGhost(self, mode, sid=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800276 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800277 """Spawn a child ghost with specific mode.
278
279 Returns:
280 The spawned child process pid.
281 """
Joel Kitching22b89042015-08-06 18:23:29 +0800282 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800283 self.SetIgnoreChild(False)
284
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800285 pid = os.fork()
286 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800287 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800288 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800289 terminal_sid=terminal_sid, tty_device=tty_device,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800290 command=command, file_op=file_op, port=port)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800291 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800292 sys.exit(0)
293 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800294 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800295 return pid
296
297 def Timestamp(self):
298 return int(time.time())
299
300 def GetGateWayIP(self):
301 with open('/proc/net/route', 'r') as f:
302 lines = f.readlines()
303
304 ips = []
305 for line in lines:
306 parts = line.split('\t')
307 if parts[2] == '00000000':
308 continue
309
310 try:
311 h = parts[2].decode('hex')
312 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
313 except TypeError:
314 pass
315
316 return ips
317
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800318 def GetShopfloorIP(self):
319 try:
320 import factory_common # pylint: disable=W0612
321 from cros.factory.test import shopfloor
322
323 url = shopfloor.get_server_url()
324 match = re.match(r'^https?://(.*):.*$', url)
325 if match:
326 return [match.group(1)]
327 except Exception:
328 pass
329 return []
330
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800331 def GetMachineID(self):
332 """Generates machine-dependent ID string for a machine.
333 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800334 1. factory device_id
335 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800336 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
337 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800338 We follow the listed order to generate machine ID, and fallback to the next
339 alternative if the previous doesn't work.
340 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800341 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800342 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800343 elif self._mid:
344 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800345
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800346 # Try factory device id
347 try:
348 import factory_common # pylint: disable=W0612
349 from cros.factory.test import event_log
350 with open(event_log.DEVICE_ID_PATH) as f:
351 return f.read().strip()
352 except Exception:
353 pass
354
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800355 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800356 try:
357 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
358 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800359 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800360 stdout, unused_stderr = p.communicate()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800361 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800362 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800363 return stdout.strip()
364 except Exception:
365 pass
366
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800367 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800368 try:
369 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
370 return f.read().strip()
371 except Exception:
372 pass
373
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800374 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800375 try:
376 macs = []
377 ifaces = sorted(os.listdir('/sys/class/net'))
378 for iface in ifaces:
379 if iface == 'lo':
380 continue
381
382 with open('/sys/class/net/%s/address' % iface, 'r') as f:
383 macs.append(f.read().strip())
384
385 return ';'.join(macs)
386 except Exception:
387 pass
388
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800389 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800390
391 def Reset(self):
392 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800393 self._reset.clear()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800394 self._last_ping = 0
395 self._requests = {}
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800396 self.LoadProperties()
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800397 self._register_status = DISCONNECTED
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800398
399 def SendMessage(self, msg):
400 """Serialize the message and send it through the socket."""
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800401 self._sock.Send(json.dumps(msg) + _SEPARATOR)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800402
403 def SendRequest(self, name, args, handler=None,
404 timeout=_REQUEST_TIMEOUT_SECS):
405 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800406 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800407
408 rid = str(uuid.uuid4())
409 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800410 if timeout >= 0:
411 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800412 self.SendMessage(msg)
413
414 def SendResponse(self, omsg, status, params=None):
415 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
416 self.SendMessage(msg)
417
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800418 def HandleTTYControl(self, fd, control_str):
419 msg = json.loads(control_str)
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800420 command = msg['command']
421 params = msg['params']
422 if command == 'resize':
423 # some error happened on websocket
424 if len(params) != 2:
425 return
426 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
427 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
428 else:
429 logging.warn('Invalid request command "%s"', command)
430
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800431 def SpawnTTYServer(self, unused_var):
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800432 """Spawn a TTY server and forward I/O to the TCP socket."""
433 logging.info('SpawnTTYServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800434
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800435 try:
436 if self._tty_device is None:
437 pid, fd = os.forkpty()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800438
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800439 if pid == 0:
440 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
441 try:
442 server = GhostRPCServer()
443 server.RegisterTTY(self._session_id, ttyname)
444 server.RegisterSession(self._session_id, os.getpid())
445 except Exception:
446 # If ghost is launched without RPC server, the call will fail but we
447 # can ignore it.
448 pass
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800449
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800450 # The directory that contains the current running ghost script
451 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800452
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800453 env = os.environ.copy()
454 env['USER'] = os.getenv('USER', 'root')
455 env['HOME'] = os.getenv('HOME', '/root')
456 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
457 os.chdir(env['HOME'])
458 os.execve(_SHELL, [_SHELL], env)
459 else:
460 fd = os.open(self._tty_device, os.O_RDWR)
Wei-Ning Huang39169902015-09-19 06:00:23 +0800461 tty.setraw(fd)
462 attr = termios.tcgetattr(fd)
463 attr[0] &= ~(termios.IXON | termios.IXOFF)
464 attr[2] |= termios.CLOCAL
465 attr[2] &= ~termios.CRTSCTS
466 attr[4] = termios.B115200
467 attr[5] = termios.B115200
468 termios.tcsetattr(fd, termios.TCSANOW, attr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800469
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800470 nonlocals = {'control_state': None, 'control_str': ''}
471
472 def _ProcessBuffer(buf):
473 write_buffer = ''
474 while buf:
475 if nonlocals['control_state']:
476 if chr(_CONTROL_END) in buf:
477 index = buf.index(chr(_CONTROL_END))
478 nonlocals['control_str'] += buf[:index]
479 self.HandleTTYControl(fd, nonlocals['control_str'])
480 nonlocals['control_state'] = None
481 nonlocals['control_str'] = ''
482 buf = buf[index+1:]
483 else:
484 nonlocals['control_str'] += buf
485 buf = ''
486 else:
487 if chr(_CONTROL_START) in buf:
488 nonlocals['control_state'] = _CONTROL_START
489 index = buf.index(chr(_CONTROL_START))
490 write_buffer += buf[:index]
491 buf = buf[index+1:]
492 else:
493 write_buffer += buf
494 buf = ''
495
496 if write_buffer:
497 os.write(fd, write_buffer)
498
499 _ProcessBuffer(self._sock.RecvBuf())
500
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800501 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800502 rd, unused_wd, unused_xd = select.select([self._sock, fd], [], [])
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800503
504 if fd in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800505 self._sock.Send(os.read(fd, _BUFSIZE))
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800506
507 if self._sock in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800508 buf = self._sock.Recv(_BUFSIZE)
509 if len(buf) == 0:
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800510 raise RuntimeError('connection terminated')
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800511 _ProcessBuffer(buf)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800512 except Exception as e:
513 logging.error('SpawnTTYServer: %s', e)
514 finally:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800515 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800516
517 logging.info('SpawnTTYServer: terminated')
518 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800519
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800520 def SpawnShellServer(self, unused_var):
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800521 """Spawn a shell server and forward input/output from/to the TCP socket."""
522 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800523
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800524 # Add ghost executable to PATH
525 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
526 env = os.environ.copy()
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800527 env['PATH'] = '%s:%s' % (script_dir, os.getenv('PATH'))
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800528
529 # Execute shell command from HOME directory
530 os.chdir(os.getenv('HOME', '/tmp'))
531
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800532 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800533 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800534 shell=True, env=env)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800535
536 def make_non_block(fd):
537 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
538 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
539
540 make_non_block(p.stdout)
541 make_non_block(p.stderr)
542
543 try:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800544 p.stdin.write(self._sock.RecvBuf())
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800545
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800546 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800547 rd, unused_wd, unused_xd = select.select(
548 [p.stdout, p.stderr, self._sock], [], [self._sock])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800549 if p.stdout in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800550 self._sock.Send(p.stdout.read(_BUFSIZE))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800551
552 if p.stderr in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800553 self._sock.Send(p.stderr.read(_BUFSIZE))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800554
555 if self._sock in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800556 ret = self._sock.Recv(_BUFSIZE)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800557 if len(ret) == 0:
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800558 raise RuntimeError('connection terminated')
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800559
560 try:
561 idx = ret.index(_STDIN_CLOSED * 2)
562 p.stdin.write(ret[:idx])
563 p.stdin.close()
564 except ValueError:
565 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800566 p.poll()
567 if p.returncode != None:
568 break
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800569 except Exception as e:
570 logging.error('SpawnShellServer: %s', e)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800571 finally:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800572 # Check if the process is terminated. If not, Send SIGTERM to process,
573 # then wait for 1 second. Send another SIGKILL to make sure the process is
574 # terminated.
575 p.poll()
576 if p.returncode is None:
577 try:
578 p.terminate()
579 time.sleep(1)
580 p.kill()
581 except Exception:
582 pass
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800583
584 p.wait()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800585 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800586
587 logging.info('SpawnShellServer: terminated')
588 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800589
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800590 def InitiateFileOperation(self, unused_var):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800591 if self._file_op[0] == 'download':
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800592 try:
593 size = os.stat(self._file_op[1]).st_size
594 except OSError as e:
595 logging.error('InitiateFileOperation: download: %s', e)
596 sys.exit(1)
597
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800598 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800599 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800600 'filename': os.path.basename(self._file_op[1]),
601 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800602 elif self._file_op[0] == 'upload':
603 self.SendRequest('clear_to_upload', {}, timeout=-1)
604 self.StartUploadServer()
605 else:
606 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800607
608 def StartDownloadServer(self):
609 logging.info('StartDownloadServer: started')
610
611 try:
612 with open(self._file_op[1], 'rb') as f:
613 while True:
614 data = f.read(_BLOCK_SIZE)
615 if len(data) == 0:
616 break
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800617 self._sock.Send(data)
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800618 except Exception as e:
619 logging.error('StartDownloadServer: %s', e)
620 finally:
621 self._sock.close()
622
623 logging.info('StartDownloadServer: terminated')
624 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800625
Wei-Ning Huange2981862015-08-03 15:03:08 +0800626 def StartUploadServer(self):
627 logging.info('StartUploadServer: started')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800628 try:
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800629 filepath = self._file_op[1]
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800630 dirname = os.path.dirname(filepath)
631 if not os.path.exists(dirname):
632 try:
633 os.makedirs(dirname)
634 except Exception:
635 pass
Wei-Ning Huange2981862015-08-03 15:03:08 +0800636
637 self._sock.setblocking(False)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800638 with open(filepath, 'wb') as f:
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800639 if self._file_op[2]:
640 os.fchmod(f.fileno(), self._file_op[2])
641
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800642 f.write(self._sock.RecvBuf())
643
Wei-Ning Huange2981862015-08-03 15:03:08 +0800644 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800645 rd, unused_wd, unused_xd = select.select([self._sock], [], [])
Wei-Ning Huange2981862015-08-03 15:03:08 +0800646 if self._sock in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800647 buf = self._sock.Recv(_BLOCK_SIZE)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800648 if len(buf) == 0:
649 break
650 f.write(buf)
651 except socket.error as e:
652 logging.error('StartUploadServer: socket error: %s', e)
653 except Exception as e:
654 logging.error('StartUploadServer: %s', e)
655 finally:
656 self._sock.close()
657
658 logging.info('StartUploadServer: terminated')
659 sys.exit(0)
660
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800661 def SpawnPortForwardServer(self, unused_var):
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800662 """Spawn a port forwarding server and forward I/O to the TCP socket."""
663 logging.info('SpawnPortForwardServer: started')
664
665 src_sock = None
666 try:
667 src_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800668 src_sock.settimeout(_CONNECT_TIMEOUT)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800669 src_sock.connect(('localhost', self._port))
670 src_sock.setblocking(False)
671
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800672 src_sock.send(self._sock.RecvBuf())
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800673
674 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800675 rd, unused_wd, unused_xd = select.select([self._sock, src_sock], [], [])
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800676
677 if self._sock in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800678 data = self._sock.Recv(_BUFSIZE)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800679 if len(data) == 0:
680 raise RuntimeError('connection terminated')
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800681 src_sock.send(data)
682
683 if src_sock in rd:
684 data = src_sock.recv(_BUFSIZE)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800685 if len(data) == 0:
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800686 break
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800687 self._sock.Send(data)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800688 except Exception as e:
689 logging.error('SpawnPortForwardServer: %s', e)
690 finally:
691 if src_sock:
692 src_sock.close()
693 self._sock.close()
694
695 logging.info('SpawnPortForwardServer: terminated')
696 sys.exit(0)
697
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800698 def Ping(self):
699 def timeout_handler(x):
700 if x is None:
701 raise PingTimeoutError
702
703 self._last_ping = self.Timestamp()
704 self.SendRequest('ping', {}, timeout_handler, 5)
705
Wei-Ning Huangae923642015-09-24 14:08:09 +0800706 def HandleFileDownloadRequest(self, msg):
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800707 params = msg['params']
Wei-Ning Huangae923642015-09-24 14:08:09 +0800708 filepath = params['filename']
709 if not os.path.isabs(filepath):
710 filepath = os.path.join(os.getenv('HOME', '/tmp'), filepath)
711
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800712 try:
Wei-Ning Huang11c35022015-10-21 16:52:32 +0800713 with open(filepath, 'r') as _:
Wei-Ning Huang46a3fc92015-10-06 02:35:27 +0800714 pass
715 except Exception as e:
Wei-Ning Huangae923642015-09-24 14:08:09 +0800716 return self.SendResponse(msg, str(e))
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800717
718 self.SpawnGhost(self.FILE, params['sid'],
Wei-Ning Huangae923642015-09-24 14:08:09 +0800719 file_op=('download', filepath))
720 self.SendResponse(msg, SUCCESS)
721
722 def HandleFileUploadRequest(self, msg):
723 params = msg['params']
724
725 # Resolve upload filepath
726 filename = params['filename']
727 dest_path = filename
728
729 # If dest is specified, use it first
730 dest_path = params.get('dest', '')
731 if dest_path:
732 if not os.path.isabs(dest_path):
733 dest_path = os.path.join(os.getenv('HOME', '/tmp'), dest_path)
734
735 if os.path.isdir(dest_path):
736 dest_path = os.path.join(dest_path, filename)
737 else:
738 target_dir = os.getenv('HOME', '/tmp')
739
740 # Terminal session ID found, upload to it's current working directory
741 if params.has_key('terminal_sid'):
742 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
743 if pid:
744 target_dir = os.readlink('/proc/%d/cwd' % pid)
745
746 dest_path = os.path.join(target_dir, filename)
747
748 try:
749 os.makedirs(os.path.dirname(dest_path))
750 except Exception:
751 pass
752
753 try:
754 with open(dest_path, 'w') as _:
755 pass
756 except Exception as e:
757 return self.SendResponse(msg, str(e))
758
Wei-Ning Huangd6f69762015-10-01 21:02:07 +0800759 # If not check_only, spawn FILE mode ghost agent to handle upload
760 if not params.get('check_only', False):
761 self.SpawnGhost(self.FILE, params['sid'],
762 file_op=('upload', dest_path, params.get('perm', None)))
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800763 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800764
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800765 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800766 command = msg['name']
767 params = msg['params']
768
769 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800770 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800771 elif command == 'terminal':
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800772 self.SpawnGhost(self.TERMINAL, params['sid'],
773 tty_device=params['tty_device'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800774 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800775 elif command == 'shell':
776 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800777 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800778 elif command == 'file_download':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800779 self.HandleFileDownloadRequest(msg)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800780 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800781 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800782 elif command == 'file_upload':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800783 self.HandleFileUploadRequest(msg)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800784 elif command == 'forward':
785 self.SpawnGhost(self.FORWARD, params['sid'], port=params['port'])
786 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800787
788 def HandleResponse(self, response):
789 rid = str(response['rid'])
790 if rid in self._requests:
791 handler = self._requests[rid][2]
792 del self._requests[rid]
793 if callable(handler):
794 handler(response)
795 else:
Joel Kitching22b89042015-08-06 18:23:29 +0800796 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800797
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800798 def ParseMessage(self, buf, single=True):
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800799 if single:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800800 index = buf.index(_SEPARATOR)
801 msgs_json = [buf[:index]]
802 self._sock.UnRecv(buf[index + 2:])
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800803 else:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800804 msgs_json = buf.split(_SEPARATOR)
805 self._sock.UnRecv(msgs_json.pop())
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800806
807 for msg_json in msgs_json:
808 try:
809 msg = json.loads(msg_json)
810 except ValueError:
811 # Ignore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800812 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800813 continue
814
815 if 'name' in msg:
816 self.HandleRequest(msg)
817 elif 'response' in msg:
818 self.HandleResponse(msg)
819 else: # Ingnore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800820 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800821
822 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800823 """Scans for pending requests which have timed out.
824
825 If any timed-out requests are discovered, their handler is called with the
826 special response value of None.
827 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800828 for rid in self._requests.keys()[:]:
829 request_time, timeout, handler = self._requests[rid]
830 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800831 if callable(handler):
832 handler(None)
833 else:
834 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800835 del self._requests[rid]
836
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800837 def InitiateDownload(self):
838 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800839 sid = self._ttyname_to_sid[ttyname]
840 self.SpawnGhost(self.FILE, terminal_sid=sid,
Wei-Ning Huangae923642015-09-24 14:08:09 +0800841 file_op=('download', filename))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800842
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800843 def Listen(self):
844 try:
845 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800846 rds, unused_wd, unused_xd = select.select([self._sock], [], [],
847 _PING_INTERVAL / 2)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800848
849 if self._sock in rds:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800850 data = self._sock.Recv(_BUFSIZE)
Wei-Ning Huang09c19612015-11-24 16:29:09 +0800851
852 # Socket is closed
853 if len(data) == 0:
854 self.Reset()
855 break
856
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800857 self.ParseMessage(data, self._register_status != SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800858
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800859 if (self._mode == self.AGENT and
860 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800861 self.Ping()
862 self.ScanForTimeoutRequests()
863
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800864 if not self._download_queue.empty():
865 self.InitiateDownload()
866
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800867 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800868 self.Reset()
869 break
870 except socket.error:
871 raise RuntimeError('Connection dropped')
872 except PingTimeoutError:
873 raise RuntimeError('Connection timeout')
874 finally:
875 self._sock.close()
876
877 self._queue.put('resume')
878
879 if self._mode != Ghost.AGENT:
880 sys.exit(1)
881
882 def Register(self):
883 non_local = {}
884 for addr in self._overlord_addrs:
885 non_local['addr'] = addr
886 def registered(response):
887 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800888 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800889 raise RuntimeError('Register request timeout')
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800890
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800891 self._register_status = response['response']
892 if response['response'] != SUCCESS:
893 self._reset.set()
894 raise RuntimeError('Reigster: ' + response['response'])
895 else:
896 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
897 self._connected_addr = non_local['addr']
898 self.Upgrade() # Check for upgrade
899 self._queue.put('pause', True)
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800900
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800901 try:
902 logging.info('Trying %s:%d ...', *addr)
903 self.Reset()
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800904 self._sock = BufferedSocket(socket.AF_INET, socket.SOCK_STREAM)
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800905 self._sock.settimeout(_CONNECT_TIMEOUT)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800906 self._sock.connect(addr)
907
908 logging.info('Connection established, registering...')
909 handler = {
910 Ghost.AGENT: registered,
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800911 Ghost.TERMINAL: self.SpawnTTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800912 Ghost.SHELL: self.SpawnShellServer,
913 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800914 Ghost.FORWARD: self.SpawnPortForwardServer,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800915 }[self._mode]
916
917 # Machine ID may change if MAC address is used (USB-ethernet dongle
918 # plugged/unplugged)
919 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800920 self.SendRequest('register',
921 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800922 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800923 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800924 except socket.error:
925 pass
926 else:
927 self._sock.settimeout(None)
928 self.Listen()
929
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800930 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800931
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800932 def Reconnect(self):
933 logging.info('Received reconnect request from RPC server, reconnecting...')
934 self._reset.set()
935
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800936 def GetStatus(self):
937 return self._register_status
938
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800939 def AddToDownloadQueue(self, ttyname, filename):
940 self._download_queue.put((ttyname, filename))
941
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800942 def RegisterTTY(self, session_id, ttyname):
943 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +0800944
945 def RegisterSession(self, session_id, process_id):
946 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800947
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800948 def StartLanDiscovery(self):
949 """Start to listen to LAN discovery packet at
950 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800951
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800952 def thread_func():
953 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
954 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
955 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800956 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800957 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
958 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800959 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800960 return
961
962 logging.info('LAN Discovery: started')
963 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800964 rd, unused_wd, unused_xd = select.select([s], [], [], 1)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800965
966 if s in rd:
967 data, source_addr = s.recvfrom(_BUFSIZE)
968 parts = data.split()
969 if parts[0] == 'OVERLORD':
970 ip, port = parts[1].split(':')
971 if not ip:
972 ip = source_addr[0]
973 self._queue.put((ip, int(port)), True)
974
975 try:
976 obj = self._queue.get(False)
977 except Queue.Empty:
978 pass
979 else:
980 if type(obj) is not str:
981 self._queue.put(obj)
982 elif obj == 'pause':
983 logging.info('LAN Discovery: paused')
984 while obj != 'resume':
985 obj = self._queue.get(True)
986 logging.info('LAN Discovery: resumed')
987
988 t = threading.Thread(target=thread_func)
989 t.daemon = True
990 t.start()
991
992 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800993 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800994 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
995 logRequests=False)
996 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800997 rpc_server.register_function(self.GetStatus, 'GetStatus')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800998 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800999 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001000 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001001 t = threading.Thread(target=rpc_server.serve_forever)
1002 t.daemon = True
1003 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001004
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001005 def ScanServer(self):
1006 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
1007 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
1008 if addr not in self._overlord_addrs:
1009 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001010
Wei-Ning Huang11c35022015-10-21 16:52:32 +08001011 def Start(self, lan_disc=False, rpc_server=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001012 logging.info('%s started', self.MODE_NAME[self._mode])
1013 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001014 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001015
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08001016 # We don't care about child process's return code, not wait is needed. This
1017 # is used to prevent zombie process from lingering in the system.
1018 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001019
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001020 if lan_disc:
1021 self.StartLanDiscovery()
1022
1023 if rpc_server:
1024 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001025
1026 try:
1027 while True:
1028 try:
1029 addr = self._queue.get(False)
1030 except Queue.Empty:
1031 pass
1032 else:
1033 if type(addr) == tuple and addr not in self._overlord_addrs:
1034 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
1035 self._overlord_addrs.append(addr)
1036
1037 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001038 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001039 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +08001040 # Don't show stack trace for RuntimeError, which we use in this file for
1041 # plausible and expected errors (such as can't connect to server).
1042 except RuntimeError as e:
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001043 logging.info('%s, retrying in %ds', e.message, _RETRY_INTERVAL)
Joel Kitching22b89042015-08-06 18:23:29 +08001044 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001045 except Exception as e:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001046 unused_x, unused_y, exc_traceback = sys.exc_info()
Joel Kitching22b89042015-08-06 18:23:29 +08001047 traceback.print_tb(exc_traceback)
1048 logging.info('%s: %s, retrying in %ds',
1049 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001050 time.sleep(_RETRY_INTERVAL)
1051
1052 self.Reset()
1053 except KeyboardInterrupt:
1054 logging.error('Received keyboard interrupt, quit')
1055 sys.exit(0)
1056
1057
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001058def GhostRPCServer():
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001059 """Returns handler to Ghost's JSON RPC server."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001060 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1061
1062
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001063def ForkToBackground():
1064 """Fork process to run in background."""
1065 pid = os.fork()
1066 if pid != 0:
1067 logging.info('Ghost(%d) running in background.', pid)
1068 sys.exit(0)
1069
1070
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001071def DownloadFile(filename):
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001072 """Initiate a client-initiated file download."""
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001073 filepath = os.path.abspath(filename)
1074 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001075 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001076 sys.exit(1)
1077
1078 # Check if we actually have permission to read the file
1079 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001080 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001081 sys.exit(1)
1082
1083 server = GhostRPCServer()
1084 server.AddToDownloadQueue(os.ttyname(0), filepath)
1085 sys.exit(0)
1086
1087
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001088def main():
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +08001089 # Setup logging format
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001090 logger = logging.getLogger()
1091 logger.setLevel(logging.INFO)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +08001092 handler = logging.StreamHandler()
1093 formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S')
1094 handler.setFormatter(formatter)
1095 logger.addHandler(handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001096
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001097 parser = argparse.ArgumentParser()
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001098 parser.add_argument('--fork', dest='fork', action='store_true', default=False,
1099 help='fork procecess to run in background')
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001100 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1101 default=None, help='use MID as machine ID')
1102 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1103 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001104 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1105 default=True, help='disable LAN discovery')
1106 parser.add_argument('--no-rpc-server', dest='rpc_server',
1107 action='store_false', default=True,
1108 help='disable RPC server')
Joel Kitching22b89042015-08-06 18:23:29 +08001109 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001110 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001111 help='file containing the JSON representation of client '
1112 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001113 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1114 default=None, help='file to download')
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001115 parser.add_argument('--reset', dest='reset', default=False,
1116 action='store_true',
1117 help='reset ghost and reload all configs')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001118 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1119 nargs='*', help='overlord server address')
1120 args = parser.parse_args()
1121
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001122 if args.fork:
1123 ForkToBackground()
1124
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001125 if args.reset:
1126 GhostRPCServer().Reconnect()
1127 sys.exit()
1128
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001129 if args.download:
1130 DownloadFile(args.download)
1131
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001132 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001133 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001134
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001135 g = Ghost(addrs, Ghost.AGENT, args.mid, prop_file=args.prop_file)
Wei-Ning Huang11c35022015-10-21 16:52:32 +08001136 g.Start(args.lan_disc, args.rpc_server)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001137
1138
1139if __name__ == '__main__':
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001140 main()