Initial code for webtry, a web application for allowing users to try out Skia.
authorcommit-bot@chromium.org <commit-bot@chromium.org@2bbb7eff-a529-9590-31e7-b0007b416f81>
Wed, 9 Apr 2014 18:59:44 +0000 (18:59 +0000)
committercommit-bot@chromium.org <commit-bot@chromium.org@2bbb7eff-a529-9590-31e7-b0007b416f81>
Wed, 9 Apr 2014 18:59:44 +0000 (18:59 +0000)
Currently running at http://108.170.220.126:8000/

BUG=skia:
R=mtklein@google.com

Author: jcgregorio@google.com

Review URL: https://codereview.chromium.org/228693002

git-svn-id: http://skia.googlecode.com/svn/trunk@14114 2bbb7eff-a529-9590-31e7-b0007b416f81

18 files changed:
experimental/webtry/DESIGN [new file with mode: 0644]
experimental/webtry/README
experimental/webtry/TODO [new file with mode: 0644]
experimental/webtry/index.html [deleted file]
experimental/webtry/main.cpp [new file with mode: 0644]
experimental/webtry/result.cpp [new file with mode: 0644]
experimental/webtry/seccomp_bpf.h [new file with mode: 0644]
experimental/webtry/server.py [deleted file]
experimental/webtry/setup/continue_install [new file with mode: 0755]
experimental/webtry/setup/webtry_setup.sh [new file with mode: 0755]
experimental/webtry/sys/webtry_init [new file with mode: 0644]
experimental/webtry/sys/webtry_monit [new file with mode: 0644]
experimental/webtry/sys/webtry_schroot [new file with mode: 0644]
experimental/webtry/template.cpp [deleted file]
experimental/webtry/templates/index.html [new file with mode: 0644]
experimental/webtry/templates/template.cpp [new file with mode: 0644]
experimental/webtry/webtry.go [new file with mode: 0644]
gyp/webtry.gyp

