blob: 228111da03e7b1d19d7840800da54f849f3ba785 [file] [log] [blame]
Charlie Mooneybbc05f52015-03-24 13:36:22 -07001# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""This module launches the cherrypy server for webplot and sets up
6web sockets to handle the messages between the clients and the server.
7"""
8
9import argparse
10import base64
11import json
12import logging
13import os
14import re
15import subprocess
16import threading
17
18import cherrypy
19
Charlie Mooneybbc05f52015-03-24 13:36:22 -070020from ws4py import configure_logger
21from ws4py.messaging import TextMessage
22from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
23from ws4py.websocket import WebSocket
24
Charlie Mooney04b41532015-04-02 12:41:37 -070025from remote import ChromeOSTouchDevice, AndroidTouchDevice
Charlie Mooneybbc05f52015-03-24 13:36:22 -070026
27
28# The WebSocket connection state object.
29state = None
30
31# The touch events are saved in this file as default.
32SAVED_FILE = '/tmp/webplot.dat'
33SAVED_IMAGE = '/tmp/webplot.png'
34
35
Joseph Hwang4782a042015-04-08 17:15:50 +080036def SimpleSystem(cmd):
37 """Execute a system command."""
38 ret = subprocess.call(cmd, shell=True)
39 if ret:
40 logging.warning('Command (%s) failed (ret=%s).', cmd, ret)
41 return ret
42
43
44def SimpleSystemOutput(cmd):
45 """Execute a system command and get its output."""
46 try:
47 proc = subprocess.Popen(
48 cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
49 stdout, _ = proc.communicate()
50 except Exception, e:
51 logging.warning('Command (%s) failed (%s).', cmd, e)
52 else:
53 return None if proc.returncode else stdout.strip()
54
55
56def IsDestinationPortEnabled(port):
57 """Check if the destination port is enabled in iptables.
58
59 If port 8000 is enabled, it looks like
60 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 ctstate NEW tcp dpt:8000
61 """
62 pattern = re.compile('ACCEPT\s+tcp.+\s+ctstate\s+NEW\s+tcp\s+dpt:%d' % port)
63 rules = SimpleSystemOutput('sudo iptables -L INPUT -n --line-number')
64 for rule in rules.splitlines():
65 if pattern.search(rule):
66 return True
67 return False
68
69
70def EnableDestinationPort(port):
71 """Enable the destination port for input traffic in iptables."""
72 if IsDestinationPortEnabled(port):
73 cherrypy.log('Port %d has been already enabled in iptables.' % port)
74 else:
75 cherrypy.log('To enable port %d in iptables.' % port)
76 cmd = ('sudo iptables -A INPUT -p tcp -m conntrack --ctstate NEW '
77 '--dport %d -j ACCEPT' % port)
78 if SimpleSystem(cmd) != 0:
79 raise Error('Failed to enable port in iptables: %d.' % port)
80
81
Charlie Mooneybbc05f52015-03-24 13:36:22 -070082def InterruptHandler():
83 """An interrupt handler for both SIGINT and SIGTERM
84
85 The stop procedure triggered is as follows:
86 1. This handler sends a 'quit' message to the listening client.
87 2. The client sends the canvas image back to the server in its quit message.
88 3. WebplotWSHandler.received_message() saves the image.
89 4. WebplotWSHandler.received_message() handles the 'quit' message.
90 The cherrypy engine exits if this is the last client.
91 """
92 cherrypy.log('Cherrypy engine is sending quit message to clients.')
Joseph Hwang95bf52a2015-04-14 13:09:39 +080093 state.QuitAndShutdown()
94
95
96def _IOError(e, filename):
97 err_msg = ['\n', '!' * 60, str(e),
98 'It is likely that %s is owned by root.' % filename,
99 'Please remove the file and then run webplot again.',
100 '!' * 60, '\n']
101 cherrypy.log('\n'.join(err_msg))
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700102
103
104class WebplotWSHandler(WebSocket):
105 """The web socket handler for webplot."""
106
107 def opened(self):
108 """This method is called when the handler is opened."""
109 cherrypy.log('WS handler is opened!')
110
111 def received_message(self, msg):
112 """A callback for received message."""
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700113 data = msg.data.split(':', 1)
114 mtype = data[0].lower()
115 content = data[1] if len(data) == 2 else None
Joseph Hwang0c1fa7d2015-04-09 17:01:45 +0800116
117 # Do not print the image data since it is too large.
118 if mtype != 'save':
119 cherrypy.log('Received message: %s' % str(msg.data))
120
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700121 if mtype == 'quit':
122 # A shutdown message requested by the user.
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700123 cherrypy.log('The user requests to shutdown the cherrypy server....')
124 state.DecCount()
125 elif mtype == 'save':
Charlie Mooneyb476e892015-04-02 13:25:49 -0700126 cherrypy.log('All data saved to "%s"' % SAVED_FILE)
127 self.SaveImage(content, SAVED_IMAGE)
128 cherrypy.log('Plot image saved to "%s"' % SAVED_IMAGE)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700129 else:
130 cherrypy.log('Unknown message type: %s' % mtype)
131
132 def closed(self, code, reason="A client left the room."):
133 """This method is called when the handler is closed."""
134 cherrypy.log('A client requests to close WS.')
135 cherrypy.engine.publish('websocket-broadcast', TextMessage(reason))
136
137 @staticmethod
138 def SaveImage(image_data, image_file):
139 """Decoded the base64 image data and save it in the file."""
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800140 try:
141 with open(image_file, 'w') as f:
142 f.write(base64.b64decode(image_data))
143 except IOError as e:
144 _IOError(e, image_file)
145 state.QuitAndShutdown()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700146
147
148class TouchDeviceWrapper(object):
149 """This is a wrapper of remote.RemoteTouchDevice.
150
151 It handles the instantiation of different device types, and the beginning
152 and ending of the event stream.
153 """
154
Joseph Hwang59db4412015-04-09 12:43:16 +0800155 def __init__(self, dut_type, addr, is_touchscreen, grab):
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700156 if dut_type == 'chromeos':
Charlie Mooney8026f2a2015-04-02 13:01:28 -0700157 if addr is None:
158 addr = '127.0.0.1'
Joseph Hwang59db4412015-04-09 12:43:16 +0800159 self.device = ChromeOSTouchDevice(addr, is_touchscreen, grab=grab)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700160 else:
161 self.device = AndroidTouchDevice(addr, True)
162
163 def close(self):
164 """ Close the device gracefully. """
165 if self.device.event_stream_process:
166 self.device.__del__()
167
168 def __str__(self):
169 return '\n '.join(sorted([str(slot) for slot in self.slots.values()]))
170
171
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700172class ConnectionState(object):
173 """A ws connection state object for shutting down the cherrypy server.
174
175 It shuts down the cherrypy server when the count is down to 0 and is not
176 increased before the shutdown_timer expires.
177
178 Note that when a page refreshes, it closes the WS connection first and
179 then re-connects immediately. This is why we would like to wait a while
180 before actually shutting down the server.
181 """
182 TIMEOUT = 1.0
183
184 def __init__(self):
185 self.count = 0;
186 self.lock = threading.Lock()
187 self.shutdown_timer = None
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800188 self.quit_flag = False
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700189
190 def IncCount(self):
191 """Increase the connection count, and cancel the shutdown timer if exists.
192 """
193 self.lock.acquire()
194 self.count += 1;
195 cherrypy.log(' WS connection count: %d' % self.count)
196 if self.shutdown_timer:
197 self.shutdown_timer.cancel()
198 self.shutdown_timer = None
199 self.lock.release()
200
201 def DecCount(self):
202 """Decrease the connection count, and start a shutdown timer if no other
203 clients are connecting to the server.
204 """
205 self.lock.acquire()
206 self.count -= 1;
207 cherrypy.log(' WS connection count: %d' % self.count)
208 if self.count == 0:
209 self.shutdown_timer = threading.Timer(self.TIMEOUT, self.Shutdown)
210 self.shutdown_timer.start()
211 self.lock.release()
212
Joseph Hwang3f561d32015-04-09 16:33:56 +0800213 def ShutdownWhenNoConnections(self):
214 """Shutdown cherrypy server when there is no client connection."""
215 self.lock.acquire()
216 if self.count == 0 and self.shutdown_timer is None:
217 self.Shutdown()
218 self.lock.release()
219
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700220 def Shutdown(self):
221 """Shutdown the cherrypy server."""
222 cherrypy.log('Shutdown timer expires. Cherrypy server for Webplot exits.')
223 cherrypy.engine.exit()
224
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800225 def QuitAndShutdown(self):
226 """The server notifies clients to quit and then shuts down."""
227 if not self.quit_flag:
228 self.quit_flag = True
229 cherrypy.engine.publish('websocket-broadcast', TextMessage('quit'))
230 self.ShutdownWhenNoConnections()
231
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700232
233class Root(object):
234 """A class to handle requests about docroot."""
235
236 def __init__(self, ip, port, touch_min_x, touch_max_x, touch_min_y,
237 touch_max_y, touch_min_pressure, touch_max_pressure):
238 self.ip = ip
239 self.port = port
240 self.touch_min_x = touch_min_x
241 self.touch_max_x = touch_max_x
242 self.touch_min_y = touch_min_y
243 self.touch_max_y = touch_max_y
244 self.touch_min_pressure = touch_min_pressure
245 self.touch_max_pressure = touch_max_pressure
246 self.scheme = 'ws'
247 cherrypy.log('Root address: (%s, %s)' % (ip, str(port)))
248 cherrypy.log('scheme: %s' % self.scheme)
249
250 @cherrypy.expose
251 def index(self):
252 """This is the default index.html page."""
253 websocket_dict = {
254 'websocketUrl': '%s://%s:%s/ws' % (self.scheme, self.ip, self.port),
255 'touchMinX': str(self.touch_min_x),
256 'touchMaxX': str(self.touch_max_x),
257 'touchMinY': str(self.touch_min_y),
258 'touchMaxY': str(self.touch_max_y),
259 'touchMinPressure': str(self.touch_min_pressure),
260 'touchMaxPressure': str(self.touch_max_pressure),
261 }
262 root_page = os.path.join(os.path.abspath(os.path.dirname(__file__)),
263 'webplot.html')
264 with open(root_page) as f:
265 return f.read() % websocket_dict
266
267 @cherrypy.expose
268 def ws(self):
269 """This handles the request to create a new web socket per client."""
270 cherrypy.log('A new client requesting for WS')
271 cherrypy.log('WS handler created: %s' % repr(cherrypy.request.ws_handler))
272 state.IncCount()
273
274
Joseph Hwang4782a042015-04-08 17:15:50 +0800275class Webplot(threading.Thread):
276 """The server handling the Plotting of finger traces.
277
278 Use case 1: embedding Webplot as a plotter in an application
279
280 # Instantiate a webplot server and starts the daemon.
281 plot = Webplot(server_addr, server_port, device)
282 plot.start()
283
284 # Repeatedly get a snapshot and add it for plotting.
285 while True:
286 # GetSnapshot() is essentially device.NextSnapshot()
287 snapshot = plot.GetSnapshot()
288 if not snapshot:
289 break
290 # Add the snapshot to the plotter for plotting.
291 plot.AddSnapshot(snapshot)
292
293 # Save a screen dump
294 plot.Save()
295
296 # Notify the browser to clear the screen.
297 plot.Clear()
298
299 # Notify both the browser and the cherrypy engine to quit.
300 plot.Quit()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700301
302
Joseph Hwang4782a042015-04-08 17:15:50 +0800303 Use case 2: using webplot standalone
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700304
Joseph Hwang4782a042015-04-08 17:15:50 +0800305 # Instantiate a webplot server and starts the daemon.
306 plot = Webplot(server_addr, server_port, device)
307 plot.start()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700308
Joseph Hwang4782a042015-04-08 17:15:50 +0800309 # Get touch snapshots from the touch device and have clients plot them.
310 webplot.GetAndPlotSnapshots()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700311 """
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700312
Joseph Hwang4782a042015-04-08 17:15:50 +0800313 def __init__(self, server_addr, server_port, device, saved_file=SAVED_FILE):
314 self._server_addr = server_addr
315 self._server_port = server_port
316 self._device = device
317 self._saved_file = saved_file
318 super(Webplot, self).__init__(name='webplot thread')
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700319
Joseph Hwang4782a042015-04-08 17:15:50 +0800320 self.daemon = True
321 self._prev_tids = []
322
323 # Allow input traffic in iptables.
324 EnableDestinationPort(self._server_port)
325
326 # Create a ws connection state object to wait for the condition to
327 # shutdown the whole process.
328 global state
329 state = ConnectionState()
330
331 cherrypy.config.update({
332 'server.socket_host': self._server_addr,
333 'server.socket_port': self._server_port,
334 })
335
336 WebSocketPlugin(cherrypy.engine).subscribe()
337 cherrypy.tools.websocket = WebSocketTool()
338
339 # If the cherrypy server exits for whatever reason, close the device
340 # for required cleanup. Otherwise, there might exist local/remote
341 # zombie processes.
342 cherrypy.engine.subscribe('exit', self._device.close)
343
344 cherrypy.engine.signal_handler.handlers['SIGINT'] = InterruptHandler
345 cherrypy.engine.signal_handler.handlers['SIGTERM'] = InterruptHandler
346
347 def run(self):
348 """Start the cherrypy engine."""
349 x_min, x_max = self._device.device.RangeX()
350 y_min, y_max = self._device.device.RangeY()
351 p_min, p_max = self._device.device.RangeP()
352
353 cherrypy.quickstart(
354 Root(self._server_addr, self._server_port,
355 x_min, x_max, y_min, y_max, p_min, p_max),
356 '',
357 config={
358 '/': {
359 'tools.staticdir.root':
360 os.path.abspath(os.path.dirname(__file__)),
361 'tools.staticdir.on': True,
362 'tools.staticdir.dir': '',
363 },
364 '/ws': {
365 'tools.websocket.on': True,
366 'tools.websocket.handler_cls': WebplotWSHandler,
367 },
368 }
369 )
370
371 def _ConvertNamedtupleToDict(self, snapshot):
372 """Convert namedtuples to ordinary dictionaries and add leaving slots.
373
374 This is to make a snapshot json serializable. Otherwise, the namedtuples
375 would be transmitted as arrays which is less readable.
376
377 A snapshot looks like
378 MtSnapshot(
379 syn_time=1420524008.368854,
380 button_pressed=False,
381 fingers=[
382 MtFinger(tid=162, slot=0, syn_time=1420524008.368854, x=524,
383 y=231, pressure=45),
384 MtFinger(tid=163, slot=1, syn_time=1420524008.368854, x=677,
385 y=135, pressure=57)
386 ]
387 )
388
389 Note:
390 1. that there are two levels of namedtuples to convert.
391 2. The leaving slots are used to notify javascript that a finger is leaving
392 so that the corresponding finger color could be released for reuse.
393 """
394 # Convert MtSnapshot.
395 converted = dict(snapshot.__dict__.items())
396
397 # Convert MtFinger.
398 converted['fingers'] = [dict(finger.__dict__.items())
399 for finger in converted['fingers']]
400 converted['raw_events'] = [str(event) for event in converted['raw_events']]
401
402 # Add leaving fingers to notify js for reclaiming the finger colors.
403 curr_tids = [finger['tid'] for finger in converted['fingers']]
404 for tid in set(self._prev_tids) - set(curr_tids):
405 leaving_finger = {'tid': tid, 'leaving': True}
406 converted['fingers'].append(leaving_finger)
407 self._prev_tids = curr_tids
408
Joseph Hwang02e829c2015-04-13 17:09:07 +0800409 # Convert raw events from a list of classes to a list of its strings
410 # so that the raw_events is serializable.
411 converted['raw_events'] = [str(event) for event in converted['raw_events']]
412
Joseph Hwang4782a042015-04-08 17:15:50 +0800413 return converted
414
415 def GetSnapshot(self):
416 """Get a snapshot from the touch device."""
417 return self._device.device.NextSnapshot()
418
419 def AddSnapshot(self, snapshot):
420 """Convert the snapshot to a proper format and publish it to clients."""
421 snapshot = self._ConvertNamedtupleToDict(snapshot)
422 cherrypy.engine.publish('websocket-broadcast', json.dumps(snapshot))
Joseph Hwang02e829c2015-04-13 17:09:07 +0800423 return snapshot
Joseph Hwang4782a042015-04-08 17:15:50 +0800424
425 def GetAndPlotSnapshots(self):
426 """Get and plot snapshots."""
427 cherrypy.log('Start getting the live stream snapshots....')
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800428 try:
429 with open(self._saved_file, 'w') as f:
430 while True:
431 try:
432 snapshot = self.GetSnapshot()
433 if not snapshot:
434 cherrypy.log('webplot is terminated.')
435 break
436 converted_snapshot = self.AddSnapshot(snapshot)
437 f.write('\n'.join(converted_snapshot['raw_events']) + '\n')
438 f.flush()
439 except KeyboardInterrupt:
440 cherrypy.log('Keyboard Interrupt accepted')
441 cherrypy.log('webplot is being terminated...')
442 state.QuitAndShutdown()
443 except IOError as e:
444 _IOError(e, self._saved_file)
445 state.QuitAndShutdown()
Joseph Hwang4782a042015-04-08 17:15:50 +0800446
447 def Publish(self, msg):
448 """Publish a message to clients."""
449 cherrypy.engine.publish('websocket-broadcast', TextMessage(msg))
450
451 def Clear(self):
452 """Notify clients to clear the display."""
453 self.Publish('clear')
454
455 def Quit(self):
456 """Notify clients to quit.
457
458 Note that the cherrypy engine would quit accordingly.
459 """
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800460 state.QuitAndShutdown()
Joseph Hwang4782a042015-04-08 17:15:50 +0800461
462 def Save(self):
463 """Notify clients to save the screen."""
464 self.Publish('save')
465
466 def Url(self):
467 """The url the server is serving at."""
468 return 'http://%s:%d' % (self._server_addr, self._server_port)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700469
470
Joseph Hwanga1c84782015-04-14 15:07:00 +0800471def _CheckLegalUser():
472 """If this program is run in chroot, it should not be run as root for security
473 reason.
474 """
475 if os.path.exists('/etc/cros_chroot_version') and os.getuid() == 0:
476 print ('You should run webplot in chroot as a regular user '
477 'instead of as root.\n')
478 exit(1)
479
480
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700481def _ParseArguments():
482 """Parse the command line options."""
483 parser = argparse.ArgumentParser(description='Webplot Server')
Charlie Mooney8026f2a2015-04-02 13:01:28 -0700484 parser.add_argument('-d', '--dut_addr', default=None,
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700485 help='the address of the dut')
Joseph Hwang59db4412015-04-09 12:43:16 +0800486
487 # Make an exclusive group to make the webplot.py command option
488 # consistent with the webplot.sh script command option.
489 # What is desired:
490 # When no command option specified in webplot.sh/webplot.py: grab is True
491 # When '--grab' option specified in webplot.sh/webplot.py: grab is True
492 # When '--nograb' option specified in webplot.sh/webplot.py: grab is False
493 grab_group = parser.add_mutually_exclusive_group()
494 grab_group.add_argument('--grab', help='grab the device exclusively',
495 action='store_true')
496 grab_group.add_argument('--nograb', help='do not grab the device',
497 action='store_true')
498
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700499 parser.add_argument('--is_touchscreen', help='the DUT is touchscreen',
500 action='store_true')
Joseph Hwang59db4412015-04-09 12:43:16 +0800501 parser.add_argument('-p', '--server_port', default=80, type=int,
502 help='the port the web server to listen to (default: 80)')
503 parser.add_argument('-s', '--server_addr', default='localhost',
504 help='the address the webplot http server listens to')
505 parser.add_argument('-t', '--dut_type', default='chromeos', type=str.lower,
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700506 help='dut type: chromeos, android')
507 args = parser.parse_args()
Joseph Hwang59db4412015-04-09 12:43:16 +0800508
509 args.grab = not args.nograb
510
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700511 return args
512
513
514def Main():
515 """The main function to launch webplot service."""
Joseph Hwanga1c84782015-04-14 15:07:00 +0800516 _CheckLegalUser()
517
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700518 configure_logger(level=logging.DEBUG)
519 args = _ParseArguments()
520
521 print '\n' + '-' * 70
522 cherrypy.log('dut machine type: %s' % args.dut_type)
523 cherrypy.log('dut\'s touch device: %s' %
524 ('touchscreen' if args.is_touchscreen else 'touchpad'))
525 cherrypy.log('dut address: %s' % args.dut_addr)
526 cherrypy.log('web server address: %s' % args.server_addr)
527 cherrypy.log('web server port: %s' % args.server_port)
Joseph Hwang59db4412015-04-09 12:43:16 +0800528 cherrypy.log('grab the touch device: %s' % args.grab)
529 if args.dut_type == 'android' and args.grab:
530 cherrypy.log('Warning: the grab option is not supported on Android devices'
531 ' yet.')
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700532 cherrypy.log('touch events are saved in %s' % SAVED_FILE)
533 print '-' * 70 + '\n\n'
534
535 if args.server_port == 80:
536 url = args.server_addr
537 else:
538 url = '%s:%d' % (args.server_addr, args.server_port)
539
540 msg = 'Type "%s" in browser %s to see finger traces.\n'
541 if args.server_addr == 'localhost':
542 which_machine = 'on the webplot server machine'
543 else:
544 which_machine = 'on any machine'
545
546 print '*' * 70
547 print msg % (url, which_machine)
548 print 'Press \'q\' on the browser to quit.'
549 print '*' * 70 + '\n\n'
550
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700551 # Instantiate a touch device.
Joseph Hwang59db4412015-04-09 12:43:16 +0800552 device = TouchDeviceWrapper(args.dut_type, args.dut_addr, args.is_touchscreen,
553 args.grab)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700554
Joseph Hwang4782a042015-04-08 17:15:50 +0800555 # Instantiate a webplot server daemon and start it.
556 webplot = Webplot(args.server_addr, args.server_port, device)
557 webplot.start()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700558
Joseph Hwang4782a042015-04-08 17:15:50 +0800559 # Get touch snapshots from the touch device and have clients plot them.
560 webplot.GetAndPlotSnapshots()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700561
562
563if __name__ == '__main__':
564 Main()