diff options
author | commit-bot@chromium.org <commit-bot@chromium.org@2bbb7eff-a529-9590-31e7-b0007b416f81> | 2014-04-09 18:59:44 +0000 |
---|---|---|
committer | commit-bot@chromium.org <commit-bot@chromium.org@2bbb7eff-a529-9590-31e7-b0007b416f81> | 2014-04-09 18:59:44 +0000 |
commit | 6d036c2e47819ce91ba4f09edeeccead60af4320 (patch) | |
tree | 7139d1d308d9b5252754cd1bfa74aa22c8f6cfce /experimental | |
parent | 0257ebe4ac7907d0293371cc6a50a0ec0a55f61c (diff) |
Initial code for webtry, a web application for allowing users to try out Skia.
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
Diffstat (limited to 'experimental')
-rw-r--r-- | experimental/webtry/DESIGN | 121 | ||||
-rw-r--r-- | experimental/webtry/README | 68 | ||||
-rw-r--r-- | experimental/webtry/TODO | 11 | ||||
-rw-r--r-- | experimental/webtry/main.cpp | 114 | ||||
-rw-r--r-- | experimental/webtry/result.cpp | 28 | ||||
-rw-r--r-- | experimental/webtry/seccomp_bpf.h | 45 | ||||
-rw-r--r-- | experimental/webtry/server.py | 79 | ||||
-rwxr-xr-x | experimental/webtry/setup/continue_install | 56 | ||||
-rwxr-xr-x | experimental/webtry/setup/webtry_setup.sh | 32 | ||||
-rw-r--r-- | experimental/webtry/sys/webtry_init | 159 | ||||
-rw-r--r-- | experimental/webtry/sys/webtry_monit | 4 | ||||
-rw-r--r-- | experimental/webtry/sys/webtry_schroot | 9 | ||||
-rw-r--r-- | experimental/webtry/template.cpp | 23 | ||||
-rw-r--r-- | experimental/webtry/templates/index.html (renamed from experimental/webtry/index.html) | 17 | ||||
-rw-r--r-- | experimental/webtry/templates/template.cpp | 13 | ||||
-rw-r--r-- | experimental/webtry/webtry.go | 226 |
16 files changed, 890 insertions, 115 deletions
diff --git a/experimental/webtry/DESIGN b/experimental/webtry/DESIGN new file mode 100644 index 0000000000..ddc26f538c --- /dev/null +++ b/experimental/webtry/DESIGN @@ -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. + + diff --git a/experimental/webtry/README b/experimental/webtry/README index a36ac8a3fe..c7573110f6 100644 --- a/experimental/webtry/README +++ b/experimental/webtry/README @@ -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 index 0000000000..81382cda5d --- /dev/null +++ b/experimental/webtry/TODO @@ -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/main.cpp b/experimental/webtry/main.cpp new file mode 100644 index 0000000000..9e7df1484f --- /dev/null +++ b/experimental/webtry/main.cpp @@ -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 index 0000000000..d06ef9c3da --- /dev/null +++ b/experimental/webtry/result.cpp @@ -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 index 0000000000..8bbe99b7f8 --- /dev/null +++ b/experimental/webtry/seccomp_bpf.h @@ -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 index 8367b3e46a..0000000000 --- a/experimental/webtry/server.py +++ /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 index 0000000000..0db794b13d --- /dev/null +++ b/experimental/webtry/setup/continue_install @@ -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 index 0000000000..6658ca8c97 --- /dev/null +++ b/experimental/webtry/setup/webtry_setup.sh @@ -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 index 0000000000..359e19e46c --- /dev/null +++ b/experimental/webtry/sys/webtry_init @@ -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 index 0000000000..5fccf8a84e --- /dev/null +++ b/experimental/webtry/sys/webtry_monit @@ -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 index 0000000000..c2bdfd320d --- /dev/null +++ b/experimental/webtry/sys/webtry_schroot @@ -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 index 20b9086cde..0000000000 --- a/experimental/webtry/template.cpp +++ /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/index.html b/experimental/webtry/templates/index.html index b3903e675f..c630f14ef5 100644 --- a/experimental/webtry/index.html +++ b/experimental/webtry/templates/index.html @@ -13,6 +13,9 @@ padding: 0; color: green; } + #output { + color: #333; + } </style> </head> <body> @@ -27,11 +30,8 @@ int main() { SkGraphics::Init(); SkImageInfo info = SkImageInfo::MakeN32(300, 300, kPremul_SkAlphaType); - SkBitmap bitmap; - bitmap.setConfig(info); - bitmap.allocPixels(); - SkCanvas c(bitmap); - c.drawColor(SK_ColorWHITE); + SkAutoTUnref<SkSurface> surface(SkSurface::NewRaster(info)); + SkCanvas* canvas = surface->getCanvas(); <textarea name='code' id='code' rows='20' cols='80'>SkPaint p; p.setColor(SK_ColorRED); @@ -39,7 +39,7 @@ p.setAntiAlias(true); p.setStyle(SkPaint::kStroke_Style); p.setStrokeWidth(10); -c.drawLine(20, 20, 100, 100, p); +canvas->drawLine(20, 20, 100, 100, p); </textarea> if (!SkImageEncoder::EncodeFile("foo.png", bitmap, SkImageEncoder::kPNG_Type, 100)) { @@ -48,12 +48,13 @@ c.drawLine(20, 20, 100, 100, p); } </code></pre> + <input type='button' value='Run' id='run'> + <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'); @@ -90,7 +91,7 @@ c.drawLine(20, 20, 100, 100, p); req.addEventListener('error', codeError); req.overrideMimeType('application/json'); req.open('POST', '.', true); - req.send(code.value + '\r\nEOF\r\n'); + req.send(code.value); } </script> </body> diff --git a/experimental/webtry/templates/template.cpp b/experimental/webtry/templates/template.cpp new file mode 100644 index 0000000000..3543b0cb24 --- /dev/null +++ b/experimental/webtry/templates/template.cpp @@ -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 index 0000000000..d70102a1c8 --- /dev/null +++ b/experimental/webtry/webtry.go @@ -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)) +} |