webplot: handle IOError properly and quit only once

This patch handles the IOError exceptions. The exception may occur
when webplot tries to open a file owned by root which would fail
because an ordinary user does not have the permission to overwrite
those files. In that condition, the server notifies the clients to
quit and terminates itself properly.

Note that the event data file or the image file may be owned by root
if a user ran webplot as root previously. In that case, print out the
error message, tell the user to remove the files owned by root, and
restart webplot again.

This patch also adds quit flags in both the server and the client
to prevent from sending the quit message multiple times.

BUG=chromium:476833
TEST=Launch a webplot server on a chromebook
$ webplot

Create /tmp/webplot.dat and /tmp/webplot.png with vi as root, and
write a random message in the files.
Observe that the program would terminate properly and print out
the following messages about how to handle the situation.

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
[Errno 13] Permission denied: '/tmp/webplot.dat'
It is likely that /tmp/webplot.dat is owned by root.
Please remove the file and then run webplot again.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

Change-Id: If365400f8cf38f7a5876866b2bb4028334cfea97
Reviewed-on: https://chromium-review.googlesource.com/265523
Reviewed-by: Charlie Mooney <charliemooney@chromium.org>
Commit-Queue: Shyh-In Hwang <josephsih@chromium.org>
Tested-by: Shyh-In Hwang <josephsih@chromium.org>
diff --git a/webplot/webplot.py b/webplot/webplot.py
index c7a0dbb..228111d 100755
--- a/webplot/webplot.py
+++ b/webplot/webplot.py
@@ -90,7 +90,15 @@
      The cherrypy engine exits if this is the last client.
   """
   cherrypy.log('Cherrypy engine is sending quit message to clients.')
-  cherrypy.engine.publish('websocket-broadcast', TextMessage('quit'))
+  state.QuitAndShutdown()
+
+
+def _IOError(e, filename):
+  err_msg = ['\n', '!' * 60, str(e),
+             'It is likely that %s is owned by root.' % filename,
+             'Please remove the file and then run webplot again.',
+             '!' * 60, '\n']
+  cherrypy.log('\n'.join(err_msg))
 
 
 class WebplotWSHandler(WebSocket):
@@ -129,8 +137,12 @@
   @staticmethod
   def SaveImage(image_data, image_file):
     """Decoded the base64 image data and save it in the file."""
-    with open(image_file, 'w') as f:
-      f.write(base64.b64decode(image_data))
+    try:
+      with open(image_file, 'w') as f:
+        f.write(base64.b64decode(image_data))
+    except IOError as e:
+      _IOError(e, image_file)
+      state.QuitAndShutdown()
 
 
 class TouchDeviceWrapper(object):
@@ -173,6 +185,7 @@
     self.count = 0;
     self.lock = threading.Lock()
     self.shutdown_timer = None
+    self.quit_flag = False
 
   def IncCount(self):
     """Increase the connection count, and cancel the shutdown timer if exists.
@@ -209,6 +222,13 @@
     cherrypy.log('Shutdown timer expires. Cherrypy server for Webplot exits.')
     cherrypy.engine.exit()
 
+  def QuitAndShutdown(self):
+    """The server notifies clients to quit and then shuts down."""
+    if not self.quit_flag:
+      self.quit_flag = True
+      cherrypy.engine.publish('websocket-broadcast', TextMessage('quit'))
+      self.ShutdownWhenNoConnections()
+
 
 class Root(object):
   """A class to handle requests about docroot."""
@@ -405,23 +425,24 @@
   def GetAndPlotSnapshots(self):
     """Get and plot snapshots."""
     cherrypy.log('Start getting the live stream snapshots....')
-    with open(self._saved_file, 'w') as f:
-      while True:
-        try:
-          snapshot = self.GetSnapshot()
-          if not snapshot:
-            cherrypy.log('webplot is terminated.')
-            break
-          converted_snapshot = self.AddSnapshot(snapshot)
-          f.write('\n'.join(converted_snapshot['raw_events']) + '\n')
-          f.flush()
-        except KeyboardInterrupt:
-          cherrypy.log('Keyboard Interrupt accepted')
-          cherrypy.log('webplot is being terminated...')
-          # Notify the clients to quit.
-          self.Publish('quit')
-          # If there is no client connection, the cherrypy server just exits.
-          state.ShutdownWhenNoConnections()
+    try:
+      with open(self._saved_file, 'w') as f:
+        while True:
+          try:
+            snapshot = self.GetSnapshot()
+            if not snapshot:
+              cherrypy.log('webplot is terminated.')
+              break
+            converted_snapshot = self.AddSnapshot(snapshot)
+            f.write('\n'.join(converted_snapshot['raw_events']) + '\n')
+            f.flush()
+          except KeyboardInterrupt:
+            cherrypy.log('Keyboard Interrupt accepted')
+            cherrypy.log('webplot is being terminated...')
+            state.QuitAndShutdown()
+    except IOError as e:
+      _IOError(e, self._saved_file)
+      state.QuitAndShutdown()
 
   def Publish(self, msg):
     """Publish a message to clients."""
@@ -436,7 +457,7 @@
 
     Note that the cherrypy engine would quit accordingly.
     """
-    self.Publish('quit')
+    state.QuitAndShutdown()
 
   def Save(self):
     """Notify clients to save the screen."""