diff --git a/experimental/webtry/DESIGN b/experimental/webtry/DESIGN
new file mode 100644 (file)
index 0000000..ddc26f5
--- /dev/null
@@ -0,0 +1,121 @@
+Design
+======
+
+
+Overview
+--------
+Allows trying out Skia code in the browser.
+
+
+Security
+--------
+We're putting a C++ compiler on the web, and promising to run the results of
+user submitted code, so security is a large concern. Security is handled in a
+layered approach, using a combination of seccomp-bpf, chroot jail and rlimits.
+
+*seccomp-bpf* - Used to limit the types of system calls that the user code can
+make. Any attempts to make a system call that isn't allowed causes the
+application to terminate immediately.
+
+*chroot jail* - The code is run in a chroot jail, making the rest of the
+operating system files unreachable from the running code.
+
+*rlimits* - Used to limit the resources the running code can get access to,
+for example runtime is limited to 5s of CPU.
+
+User submitted code is also restricted in the following ways:
+  * Limited to 10K of code total.
+  * No preprocessor use is allowed (no lines can begin with \s*#).
+
+
+Architecture
+------------
+
+The server runs on GCE, and consists of a Go Web Server that calls out to the
+c++ compiler and executes code in a chroot jail. See the diagram below:
+
+                           
+   +–––––––––––––+         
+   |             |         
+   |  Browser    |         
+   |             |         
+   +––––––+––––––+         
+          |                
+   +––––––+––––––+         
+   |             |         
+   |             |         
+   | Web Server  |         
+   |             |         
+   |   (Go)      |         
+   |             |         
+   |             |         
+   +–––––––+–––––+         
+           |               
+   +–––––––+––––––––––+    
+   | chroot jail      |    
+   |  +––––––––––––––+|    
+   |  | seccomp      ||    
+   |  |  +––––––––––+||    
+   |  |  |User code |||    
+   |  |  |          |||    
+   |  |  +----------+||    
+   |  +––------------+|    
+   |                  |    
+   +––––––––––––––––––+    
+                           
+                           
+The user code is expanded into a simple template and linked against libskia
+and a couple other .o files that contain main() and the code that sets up the
+seccomp and rlimit restrictions. This code also sets up the SkCanvas that is
+handed to the user code. Any code the user submits is restricted to running in
+a single function that looks like this:
+
+
+    void draw(SkCanvas* canvas) {
+      // User code goes here.
+    }
+
+The user code is tracked by taking an MD5 hash of the code The template is
+expanded out into <hash>.cpp, which is compiled into <hash>.o, which is then
+linked together with all the other libs and object files to create an
+executable named <hash>.  That executable is copied into a directory
+/home/webtry/inout, that is accessible to both the web server and the schroot
+jail. The application is then run in the schroot jail, writing its response,
+<hash>.png, out into the same directory, /home/webtry/inout/, where is it read
+by the web server and returned to the user.
+
+Startup and config
+------------------
+The server is started and stopped via:
+
+    sudo /etc/init.d/webtry [start|stop|restart]
+
+By sysv init only handles starting and stopping a program once, so we use
+Monit to monitor the application and restart it if it crashes. The config
+is in:
+
+    /etc/monit/conf.d/webtry
+
+The chroot jail is implemented using schroot, its configuration
+file is found in:
+
+    /etc/schroot/chroot.d/webtry
+
+The seccomp configuration is in main.cpp and only allows the following system
+calls:
+
+    exit_group
+    exit
+    fstat
+    read
+    write
+    close
+    mmap
+    munmap
+    brk
+
+Installation
+------------
+See the README file.
+
+
index a36ac8a3fedf6b0ad7512982bd56e681934b0c35..c7573110f6197d01a22b66c29bad66f7f63a4156 100644 (file)
@@ -3,14 +3,72 @@ WebTry
 
 Allows trying out Skia code in the browser. Run a local webserver
 and from the pages it serves try out Skia code and see the results
-immediately.
+immediately. To make sandboxing easier this must be built w/GPU off.
 
-Running
-=======
+Running Locally
+===============
 
+$ GYP_GENERATORS=ninja  ./gyp_skia  gyp/webtry.gyp gyp/most.gyp -Dskia_gpu=0
+$ ninja -C out/Debug webtry
 $ cd experimental/webtry
-$ python server.py
+$ go build webtry.go
+$ ./webtry
 
-Then visit http://localhost:8765 in your browser.
+Then visit http://localhost:8000 in your browser.
 
+Only tested under linux, doubtful it will work on other platforms.
+
+Full Server Setup
+=================
+
+Create a GCE instance:
+
+gcutil --project=google.com:skia-buildbots addinstance skia-webtry-b \
+  --zone=us-central2-b --external_ip_address=108.170.220.126 \
+  --service_account=default \
+  --service_account_scopes="https://www.googleapis.com/auth/devstorage.full_control" \
+  --network=default --machine_type=n1-standard-1 --image=backports-debian-7-wheezy-v20140331 \
+  --persistent_boot_disk
+
+SSH into the instance:
+
+  gcutil --project=google.com:skia-buildbots ssh --ssh_user=default skia-webtry-b
+
+
+The following things only need to be done once
+----------------------------------------------
+
+1. sudo apt-get install git schroot debootstrap
+2. git clone https://skia.googlesource.com/skia
+3. Add the following to /etc/fstab and reboot:
+
+  none /dev/shm tmpfs rw,nosuid,nodev,noexec 0 0
+
+The above will allow ninja to run. See http://stackoverflow.com/questions/2009278/python-multiprocessing-permission-denied
+
+4. Add the following to the /etc/schroot/minimal/fstab:
+
+  /home/webtry/inout             /inout  none    rw,bind         0       0
+
+5. Change /etc/monit/monitrc to:
+
+  set daemon 2
+
+then run the following so it applies:
+
+  sudo /etc/init.d/monit restart
+
+This means that monit will poll every two seconds that our application is up and running.
+
+Do the following the first time you setup a machine, and each time you want to update the code running on the server
+--------------------------------------------------------------------------------------------------------------------
+
+cd ~/skia/experimental/webtry/setup
+./webtry_setup.sh
+
+
+Do these steps only once, but only after running webtry_setup.sh the first time
+-------------------------------------------------------------------------------
+
+1. sudo debootstrap --variant=minbase wheezy /srv/chroot/webtry
 
diff --git a/experimental/webtry/TODO b/experimental/webtry/TODO
new file mode 100644 (file)
index 0000000..81382cd
--- /dev/null
@@ -0,0 +1,11 @@
+ - Add -port flag to webtry.go.
+ - Add flag for inout directory to webtry.go.
+ - In webtry.go add mutexes per hash, to avoid conflicts of writing the same file at the same time.
+ - Don't allow #macros in user code.
+ - Limit the size of the user code submitted.
+ - Add font support in the c++ template.
+ - Add in all the Sk header files that could be needed.
+ - Clean up web page appearance.
+ - Add inline links to doxygen.
+ - Set wait cursor when doing the compiling.
+ - Add monitoring and probing (nagios).
diff --git a/experimental/webtry/index.html b/experimental/webtry/index.html
deleted file mode 100644 (file)
index b3903e6..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>Skia WebTry</title>
-    <meta charset='utf-8' />
-    <style type="text/css" media="screen">
-        textarea {
-            margin-left: 0;
-            border: solid 1px #ccc;
-            color: green;
-        }
-        pre, code {
-            padding: 0;
-            color: green;
-        }
-    </style>
-</head>
-<body>
-  <pre><code>#include "SkCanvas.h"
-#include "SkGraphics.h"
-#include "SkImageEncoder.h"
-#include "SkImageInfo.h"
-#include "SkForceLinking.h"
-
-int main() {
-  SkForceLinking(false);
-  SkGraphics::Init();
-
-  SkImageInfo info = SkImageInfo::MakeN32(300, 300, kPremul_SkAlphaType);
-  SkBitmap bitmap;
-  bitmap.setConfig(info);
-  bitmap.allocPixels();
-  SkCanvas c(bitmap);
-  c.drawColor(SK_ColorWHITE);
-
-  <textarea name='code' id='code' rows='20' cols='80'>SkPaint p;
-p.setColor(SK_ColorRED);
-p.setAntiAlias(true);
-p.setStyle(SkPaint::kStroke_Style);
-p.setStrokeWidth(10);
-
-c.drawLine(20, 20, 100, 100, p);
-</textarea>
-
-  if (!SkImageEncoder::EncodeFile("foo.png", bitmap, SkImageEncoder::kPNG_Type, 100)) {
-    printf("Failed to encode\n");
-  }
-}
-</code></pre>
-
-  <p>Image appears here:</p>
-  <img id='img' src=''/>
-
-  <pre><code id='output'></code></pre>
-
-  <input type='button' value='Run' id='run'>
-  <script type='text/javascript' charset='utf-8'>
-      var run = document.getElementById('run');
-      var code = document.getElementById('code');
-      var output = document.getElementById('output');
-      var img = document.getElementById('img');
-
-      function codeComplete(e) {
-        // The response is JSON of the form:
-        // {
-        //   "message": "you had an error...",
-        //   "img": "<base64 encoded image but only on success>"
-        // }
-        //
-        // The img is optional and only appears if there is a valid
-        // image to display.
-        console.log(e.target.response);
-        body = JSON.parse(e.target.response);
-        output.innerText = body.message;
-        if (body.hasOwnProperty('img')) {
-            img.src = 'data:image/png;base64,' + body.img;
-        } else {
-            img.src = '';
-        }
-      }
-
-      function codeError(e) {
-        alert('Something bad happened: ' + e);
-      }
-
-      run.addEventListener('click', onSubmitCode);
-      function onSubmitCode() {
-          var req = new XMLHttpRequest();
-          req.addEventListener('load', codeComplete);
-          req.addEventListener('error', codeError);
-          req.overrideMimeType('application/json');
-          req.open('POST', '.', true);
-          req.send(code.value + '\r\nEOF\r\n');
-      }
-  </script>
-</body>
-</html>
diff --git a/experimental/webtry/main.cpp b/experimental/webtry/main.cpp
new file mode 100644 (file)
index 0000000..9e7df14
--- /dev/null
@@ -0,0 +1,114 @@
+#include <sys/time.h>
+#include <sys/resource.h>
+
+#include "SkCanvas.h"
+#include "SkCommandLineFlags.h"
+#include "SkData.h"
+#include "SkForceLinking.h"
+#include "SkGraphics.h"
+#include "SkImageEncoder.h"
+#include "SkImageInfo.h"
+#include "SkStream.h"
+#include "SkSurface.h"
+
+#include "seccomp_bpf.h"
+
+__SK_FORCE_IMAGE_DECODER_LINKING;
+
+DEFINE_string(out, "", "Filename of the PNG to write to.");
+
+static bool install_syscall_filter() {
+    struct sock_filter filter[] = {
+        /* Grab the system call number. */
+        EXAMINE_SYSCALL,
+        /* List allowed syscalls. */
+        ALLOW_SYSCALL(exit_group),
+        ALLOW_SYSCALL(exit),
+        ALLOW_SYSCALL(fstat),
+        ALLOW_SYSCALL(read),
+        ALLOW_SYSCALL(write),
+        ALLOW_SYSCALL(close),
+        ALLOW_SYSCALL(mmap),
+        ALLOW_SYSCALL(munmap),
+        ALLOW_SYSCALL(brk),
+        KILL_PROCESS,
+    };
+    struct sock_fprog prog = {
+        SK_ARRAY_COUNT(filter),
+        filter,
+    };
+
+    // Lock down the app so that it can't get new privs, such as setuid.
+    // Calling this is a requirement for an unpriviledged process to use mode
+    // 2 seccomp filters, ala SECCOMP_MODE_FILTER, otherwise we'd have to be
+    // root.
+    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
+        perror("prctl(NO_NEW_PRIVS)");
+        goto failed;
+    }
+    // Now call seccomp and restrict the system calls that can be made to only
+    // the ones in the provided filter list.
+    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
+        perror("prctl(SECCOMP)");
+        goto failed;
+    }
+    return true;
+
+failed:
+    if (errno == EINVAL) {
+        fprintf(stderr, "SECCOMP_FILTER is not available. :(\n");
+    }
+    return false;
+}
+
+static void setLimits() {
+    struct rlimit n;
+
+    // Limit to 5 seconds of CPU.
+    n.rlim_cur = 5;
+    n.rlim_max = 5;
+    if (setrlimit(RLIMIT_CPU, &n)) {
+        perror("setrlimit(RLIMIT_CPU)");
+    }
+
+    // Limit to 50M of Address space.
+    n.rlim_cur = 50000000;
+    n.rlim_max = 50000000;
+    if (setrlimit(RLIMIT_AS, &n)) {
+        perror("setrlimit(RLIMIT_CPU)");
+    }
+}
+
+extern void draw(SkCanvas* canvas);
+
+int main(int argc, char** argv) {
+    SkCommandLineFlags::Parse(argc, argv);
+    SkAutoGraphics init;
+
+    if (FLAGS_out.count() == 0) {
+      perror("The --out flag must have an argument.");
+      return 1;
+    }
+    SkFILEWStream stream(FLAGS_out[0]);
+
+    SkImageInfo info = SkImageInfo::MakeN32(300, 300, kPremul_SkAlphaType);
+    SkAutoTUnref<SkSurface> surface(SkSurface::NewRaster(info));
+    SkCanvas* canvas = surface->getCanvas();
+
+    setLimits();
+
+    if (!install_syscall_filter()) {
+        return 1;
+    }
+
+    draw(canvas);
+
+    // Write out the image as a PNG.
+    SkAutoTUnref<SkImage> image(surface->newImageSnapshot());
+    SkAutoTUnref<SkData> data(image->encode(SkImageEncoder::kPNG_Type, 100));
+    if (NULL == data.get()) {
+        printf("Failed to encode\n");
+        exit(1);
+    }
+    stream.write(data->data(), data->size());
+}
diff --git a/experimental/webtry/result.cpp b/experimental/webtry/result.cpp
new file mode 100644 (file)
index 0000000..d06ef9c
--- /dev/null
@@ -0,0 +1,28 @@
+#include "SkCanvas.h"
+#include "SkData.h"
+#include "SkForceLinking.h"
+#include "SkGraphics.h"
+#include "SkImageEncoder.h"
+#include "SkImageInfo.h"
+#include "SkStream.h"
+#include "SkSurface.h"
+
+
+void draw(SkCanvas* canvas) {
+#line 1
+SkPaint p;
+#line 2
+p.setColor(SK_ColorRED);
+#line 3
+p.setAntiAlias(true);
+#line 4
+p.setStyle(SkPaint::kStroke_Style);
+#line 5
+p.setStrokeWidth(10);
+#line 6
+
+#line 7
+canvas->drawLine(20, 20, 100, 100, p);
+#line 8
+
+}
diff --git a/experimental/webtry/seccomp_bpf.h b/experimental/webtry/seccomp_bpf.h
new file mode 100644 (file)
index 0000000..8bbe99b
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * seccomp example for x86 (32-bit and 64-bit) with BPF macros
+ *
+ * Copyright (c) 2012 The Chromium OS Authors <chromium-os-dev@chromium.org>
+ * Authors:
+ *  Will Drewry <wad@chromium.org>
+ *  Kees Cook <keescook@chromium.org>
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ * A stripped down version of the file found in this tutorial: http://outflux.net/teach-seccomp/.
+ */
+#ifndef _SECCOMP_BPF_H_
+#define _SECCOMP_BPF_H_
+
+#define _GNU_SOURCE 1
+#include <stdio.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <signal.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/prctl.h>
+
+#include <linux/unistd.h>
+#include <linux/audit.h>
+#include <linux/filter.h>
+#include <linux/seccomp.h>
+
+#define syscall_nr (offsetof(struct seccomp_data, nr))
+
+#define EXAMINE_SYSCALL \
+       BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_nr)
+
+#define ALLOW_SYSCALL(name) \
+       BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_##name, 0, 1), \
+       BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW)
+
+#define KILL_PROCESS \
+       BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL)
+
+#endif /* _SECCOMP_BPF_H_ */
diff --git a/experimental/webtry/server.py b/experimental/webtry/server.py
deleted file mode 100644 (file)
index 8367b3e..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-import BaseHTTPServer
-import base64
-import json
-import logging
-import string
-import subprocess
-
-HOST_NAME = 'localhost'
-PORT_NUMBER = 8765
-
-def runCode(usercode):
-  f = open('template.cpp', 'rb')
-  template = string.Template(f.read())
-  f.close()
-
-  code = template.substitute(usercode=usercode)
-
-  f = open('result.cpp', 'wb')
-  f.write(code)
-  f.close()
-
-  msg = ""
-  img = ""
-  try:
-    logging.info("compiling")
-    msg = subprocess.check_output('ninja -C ../../out/Debug webtry'.split())
-    try:
-      logging.info("running")
-      msg = subprocess.check_output('../../out/Debug/webtry'.split())
-      f = open('foo.png', 'rb')
-      img = base64.b64encode(f.read())
-      f.close()
-    except subprocess.CalledProcessError as e:
-      logging.info(e)
-      msg = e.output
-  except subprocess.CalledProcessError as e:
-    logging.info(e)
-    msg = e.output
-
-  retval = {
-    'message': msg
-  }
-  if img:
-    retval['img'] = img
-  return retval
-
-class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
-  def do_POST(self):
-    logging.info("POST")
-    body = ""
-    l = self.rfile.readline()
-    while l.strip() != "EOF":
-      body += l
-      l = self.rfile.readline()
-    self.send_response(200)
-    self.send_header("Content-type", "application/json")
-    self.end_headers()
-    resp = runCode(body)
-    self.wfile.write(json.dumps(resp))
-    self.end_headers()
-
-  def do_GET(self):
-    """Respond to a GET request."""
-    self.send_response(200)
-    self.send_header("Content-type", "text/html")
-    self.end_headers()
-    f = open('index.html', 'rb')
-    self.wfile.write(f.read())
-    f.close()
-
-if __name__ == '__main__':
-  server_class = BaseHTTPServer.HTTPServer
-  httpd = server_class((HOST_NAME, PORT_NUMBER), MyHandler)
-  logging.info("Server Start: %s:%s" % (HOST_NAME, PORT_NUMBER))
-  try:
-    httpd.serve_forever()
-  except KeyboardInterrupt:
-    pass
-  httpd.server_close()
diff --git a/experimental/webtry/setup/continue_install b/experimental/webtry/setup/continue_install
new file mode 100755 (executable)
index 0000000..0db794b
--- /dev/null
@@ -0,0 +1,56 @@
+#!/bin/bash
+#
+# Don't execute this script directly, instead it is copied into the webtry
+# user's directory and executed as the user webtry by the webtry_setup.sh
+# script.
+#
+# See the README file for detailed installation instructions.
+cd
+pwd
+
+# Install depot_tools.
+if [ -d depot_tools ]; then
+  (cd depot_tools && git pull);
+else
+  git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git;
+fi
+export PATH=$PATH:$HOME/depot_tools
+
+# Install Go
+if [ -d go ]; then
+  echo Go already installed.
+else
+  wget https://go.googlecode.com/files/go1.2.1.linux-amd64.tar.gz
+  tar -xzf go1.2.1.linux-amd64.tar.gz
+fi
+export GOROOT=$HOME/go
+export PATH=$PATH:$GOROOT/bin
+
+mkdir /home/webtry/cache
+mkdir /home/webtry/inout
+chmod 777 /home/webtry/inout
+
+# Sometimes you need to test patches on the server, to do that uncomment
+# the following commented out lines and update the PATCH env variable to the
+# name of the codereview to use.
+
+# rm -rf skia
+
+# Checkout the skia code and dependencies.
+mkdir skia
+cd skia
+gclient config --name . https://skia.googlesource.com/skia.git
+gclient sync
+git checkout master
+
+# PATCH=issue196723021_100001.diff
+# rm $PATCH
+# wget https://codereview.chromium.org/download/$PATCH
+# git apply $PATCH
+
+GYP_GENERATORS=ninja ./gyp_skia gyp/webtry.gyp gyp/most.gyp -Dskia_gpu=0
+ninja -C out/Debug webtry
+
+cd experimental/webtry
+
+go build webtry.go
diff --git a/experimental/webtry/setup/webtry_setup.sh b/experimental/webtry/setup/webtry_setup.sh
new file mode 100755 (executable)
index 0000000..6658ca8
--- /dev/null
@@ -0,0 +1,32 @@
+#!/bin/bash
+#
+# Script to setup a GCE instance to run the webtry server.
+# For full instructions see the README file.
+sudo apt-get install schroot debootstrap monit
+sudo apt-get install g++ libfreetype6 libfreetype6-dev libpng12-0 libpng12-dev libglu1-mesa-dev mesa-common-dev freeglut3-dev libgif-dev libfontconfig libfontconfig-dev
+
+echo "Adding the webtry user account"
+sudo adduser webtry
+
+sudo cp continue_install /home/webtry/continue_install
+sudo chmod 766 /home/webtry/continue_install
+sudo chown webtry:webtry /home/webtry/continue_install
+sudo su webtry -c /home/webtry/continue_install
+
+sudo mkdir -p /srv/chroot/webtry
+sudo cp /home/webtry/skia/experimental/webtry/sys/webtry_schroot /etc/schroot/chroot.d/webtry
+
+sudo mkdir /srv/chroot/webtry/etc
+sudo mkdir /srv/chroot/webtry/bin
+sudo cp /bin/sh /srv/chroot/webtry/bin/sh
+
+# Copy all the dependent libraries into the schroot.
+sudo cp --parents `ldd /home/webtry/skia/out/Debug/webtry | cut -d " " -f 3` /srv/chroot/webtry
+sudo cp --parents `ldd /bin/sh | cut -d " " -f 3` /srv/chroot/webtry
+
+sudo cp /home/webtry/skia/experimental/webtry/sys/webtry_init /etc/init.d/webtry
+sudo cp /home/webtry/skia/experimental/webtry/sys/webtry_monit /etc/monit/conf.d/webtry
+sudo chmod 744 /etc/init.d/webtry
+
+# Confirm that monit is happy.
+sudo monit -t
diff --git a/experimental/webtry/sys/webtry_init b/experimental/webtry/sys/webtry_init
new file mode 100644 (file)
index 0000000..359e19e
--- /dev/null
@@ -0,0 +1,159 @@
+#! /bin/sh
+### BEGIN INIT INFO
+# Provides:          webtry
+# Required-Start:    $remote_fs $syslog
+# Required-Stop:     $remote_fs $syslog
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: Start webtry.
+# Description:       Web server for trying Skia C++ code.
+### END INIT INFO
+
+# Author: Joe Gregorio <jcgregorio@google.com>
+#
+# Copied from /etc/init.d/skeleton and modified only the following
+# environment variables and updated the start-stop-daemon calls
+# in do_start() to add --make-pidfile, --background, and --chuid.
+
+# Do NOT "set -e"
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+DESC="The Skia webtry application."
+NAME=webtry
+DAEMON=/home/webtry/skia/experimental/webtry/$NAME
+DAEMON_ARGS="--use_chroot"
+PIDFILE=/var/run/$NAME.pid
+SCRIPTNAME=/etc/init.d/$NAME
+
+# Exit if the package is not installed
+[ -x "$DAEMON" ] || exit 0
+
+# Read configuration variable file if it is present
+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
+# and status_of_proc is working.
+. /lib/lsb/init-functions
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+       # Return
+       #   0 if daemon has been started
+       #   1 if daemon was already running
+       #   2 if daemon could not be started
+       start-stop-daemon --start --pidfile $PIDFILE --exec $DAEMON --make-pidfile --background --chuid webtry --test > /dev/null \
+               || return 1
+       start-stop-daemon --start --pidfile $PIDFILE --exec $DAEMON --make-pidfile --background --chuid webtry --exec $DAEMON -- \
+               $DAEMON_ARGS \
+               || return 2
+       # Add code here, if necessary, that waits for the process to be ready
+       # to handle requests from services started subsequently which depend
+       # on this one.  As a last resort, sleep for some time.
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+       # Return
+       #   0 if daemon has been stopped
+       #   1 if daemon was already stopped
+       #   2 if daemon could not be stopped
+       #   other if a failure occurred
+       start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
+       RETVAL="$?"
+       [ "$RETVAL" = 2 ] && return 2
+       # Wait for children to finish too if this is a daemon that forks
+       # and if the daemon is only ever run from this initscript.
+       # If the above conditions are not satisfied then add some other code
+       # that waits for the process to drop all resources that could be
+       # needed by services started subsequently.  A last resort is to
+       # sleep for some time.
+       start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
+       [ "$?" = 2 ] && return 2
+       # Many daemons don't delete their pidfiles when they exit.
+       rm -f $PIDFILE
+       return "$RETVAL"
+}
+
+#
+# Function that sends a SIGHUP to the daemon/service
+#
+do_reload() {
+       #
+       # If the daemon can reload its configuration without
+       # restarting (for example, when it is sent a SIGHUP),
+       # then implement that here.
+       #
+       start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
+       return 0
+}
+
+case "$1" in
+  start)
+       [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+       do_start
+       case "$?" in
+               0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+               2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+       esac
+       ;;
+  stop)
+       [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+       do_stop
+       case "$?" in
+               0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+               2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+       esac
+       ;;
+  status)
+       status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+       ;;
+  #reload|force-reload)
+       #
+       # If do_reload() is not implemented then leave this commented out
+       # and leave 'force-reload' as an alias for 'restart'.
+       #
+       #log_daemon_msg "Reloading $DESC" "$NAME"
+       #do_reload
+       #log_end_msg $?
+       #;;
+  restart|force-reload)
+       #
+       # If the "reload" option is implemented then remove the
+       # 'force-reload' alias
+       #
+       log_daemon_msg "Restarting $DESC" "$NAME"
+       do_stop
+       case "$?" in
+         0|1)
+               do_start
+               case "$?" in
+                       0) log_end_msg 0 ;;
+                       1) log_end_msg 1 ;; # Old process is still running
+                       *) log_end_msg 1 ;; # Failed to start
+               esac
+               ;;
+         *)
+               # Failed to stop
+               log_end_msg 1
+               ;;
+       esac
+       ;;
+  *)
+       #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
+       echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
+       exit 3
+       ;;
+esac
+
+:
diff --git a/experimental/webtry/sys/webtry_monit b/experimental/webtry/sys/webtry_monit
new file mode 100644 (file)
index 0000000..5fccf8a
--- /dev/null
@@ -0,0 +1,4 @@
+check process webtry with pidfile /var/run/webtry.pid
+   start program = "/etc/init.d/webtry start"
+   stop program = "/etc/init.d/webtry stop"
+
diff --git a/experimental/webtry/sys/webtry_schroot b/experimental/webtry/sys/webtry_schroot
new file mode 100644 (file)
index 0000000..c2bdfd3
--- /dev/null
@@ -0,0 +1,9 @@
+[webtry]
+description=Chroot jail for webtry runs.
+type=directory
+directory=/srv/chroot/webtry
+users=default,webtry
+root-groups=default
+personality=linux
+preserve-environment=false
+profile=minimal
diff --git a/experimental/webtry/template.cpp b/experimental/webtry/template.cpp
deleted file mode 100644 (file)
index 20b9086..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-#include "SkCanvas.h"
-#include "SkGraphics.h"
-#include "SkImageEncoder.h"
-#include "SkImageInfo.h"
-#include "SkForceLinking.h"
-
-int main() {
-    SkForceLinking(false);
-    SkGraphics::Init();
-
-    SkImageInfo info = SkImageInfo::MakeN32(300, 300, kPremul_SkAlphaType);
-    SkBitmap bitmap;
-    bitmap.setConfig(info);
-    bitmap.allocPixels();
-    SkCanvas c(bitmap);
-    c.drawColor(SK_ColorWHITE);
-
-    $usercode
-
-    if (!SkImageEncoder::EncodeFile("foo.png", bitmap, SkImageEncoder::kPNG_Type, 100)) {
-        printf("Failed to encode\n");
-    }
-}
diff --git a/experimental/webtry/templates/index.html b/experimental/webtry/templates/index.html
new file mode 100644 (file)
index 0000000..c630f14
--- /dev/null
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Skia WebTry</title>
+    <meta charset='utf-8' />
+    <style type="text/css" media="screen">
+        textarea {
+            margin-left: 0;
+            border: solid 1px #ccc;
+            color: green;
+        }
+        pre, code {
+            padding: 0;
+            color: green;
+        }
+        #output {
+            color: #333;
+        }
+    </style>
+</head>
+<body>
+  <pre><code>#include "SkCanvas.h"
+#include "SkGraphics.h"
+#include "SkImageEncoder.h"
+#include "SkImageInfo.h"
+#include "SkForceLinking.h"
+
+int main() {
+  SkForceLinking(false);
+  SkGraphics::Init();
+
+  SkImageInfo info = SkImageInfo::MakeN32(300, 300, kPremul_SkAlphaType);
+  SkAutoTUnref&lt;SkSurface> surface(SkSurface::NewRaster(info));
+  SkCanvas* canvas = surface->getCanvas();
+
+  <textarea name='code' id='code' rows='20' cols='80'>SkPaint p;
+p.setColor(SK_ColorRED);
+p.setAntiAlias(true);
+p.setStyle(SkPaint::kStroke_Style);
+p.setStrokeWidth(10);
+
+canvas->drawLine(20, 20, 100, 100, p);
+</textarea>
+
+  if (!SkImageEncoder::EncodeFile("foo.png", bitmap, SkImageEncoder::kPNG_Type, 100)) {
+    printf("Failed to encode\n");
+  }
+}
+</code></pre>
+
+  <input type='button' value='Run' id='run'>
+
+  <p>Image appears here:</p>
+  <img id='img' src=''/>
+
+  <pre><code id='output'></code></pre>
+
+  <script type='text/javascript' charset='utf-8'>
+      var run = document.getElementById('run');
+      var code = document.getElementById('code');
+      var output = document.getElementById('output');
+      var img = document.getElementById('img');
+
+      function codeComplete(e) {
+        // The response is JSON of the form:
+        // {
+        //   "message": "you had an error...",
+        //   "img": "<base64 encoded image but only on success>"
+        // }
+        //
+        // The img is optional and only appears if there is a valid
+        // image to display.
+        console.log(e.target.response);
+        body = JSON.parse(e.target.response);
+        output.innerText = body.message;
+        if (body.hasOwnProperty('img')) {
+            img.src = 'data:image/png;base64,' + body.img;
+        } else {
+            img.src = '';
+        }
+      }
+
+      function codeError(e) {
+        alert('Something bad happened: ' + e);
+      }
+
+      run.addEventListener('click', onSubmitCode);
+      function onSubmitCode() {
+          var req = new XMLHttpRequest();
+          req.addEventListener('load', codeComplete);
+          req.addEventListener('error', codeError);
+          req.overrideMimeType('application/json');
+          req.open('POST', '.', true);
+          req.send(code.value);
+      }
+  </script>
+</body>
+</html>
diff --git a/experimental/webtry/templates/template.cpp b/experimental/webtry/templates/template.cpp
new file mode 100644 (file)
index 0000000..3543b0c
--- /dev/null
@@ -0,0 +1,13 @@
+#include "SkCanvas.h"
+#include "SkData.h"
+#include "SkForceLinking.h"
+#include "SkGraphics.h"
+#include "SkImageEncoder.h"
+#include "SkImageInfo.h"
+#include "SkStream.h"
+#include "SkSurface.h"
+
+
+void draw(SkCanvas* canvas) {
+{{.UserCode}}
+}
diff --git a/experimental/webtry/webtry.go b/experimental/webtry/webtry.go
new file mode 100644 (file)
index 0000000..d70102a
--- /dev/null
@@ -0,0 +1,226 @@
+package main
+
+import (
+       "bytes"
+       "crypto/md5"
+       "encoding/base64"
+       "encoding/json"
+       "flag"
+       "fmt"
+       "io/ioutil"
+       "log"
+       "net/http"
+       "os"
+       "os/exec"
+       "path/filepath"
+       "strings"
+       "text/template"
+)
+
+const (
+       RESULT_COMPILE = `c++ -DSK_GAMMA_SRGB -DSK_GAMMA_APPLY_TO_A8 -DSK_SCALAR_TO_FLOAT_EXCLUDED -DSK_ALLOW_STATIC_GLOBAL_INITIALIZERS=1 -DSK_SUPPORT_GPU=0 -DSK_SUPPORT_OPENCL=0 -DSK_FORCE_DISTANCEFIELD_FONTS=0 -DSK_SCALAR_IS_FLOAT -DSK_CAN_USE_FLOAT -DSK_SAMPLES_FOR_X -DSK_BUILD_FOR_UNIX -DSK_USE_POSIX_THREADS -DSK_SYSTEM_ZLIB=1 -DSK_DEBUG -DSK_DEVELOPER=1 -I../../src/core -I../../src/images -I../../tools/flags -I../../include/config -I../../include/core -I../../include/pathops -I../../include/pipe -I../../include/effects -I../../include/ports -I../../src/sfnt -I../../include/utils -I../../src/utils -I../../include/images -g -fno-exceptions -fstrict-aliasing -Wall -Wextra -Winit-self -Wpointer-arith -Wno-unused-parameter -Wno-c++11-extensions -Werror -m64 -fno-rtti -Wnon-virtual-dtor -c ../../../cache/%s.cpp -o ../../../cache/%s.o`
+       LINK           = `c++ -m64 -lstdc++ -lm -o ../../../inout/%s -Wl,--start-group ../../../cache/%s.o obj/experimental/webtry/webtry.main.o obj/experimental/webtry/webtry.syscall_reporter.o obj/gyp/libflags.a libskia_images.a libskia_core.a libskia_effects.a obj/gyp/libjpeg.a obj/gyp/libwebp_dec.a obj/gyp/libwebp_demux.a obj/gyp/libwebp_dsp.a obj/gyp/libwebp_enc.a obj/gyp/libwebp_utils.a libskia_utils.a libskia_opts.a libskia_opts_ssse3.a libskia_ports.a libskia_sfnt.a -Wl,--end-group -lpng -lz -lgif -lpthread -lfontconfig -ldl -lfreetype`
+)
+
+var (
+       // codeTemplate is the cpp code template the user's code is copied into.
+       codeTemplate *template.Template = nil
+
+       // index is the main index.html page we serve.
+       index []byte
+)
+
+// flags
+var (
+       useChroot = flag.Bool("use_chroot", false, "Run the compiled code in the schroot jail.")
+)
+
+// lineNumbers adds #line numbering to the user's code.
+func LineNumbers(c string) string {
+       lines := strings.Split(c, "\n")
+       ret := []string{}
+       for i, line := range lines {
+               ret = append(ret, fmt.Sprintf("#line %d", i+1))
+               ret = append(ret, line)
+       }
+       return strings.Join(ret, "\n")
+}
+
+func init() {
+       // Change the current working directory to the directory of the executable.
+       var err error
+       cwd, err := filepath.Abs(filepath.Dir(os.Args[0]))
+       if err != nil {
+               log.Fatal(err)
+       }
+       os.Chdir(cwd)
+
+       codeTemplate, err = template.ParseFiles(filepath.Join(cwd, "templates/template.cpp"))
+       if err != nil {
+               panic(err)
+       }
+       index, err = ioutil.ReadFile(filepath.Join(cwd, "templates/index.html"))
+       if err != nil {
+               panic(err)
+       }
+}
+
+// userCode is used in template expansion.
+type userCode struct {
+       UserCode string
+}
+
+// expandToFile expands the template and writes the result to the file.
+func expandToFile(filename string, code string, t *template.Template) error {
+       f, err := os.Create(filename)
+       if err != nil {
+               return err
+       }
+       defer f.Close()
+       return t.Execute(f, struct{ UserCode string }{UserCode: code})
+}
+
+// expandCode expands the template into a file and calculate the MD5 hash.
+func expandCode(code string) (string, error) {
+       h := md5.New()
+       h.Write([]byte(code))
+       hash := fmt.Sprintf("%x", h.Sum(nil))
+       // At this point we are running in skia/experimental/webtry, making cache a
+       // peer directory to skia.
+       // TODO(jcgregorio) Make all relative directories into flags.
+       err := expandToFile(fmt.Sprintf("../../../cache/%s.cpp", hash), code, codeTemplate)
+       return hash, err
+}
+
+// response is serialized to JSON as a response to POSTs.
+type response struct {
+       Message string `json:"message"`
+       Img     string `json:"img"`
+}
+
+// doCmd executes the given command line string in either the out/Debug
+// directory or the inout directory. Returns the stdout, and stderr in the case
+// of a non-zero exit code.
+func doCmd(commandLine string, moveToDebug bool) (string, error) {
+       log.Printf("Command: %q\n", commandLine)
+       programAndArgs := strings.SplitN(commandLine, " ", 2)
+       program := programAndArgs[0]
+       args := []string{}
+       if len(programAndArgs) > 1 {
+               args = strings.Split(programAndArgs[1], " ")
+       }
+       cmd := exec.Command(program, args...)
+       abs, err := filepath.Abs("../../out/Debug")
+       if err != nil {
+               return "", fmt.Errorf("Failed to find absolute path to Debug directory.")
+       }
+       if moveToDebug {
+               cmd.Dir = abs
+       } else if !*useChroot { // Don't set cmd.Dir when using chroot.
+               abs, err := filepath.Abs("../../../inout")
+               if err != nil {
+                       return "", fmt.Errorf("Failed to find absolute path to inout directory.")
+               }
+               cmd.Dir = abs
+       }
+       log.Printf("Run in directory: %q\n", cmd.Dir)
+       var stdOut bytes.Buffer
+       cmd.Stdout = &stdOut
+       var stdErr bytes.Buffer
+       cmd.Stderr = &stdErr
+       cmd.Start()
+       err = cmd.Wait()
+       message := stdOut.String()
+       log.Printf("StdOut: %s\n", message)
+       if err != nil {
+               log.Printf("Exit status: %s\n", err.Error())
+               log.Printf("StdErr: %s\n", stdErr.String())
+               message += stdErr.String()
+               return message, fmt.Errorf("Failed to run command.")
+       }
+       return message, nil
+}
+
+// reportError formats an HTTP error response and also logs the detailed error message.
+func reportError(w http.ResponseWriter, r *http.Request, err error, message string) {
+       m := response{
+               Message: message,
+       }
+       log.Printf("Error: %s\n%s", message, err.Error())
+       resp, err := json.Marshal(m)
+       if err != nil {
+               http.Error(w, "Failed to serialize a response", 500)
+               return
+       }
+       w.Write(resp)
+}
+
+// mainHandler handles the GET and POST of the main page.
+func mainHandler(w http.ResponseWriter, r *http.Request) {
+       if r.Method == "GET" {
+               w.Write(index)
+       } else if r.Method == "POST" {
+               w.Header().Set("Content-Type", "application/json")
+               b, err := ioutil.ReadAll(r.Body)
+               if err != nil {
+                       reportError(w, r, err, "Failed to read a request body.")
+                       return
+               }
+               hash, err := expandCode(LineNumbers(string(b)))
+               if err != nil {
+                       reportError(w, r, err, "Failed to write the code to compile.")
+                       return
+               }
+               message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), true)
+               if err != nil {
+                       reportError(w, r, err, "Failed to compile the code:\n"+message)
+                       return
+               }
+               linkMessage, err := doCmd(fmt.Sprintf(LINK, hash, hash), true)
+               if err != nil {
+                       reportError(w, r, err, "Failed to link the code:\n"+linkMessage)
+                       return
+               }
+               message += linkMessage
+               cmd := hash + " --out " + hash + ".png"
+               if *useChroot {
+                       cmd = "schroot -c webtry --directory=/inout -- /inout/" + cmd
+               } else {
+                       abs, err := filepath.Abs("../../../inout")
+                       if err != nil {
+                               reportError(w, r, err, "Failed to find executable directory.")
+                               return
+                       }
+                       cmd = abs + "/" + cmd
+               }
+
+               execMessage, err := doCmd(cmd, false)
+               if err != nil {
+                       reportError(w, r, err, "Failed to run the code:\n"+execMessage)
+                       return
+               }
+               png, err := ioutil.ReadFile("../../../inout/" + hash + ".png")
+               if err != nil {
+                       reportError(w, r, err, "Failed to open the generated PNG.")
+                       return
+               }
+
+               m := response{
+                       Message: message,
+                       Img:     base64.StdEncoding.EncodeToString([]byte(png)),
+               }
+               resp, err := json.Marshal(m)
+               if err != nil {
+                       reportError(w, r, err, "Failed to serialize a response.")
+                       return
+               }
+               w.Write(resp)
+       }
+}
+
+func main() {
+       flag.Parse()
+
+       http.HandleFunc("/", mainHandler)
+       log.Fatal(http.ListenAndServe(":8000", nil))
+}
index ba0b9263a185b12bd308a1a53a3f234b4fdec458..1f476f3d4675ce490bbf3dc9adb1474aeed76621 100644 (file)
@@ -4,13 +4,13 @@
     {
       'target_name': 'webtry',
       'type': 'executable',
-      'mac_bundle' : 1,
       'include_dirs' : [
         '../src/core',
         '../src/images',
         ],
        'sources': [
          '../experimental/webtry/result.cpp',
+         '../experimental/webtry/main.cpp',
        ],
        'dependencies': [
          'flags.gyp:flags',