PageRenderTime 55ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/support/android/fastdev.py

https://github.com/bataboske/titanium_mobile
Python | 468 lines | 437 code | 15 blank | 16 comment | 15 complexity | fb52be36fdbd94a64ffb01ce3710d369 MD5 | raw file
  1. #
  2. # Appcelerator Titanium Mobile
  3. # Copyright (c) 2011 by Appcelerator, Inc. All Rights Reserved.
  4. # Licensed under the terms of the Apache Public License
  5. # Please see the LICENSE included with this distribution for details.
  6. #
  7. # A custom server that speeds up development time in Android significantly
  8. import os, sys, time, optparse, logging
  9. import urllib, simplejson, threading
  10. import SocketServer, socket, struct, codecs
  11. # we use our compatibility code for python 2.5
  12. if sys.version_info < (2, 6):
  13. from tcpserver import TCPServer
  14. else:
  15. from SocketServer import TCPServer
  16. logging.basicConfig(format='[%(levelname)s] [%(asctime)s] %(message)s', level=logging.INFO)
  17. support_android_dir = os.path.dirname(os.path.abspath(__file__))
  18. support_dir = os.path.dirname(support_android_dir)
  19. sys.path.append(support_dir)
  20. import tiapp
  21. server = None
  22. request_count = 0
  23. start_time = time.time()
  24. idle_thread = None
  25. def pack_int(i):
  26. return struct.pack("!i", i)
  27. def send_tokens(socket, *tokens):
  28. buffer = pack_int(len(tokens))
  29. for token in tokens:
  30. buffer += pack_int(len(token))
  31. buffer += token
  32. socket.sendall(buffer)
  33. def read_int(socket):
  34. data = socket.recv(4)
  35. if not data: return None
  36. return struct.unpack("!i", data)[0]
  37. def read_tokens(socket):
  38. token_count = read_int(socket)
  39. if token_count == None: return None
  40. tokens = []
  41. for i in range(0, token_count):
  42. length = read_int(socket)
  43. data = socket.recv(length)
  44. tokens.append(data)
  45. return tokens
  46. utf8_codec = codecs.lookup("utf-8")
  47. """ A simple idle checker thread """
  48. class IdleThread(threading.Thread):
  49. def __init__(self, max_idle_time):
  50. super(IdleThread, self).__init__()
  51. self.idle_time = 0
  52. self.max_idle_time = max_idle_time
  53. self.running = True
  54. def clear_idle_time(self):
  55. self.idle_lock.acquire()
  56. self.idle_time = 0
  57. self.idle_lock.release()
  58. def run(self):
  59. self.idle_lock = threading.Lock()
  60. while self.running:
  61. if self.idle_time < self.max_idle_time:
  62. time.sleep(1)
  63. self.idle_lock.acquire()
  64. self.idle_time += 1
  65. self.idle_lock.release()
  66. else:
  67. logging.info("Shutting down Fastdev server due to idle timeout: %s" % self.idle_time)
  68. server.shutdown()
  69. self.running = False
  70. """
  71. A handler for fastdev requests.
  72. The fastdev server uses a simple binary protocol comprised of messages and tokens.
  73. Without a valid handshake, no requests will be processed.
  74. Currently supported commands are:
  75. - "handshake" <guid> : Application handshake
  76. - "script-handshake" <guid> : Script control handshake
  77. - "get" <Resources relative path> : Get the contents of a file from the Resources folder
  78. - "kill-app" : Kills the connected app's process
  79. - "restart-app" : Restarts the connected app's process
  80. -"shutdown" : Shuts down the server
  81. Right now the VFS rules for "get" are:
  82. - Anything under "Resources" is served as is
  83. - Anything under "Resources/android" overwrites anything under "Resources" (and is mapped to the root)
  84. """
  85. class FastDevHandler(SocketServer.BaseRequestHandler):
  86. resources_dir = None
  87. handshake = None
  88. app_handler = None
  89. def handle(self):
  90. logging.info("connected: %s:%d" % self.client_address)
  91. global request_count
  92. self.valid_handshake = False
  93. self.request.settimeout(1.0)
  94. while True:
  95. try:
  96. tokens = read_tokens(self.request)
  97. if tokens == None:
  98. break
  99. except socket.timeout, e:
  100. # only break the loop when not serving, otherwise timeouts are normal
  101. serving = False
  102. if sys.version_info < (2, 6):
  103. serving = server.is_serving()
  104. elif sys.version_info < (2, 7):
  105. serving = server._BaseServer__serving
  106. else:
  107. serving = not server._BaseServer__is_shut_down.isSet()
  108. if not serving:
  109. break
  110. else: continue
  111. idle_thread.clear_idle_time()
  112. command = tokens[0]
  113. if command == "handshake":
  114. FastDevHandler.app_handler = self
  115. self.handle_handshake(tokens[1])
  116. elif command == "script-handshake":
  117. self.handle_handshake(tokens[1])
  118. else:
  119. if not self.valid_handshake:
  120. self.send_tokens("Invalid Handshake")
  121. break
  122. if command == "length":
  123. request_count += 1
  124. self.handle_length(tokens[1])
  125. elif command == "get":
  126. request_count += 1
  127. self.handle_get(tokens[1])
  128. elif command == "kill-app":
  129. self.handle_kill_app()
  130. break
  131. elif command == "restart-app":
  132. self.handle_restart_app()
  133. break
  134. elif command == "status":
  135. self.handle_status()
  136. elif command == "shutdown":
  137. self.handle_shutdown()
  138. break
  139. logging.info("disconnected: %s:%d" % self.client_address)
  140. def handle_handshake(self, handshake):
  141. logging.info("handshake: %s" % handshake)
  142. if handshake == self.handshake:
  143. self.send_tokens("OK")
  144. self.valid_handshake = True
  145. else:
  146. logging.warn("handshake: invalid handshake sent, rejecting")
  147. self.send_tokens("Invalid Handshake")
  148. def get_resource_path(self, relative_path):
  149. android_path = os.path.join(self.resources_dir, 'android', relative_path)
  150. path = os.path.join(self.resources_dir, relative_path)
  151. if os.path.exists(android_path):
  152. return android_path
  153. elif os.path.exists(path):
  154. return path
  155. else:
  156. return None
  157. def handle_length(self, relative_path):
  158. path = self.get_resource_path(relative_path)
  159. if path != None:
  160. length = os.path.getsize(path)
  161. logging.info("length %s: %d" % (relative_path, length))
  162. self.send_tokens(pack_int(length))
  163. else:
  164. logging.info("length %s: path not found" % relative_path)
  165. self.send_tokens(pack_int(-1))
  166. def handle_get(self, relative_path):
  167. path = self.get_resource_path(relative_path)
  168. if path != None:
  169. logging.info("get %s: %s" % (relative_path, path))
  170. self.send_file(path)
  171. else:
  172. logging.warn("get %s: path not found" % relative_path)
  173. self.send_tokens("NOT_FOUND")
  174. def send_tokens(self, *tokens):
  175. send_tokens(self.request, *tokens)
  176. def send_file(self, path):
  177. buffer = open(path, 'r').read()
  178. self.send_tokens(buffer)
  179. def handle_kill_app(self):
  180. logging.info("request: kill-app")
  181. if FastDevHandler.app_handler != None:
  182. try:
  183. FastDevHandler.app_handler.send_tokens("kill")
  184. self.send_tokens("OK")
  185. except Exception, e:
  186. logging.error("kill: error: %s" % e)
  187. self.send_tokens(str(e))
  188. else:
  189. self.send_tokens("App not connected")
  190. logging.warn("kill: no app is connected")
  191. def handle_restart_app(self):
  192. logging.info("request: restart-app")
  193. if FastDevHandler.app_handler != None:
  194. try:
  195. FastDevHandler.app_handler.send_tokens("restart")
  196. self.send_tokens("OK")
  197. except Exception, e:
  198. logging.error("restart: error: %s" % e)
  199. self.send_tokens(str(e))
  200. else:
  201. self.send_tokens("App not connected")
  202. logging.warn("restart: no app is connected")
  203. def handle_status(self):
  204. logging.info("request: status")
  205. global server
  206. global request_count
  207. global start_time
  208. app_connected = FastDevHandler.app_handler != None
  209. status = {
  210. "uptime": int(time.time() - start_time),
  211. "pid": os.getpid(),
  212. "app_connected": app_connected,
  213. "request_count": request_count,
  214. "port": server.server_address[1]
  215. }
  216. self.send_tokens(simplejson.dumps(status))
  217. def handle_shutdown(self):
  218. self.send_tokens("OK")
  219. server.shutdown()
  220. idle_thread.running = False
  221. class ThreadingTCPServer(SocketServer.ThreadingMixIn, TCPServer):
  222. def shutdown_noblock(self):
  223. if sys.version_info < (2, 6):
  224. self.__serving = False
  225. elif sys.version_info < (2, 7):
  226. self._BaseServer__serving = False
  227. else:
  228. self._BaseServer__shutdown_request = True
  229. class FastDevRequest(object):
  230. def __init__(self, dir, options):
  231. self.lock_file = get_lock_file(dir, options)
  232. if not os.path.exists(self.lock_file):
  233. print >>sys.stderr, "Error: No Fastdev Servers found. " \
  234. "The lock file at %s does not exist, you either need to run \"stop\" " \
  235. "within your Titanium project or specify the lock file with -l <lock file>" \
  236. % self.lock_file
  237. sys.exit(1)
  238. f = open(self.lock_file, 'r')
  239. self.data = simplejson.loads(f.read())
  240. f.close()
  241. self.port = self.data["port"]
  242. self.app_guid = self.data["app_guid"]
  243. self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  244. self.socket.connect((socket.gethostname(), self.port))
  245. send_tokens(self.socket, "script-handshake", self.app_guid)
  246. response = read_tokens(self.socket)[0]
  247. if response != "OK":
  248. print >>sys.stderr, "Error: Handshake was not accepted by the Fastdev server"
  249. sys.exit(1)
  250. def send(self, *tokens):
  251. send_tokens(self.socket, *tokens)
  252. return read_tokens(self.socket)
  253. def close(self):
  254. self.socket.close()
  255. def get_lock_file(dir, options):
  256. lock_file = options.lock_file
  257. if lock_file == None:
  258. lock_file = os.path.join(dir, ".fastdev.lock")
  259. return lock_file
  260. def start_server(dir, options):
  261. xml = tiapp.TiAppXML(os.path.join(dir, "tiapp.xml"))
  262. app_id = xml.properties["id"]
  263. app_guid = xml.properties["guid"]
  264. lock_file = get_lock_file(dir, options)
  265. if os.path.exists(lock_file):
  266. print "Fastdev server already running for %s" % app_id
  267. sys.exit(0)
  268. resources_dir = os.path.join(dir, 'Resources')
  269. FastDevHandler.resources_dir = resources_dir
  270. FastDevHandler.handshake = app_guid
  271. global server
  272. global idle_thread
  273. server = ThreadingTCPServer(("", int(options.port)), FastDevHandler)
  274. port = server.server_address[1]
  275. logging.info("Serving up files for %s at 0.0.0.0:%d from %s" % (app_id, port, dir))
  276. f = open(lock_file, 'w+')
  277. f.write(simplejson.dumps({
  278. "ip": "0.0.0.0",
  279. "port": port,
  280. "dir": dir,
  281. "app_id": app_id,
  282. "app_guid": app_guid
  283. }))
  284. f.close()
  285. try:
  286. idle_thread = IdleThread(int(options.timeout))
  287. idle_thread.start()
  288. server.serve_forever()
  289. except KeyboardInterrupt, e:
  290. idle_thread.running = False
  291. server.shutdown_noblock()
  292. print "Terminated"
  293. logging.info("Fastdev server stopped.")
  294. os.unlink(lock_file)
  295. def stop_server(dir, options):
  296. request = FastDevRequest(dir, options)
  297. print request.send("shutdown")[0]
  298. request.close()
  299. print "Fastdev server for %s stopped." % request.data["app_id"]
  300. def kill_app(dir, options):
  301. request = FastDevRequest(dir, options)
  302. result = request.send("kill-app")
  303. request.close()
  304. if result and result[0] == "OK":
  305. print "Killed app %s." % request.data["app_id"]
  306. return True
  307. else:
  308. print "Error killing app, result: %s" % result
  309. return False
  310. def restart_app(dir, options):
  311. request = FastDevRequest(dir, options)
  312. result = request.send("restart-app")
  313. request.close()
  314. if result and result[0] == "OK":
  315. print "Restarted app %s." % request.data["app_id"]
  316. return True
  317. else:
  318. print "Error restarting app, result: %s" % result
  319. return False
  320. def is_running(dir):
  321. class Options(object): pass
  322. options = Options()
  323. options.lock_file = os.path.join(dir, '.fastdev.lock')
  324. if not os.path.exists(options.lock_file):
  325. return False
  326. try:
  327. request = FastDevRequest(dir, options)
  328. result = request.send("status")[0]
  329. request.close()
  330. status = simplejson.loads(result)
  331. return type(status) == dict
  332. except Exception, e:
  333. return False
  334. def status(dir, options):
  335. lock_file = get_lock_file(dir, options)
  336. if lock_file == None or not os.path.exists(lock_file):
  337. print "No Fastdev servers running in %s" % dir
  338. else:
  339. data = simplejson.loads(open(lock_file, 'r').read())
  340. port = data["port"]
  341. try:
  342. request = FastDevRequest(dir, options)
  343. result = request.send("status")[0]
  344. request.close()
  345. status = simplejson.loads(result)
  346. print "Fastdev server running for app %s:" % data["app_id"]
  347. print "Port: %d" % port
  348. print "Uptime: %d sec" % status["uptime"]
  349. print "PID: %d" % status["pid"]
  350. print "Requests: %d" % status["request_count"]
  351. except Exception, e:
  352. print >>sys.stderr, "Error: .fastdev.lock found in %s, but couldn't connect to the server on port %d: %s. Try manually deleting .fastdev.lock." % (dir, port, e)
  353. def get_optparser():
  354. usage = """Usage: %prog [command] [options] [app-dir]
  355. Supported Commands:
  356. start start the fastdev server
  357. status get the status of the fastdev server
  358. stop stop the fastdev server
  359. restart-app restart the app connected to this fastdev server
  360. kill-app kill the app connected to this fastdev server
  361. """
  362. parser = optparse.OptionParser(usage)
  363. parser.add_option('-p', '--port', dest='port',
  364. help='port to bind the server to [default: first available port]', default=0)
  365. parser.add_option('-t', '--timeout', dest='timeout',
  366. help='Timeout in seconds before the Fastdev server shuts itself down when it hasn\'t received a request [default: %default]',
  367. default=30 * 60)
  368. parser.add_option('-l', '--lock-file', dest='lock_file',
  369. help='Path to the server lock file [default: app-dir/.fastdev.lock]',
  370. default=None)
  371. return parser
  372. def main():
  373. parser = get_optparser()
  374. (options, args) = parser.parse_args()
  375. if len(args) == 0 or args[0] not in ['start', 'stop', 'kill-app', 'restart-app', 'status']:
  376. parser.error("Missing required command")
  377. sys.exit(1)
  378. command = args[0]
  379. dir = os.getcwd()
  380. if len(args) > 1:
  381. dir = os.path.expanduser(args[1])
  382. dir = os.path.abspath(dir)
  383. if command == "start":
  384. if not os.path.exists(os.path.join(dir, "tiapp.xml")):
  385. parser.error("Directory is not a Titanium Project: %s" % dir)
  386. sys.exit(1)
  387. try:
  388. start_server(dir, options)
  389. except Exception, e:
  390. print >>sys.stderr, "Error starting Fastdev server: %s" % e
  391. elif command == "stop":
  392. stop_server(dir, options)
  393. elif command == "kill-app":
  394. kill_app(dir, options)
  395. elif command == 'restart-app':
  396. restart_app(dir, options)
  397. elif command == "status":
  398. status(dir, options)
  399. if __name__ == "__main__":
  400. main()