diff --git a/.gitignore b/.gitignore index cfd03fe10..dfd56e7b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.tar.* .*.swp .*.tmp +nacl_sdk/ /crouton* /tests/run /releases/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..48c6cc6b0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +##For users who want to report an issue +Please read [this wiki page] +(https://github.com/dnschneid/crouton/wiki/Common-issues-and-reporting) +for instructions on reporting an issue. + +##For contributors +Please read [this +section](https://github.com/dnschneid/crouton#i-want-to-be-a-contributor) +of the README and the following relevant sections first. diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 38f76de20..7469fffab 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -12,9 +12,12 @@ John Tantalo Maurice van Kruchten Micah Lee Michael Orr +Mike Kasick +Mikito Takada Nevada Romsdahl Nicolas Boichat Stephen Barber Steven Maude Tobbe Lundberg +Tony Xue Yuri Pole diff --git a/Makefile b/Makefile index 1261d977c..7dab818c6 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,9 @@ SCRIPTS := \ $(wildcard installer/*/*) \ $(wildcard src/*) \ $(wildcard targets/*) +EXTPEXE = host-ext/crouton/kiwi.pexe +EXTPEXESOURCES = $(wildcard host-ext/nacl_src/*.h) \ + $(wildcard host-ext/nacl_src/*.cc) EXTSOURCES = $(wildcard host-ext/crouton/*) GENVERSION = build/genversion.sh CONTRIBUTORSSED = build/CONTRIBUTORS.sed @@ -22,10 +25,16 @@ RELEASE = build/release.sh VERSION = 1 TARPARAMS ?= -j +CFLAGS=-g -Wall -Werror -Os + croutoncursor_LIBS = -lX11 -lXfixes -lXrender +croutonfbserver_LIBS = -lX11 -lXdamage -lXext -lXfixes -lXtst croutonwmtools_LIBS = -lX11 croutonxi2event_LIBS = -lX11 -lXi +croutonwebsocket_DEPS = src/websocket.h +croutonfbserver_DEPS = src/websocket.h + ifeq ($(wildcard .git/HEAD),) GITHEAD := else @@ -50,8 +59,17 @@ $(TARGET): $(WRAPPER) $(SCRIPTS) $(GENVERSION) $(GITHEAD) Makefile $(EXTTARGET): $(EXTSOURCES) Makefile rm -f $(EXTTARGET) && zip -q --junk-paths $(EXTTARGET) $(EXTSOURCES) -$(SRCTARGETS): src/$(patsubst crouton%,src/%.c,$@) Makefile - gcc -g -Wall -Werror $(patsubst crouton%,src/%.c,$@) $($@_LIBS) -o $@ +$(EXTPEXE): $(EXTPEXESOURCES) + $(MAKE) -C host-ext/nacl_src + +$(SRCTARGETS): src/$(patsubst crouton%,src/%.c,$@) $($@_DEPS) Makefile + gcc $(CFLAGS) $(patsubst crouton%,src/%.c,$@) $($@_LIBS) -o $@ + +croutonfreon.so: src/freon.c Makefile + gcc $(CFLAGS) -shared -fPIC src/freon.c -ldl -o croutonfreon.so + +croutonxorg.so: src/xorg.c Makefile + gcc $(CFLAGS) -shared -fPIC src/xorg.c -ldl -o croutonxorg.so extension: $(EXTTARGET) diff --git a/README.md b/README.md index 292770f47..78151b12a 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,21 @@ to the rest of Chromium OS. Prerequisites ------------- You need a device running Chromium OS that has been switched to developer mode. + +For instructions on how to do that, go to [this Chromium OS wiki page] +(http://www.chromium.org/chromium-os/developer-information-for-chrome-os-devices), +click on your device model and follow the steps in the *Entering Developer Mode* +section. + Note that developer mode, in its default configuration, is *completely insecure*, so don't expect a password in your chroot to keep anyone from your data. crouton does support encrypting chroots, but the encryption is only as strong as the quality of your passphrase. Consider this your warning. +It's also highly recommended that you install the [crouton extension] +(https://goo.gl/OVQOEt), which, when combined with the `extension` or `xiwi` +targets, provides much improved integration with Chromium OS. + That's it! Surprised? @@ -99,7 +109,8 @@ Examples 6. Exit the chroot by logging out of Xfce. ### With encryption! - 1. Add the `-e` parameter when you run crouton to create an encrypted chroot. + 1. Add the `-e` parameter when you run crouton to create an encrypted chroot + or encrypt a non-encrypted chroot. 2. You can get some extra protection on your chroot by storing the decryption key separately from the place the chroot is stored. Use the `-k` parameter to specify a file or directory to store the keys in (such as a USB drive or @@ -111,6 +122,15 @@ Examples 2. Run `sh ~/Downloads/crouton -r list` to list the recognized releases and which distros they belong to. +### Wasteful rendundancies are wasteful: one clipboard, one browser, one window + 1. Install the [crouton extension](https://goo.gl/OVQOEt) into Chromium OS. + 2. Add the `extension` or `xiwi` version to your chroot. + 3. Try some copy-pasta, or uninstall all your web browsers from the chroot. + +*Installing the extension and its target gives you synchronized clipboards, the +option of using Chromium OS to handle URLs, and allows chroots to create +graphical sessions as Chromium OS windows.* + ### I don't always use Linux, but when I do, I use CLI 1. You can save a chunk of space by ditching X and just installing command-line tools using `-t core` or `-t cli-extra` @@ -118,6 +138,8 @@ Examples `sudo enter-chroot` 3. Use the [Crosh Window](https://goo.gl/eczLT) extension to keep Chromium OS from eating standard keyboard shortcuts. + 4. If you installed cli-extra, `startcli` will launch a new VT right into the + chroot. ### A new version of crouton came out; my chroot is therefore obsolete and sad 1. Check for updates, download the latest version, and see what's new by @@ -125,8 +147,6 @@ Examples to see what those parameters actually do). 2. Exit the chroot and run `sudo sh ~/Downloads/crouton -u -n chrootname`. It will update all installed targets. - 3. You can use this with `-e` to encrypt a non-encrypted chroot, but make sure - you don't interrupt the operation. ### A backup a day keeps the price-gouging data restoration services away 1. `sudo edit-chroot -b chrootname` backs up your chroot to a timestamped @@ -160,7 +180,8 @@ Examples 3. Include the `-r` parameter if you want to specify for which release to prepare a bootstrap. 4. You can then create chroots using the tarball by running - `sudo sh ~/Downloads/crouton -f ~/Downloads/mybootstrap.tar.bz2` + `sudo sh ~/Downloads/crouton -f ~/Downloads/mybootstrap.tar.bz2`. Make sure + you also specify the target environment with `-t`. *This is the quickest way to create multiple chroots at once, since you won't have to determine and download the bootstrap files every time.* @@ -184,7 +205,7 @@ Tips * Chroots are cheap! Create multiple ones using `-n`, break them, then make new, better ones! * You can change the distro mirror from the default by using `-m` - * Behind a proxy? `-P` lets you specify one. + * Want to use a proxy? `-P` lets you specify one (or disable it). * A script is installed in your chroot called `brightness`. You can assign this to keyboard shortcuts to adjust the brightness of the screen (e.g. `brightness up`) or keyboard (e.g. `brightness k down`). @@ -198,6 +219,8 @@ Tips `croutonpowerd -i command and arguments` will automatically stop inhibiting power management when the command exits. * Have a Pixel or two or 4.352 million? `-t touch` improves touch support. + * Want to share some files and/or folders between ChromeOS and your chroot? + Check out the `/etc/crouton/shares` file, or read all about it in the wiki. * Want more tips? Check the [wiki](https://github.com/dnschneid/crouton/wiki). @@ -207,13 +230,14 @@ Running another OS in a chroot is a pretty messy technique (although it's hidden behind very pretty scripts), and these scripts are relatively new, so problems are not surprising. Check the issue tracker and file a bug if your issue isn't there. When filing a new bug, include the output of `croutonversion` run from -inside the chroot (if possible). +inside the chroot or, if you cannot mount your chroot, include the output +of `cat /etc/lsb-release` from Crosh. I want to be a Contributor! --------------------------- That's great! But before your code can be merged, you'll need to have signed -the [Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual#sign-electronically). +the [Individual Contributor License Agreement](https://cla.developers.google.com/clas/new?kind=KIND_INDIVIDUAL&domain=DOMAIN_GOOGLE). Don't worry, it only takes a minute and you'll definitely get to keep your firstborn, probably. If you've already signed it for contributing to Chromium or Chromium OS, you're already done. @@ -243,7 +267,8 @@ But how? There's a way For Everyone to help! * Something broken? File a bug! Bonus points if you try to fix it. It helps if - you provide the output of `croutonversion` when you submit the bug. + you provide the output of `croutonversion` (or the output of + `cat /etc/lsb-release` from Crosh) when you submit the bug. * Want to try and break something? Look through [requests for testing](https://github.com/dnschneid/crouton/issues?labels=needstesting&state=open) and then do your best to brutally rip the author's work to shreds. * Look through [open issues](https://github.com/dnschneid/crouton/issues?state=open) diff --git a/build/wrapper.sh b/build/wrapper.sh index 502ad1bbe..ea7f11d5c 100644 --- a/build/wrapper.sh +++ b/build/wrapper.sh @@ -38,8 +38,8 @@ set -e VERSION='git' -# Minimum Chromium OS version is R31 stable -CROS_MIN_VERS=4731 +# Minimum Chromium OS version is R35 stable +CROS_MIN_VERS=5712 if [ "$1" = '-x' -a "$#" -le 2 ]; then # Extract to the specified directory. @@ -62,6 +62,33 @@ if [ -z "$TRAP" ]; then exit fi +# See if we want to just run a script from the bundle +if [ "$1" = '-X' ]; then + script="$SCRIPTDIR/$2" + if [ ! -f "$script" ]; then + cd "$SCRIPTDIR" + echo "USAGE: ${0##*/} -X DIR/SCRIPT [ARGS] +Runs a script directly from the bundle. Valid DIR/SCRIPT combos:" 1>&2 + ls chroot-bin/* host-bin/* 1>&2 + if [ -n "$2" ]; then + echo 1>&2 + echo "Invalid script '$2'" 1>&2 + fi + exit 2 + fi + shift 2 + # If this script was called with '-x' or '-v', pass that on + SETOPTIONS="-e" + if set -o | grep -q '^xtrace.*on$'; then + SETOPTIONS="$SETOPTIONS -x" + fi + if set -o | grep -q '^verbose.*on$'; then + SETOPTIONS="$SETOPTIONS -v" + fi + sh $SETOPTIONS "$script" "$@" + exit "$?" +fi + # Execute the main script inline. It will use SCRIPTDIR to find what it needs. . "$SCRIPTDIR/installer/main.sh" diff --git a/chroot-bin/crouton-noroot b/chroot-bin/crouton-noroot new file mode 100755 index 000000000..08b94fb0e --- /dev/null +++ b/chroot-bin/crouton-noroot @@ -0,0 +1,32 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Wrapper for scripts that shouldn't be launched as root. +# Symlink to launch the application with the same name in /usr/bin/ +# Launch directly with a parameter to launch that executable +# Launch directly with a blank or - first parameter and a word to just check and +# exit with an exit code, printing out the second parameter as the app name. + +APPLICATION="${0##*/}" +if [ "$APPLICATION" = 'crouton-noroot' ]; then + if [ -z "$1" -o "$1" = '-' -o "$1" = '--' ]; then + APPLICATION='' + else + APPLICATION="$1" + shift + fi +else + APPLICATION="/usr/bin/$APPLICATION" +fi + +if [ "$USER" = root -o "$UID" = 0 ]; then + app="${APPLICATION:-"$2"}" + echo "Do not launch ${app##*/} as root inside the chroot." 1>&2 + exit 2 +elif [ -z "$APPLICATION" ]; then + exit 0 +fi + +exec "$APPLICATION" "$@" diff --git a/chroot-bin/croutonclip b/chroot-bin/croutonclip index 4dfa38b32..310419ab1 100755 --- a/chroot-bin/croutonclip +++ b/chroot-bin/croutonclip @@ -10,36 +10,18 @@ VERBOSE='' . "`dirname "$0"`/../installer/functions" -PIPEDIR='/tmp/crouton-ext' -PIPEIN="$PIPEDIR/in" -PIPEOUT="$PIPEDIR/out" -PIPELOCK="$PIPEDIR/lock" - -# Write a command to croutonwebsocket, and read back response -websocketcommand() { - # Check that $PIPEDIR and the FIFO pipes exist - if ! [ -d "$PIPEDIR" -a -p "$PIPEIN" -a -p "$PIPEOUT" ]; then - echo "EError $PIPEIN or $PIPEOUT are not pipes." - exit 0 - fi - - ( - flock 5 - cat > "$PIPEIN" - cat "$PIPEOUT" - ) 5>"$PIPELOCK" -} - # rundisplay :X cmd ... -# Run a command on the specified display (uses host-x11 on :0) +# Run a command on the specified display rundisplay() { local disp="$1" shift - if [ "$disp" = ":0" ]; then - host-x11 "$@" - else + ( + # If display is :0, setup XAUTHORITY + if [ "$disp" = ":0" ]; then + eval "`host-x11`" 2>/dev/null + fi DISPLAY="$disp" "$@" - fi + ) } copyclip() { @@ -60,7 +42,7 @@ copyclip() { # Copy clipboard content from the current display { - if [ "$current" = 'aura' ]; then + if [ "$current" = 'cros' ]; then echo -n 'R' | websocketcommand else # Check if display is still running @@ -81,7 +63,7 @@ copyclip() { fi # Paste clipboard content to the next display - if [ "$next" = 'aura' ]; then + if [ "$next" = 'cros' ]; then STATUS="`(echo -n 'W'; cat) | websocketcommand`" if [ "$STATUS" != 'WOK' ]; then # Write failed, skip Chromium OS (do not update $current) @@ -138,19 +120,16 @@ waitwebsocket() { # Assume current display is Chromium OS: avoid race as we may not be able to # detect the first VT/window change. -current='aura' - -# Only let one instance *really* run at a time -PIDFILE='/tmp/crouton-lock/clip' +current='cros' -mkdir -m 775 -p /tmp/crouton-lock -exec 3>>"$PIDFILE" +mkdir -m 775 -p "$CROUTONLOCKDIR" +exec 3>>"$CROUTONLOCKDIR/clip" if ! flock -n 3; then echo "Another instance of croutonclip running, waiting..." flock 3 fi -addtrap "echo -n > '$PIDFILE' 2>/dev/null" +addtrap "echo -n > '$CROUTONLOCKDIR/clip' 2>/dev/null" ( # This subshell handles USR1 signals from croutoncycle. @@ -172,7 +151,7 @@ addtrap "echo -n > '$PIDFILE' 2>/dev/null" trap "echo 'USR1'" USR1 # Set the PID of this subshell after the trap is in place - sh -c 'echo -n "$PPID"' > "$PIDFILE" + sh -c 'echo -n "$PPID"' > "$CROUTONLOCKDIR/clip" # Force an update when started. echo "Force" diff --git a/chroot-bin/croutoncycle b/chroot-bin/croutoncycle index 7e6c6174a..50a0576bf 100755 --- a/chroot-bin/croutoncycle +++ b/chroot-bin/croutoncycle @@ -3,6 +3,8 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +. "`dirname "$0"`/../installer/functions" + USAGE="${0##*/} next|prev|cros|list|# Cycles through running graphical chroots. next: switch to the next display @@ -15,9 +17,20 @@ Cycles through running graphical chroots. # Undocumented: # display: return display associated with current window # (used from croutonclip): -# - aura: Chromium OS -# - 0: Chromium OS X11 display (non-aura window) -# - 1-9: chroot displays +# - cros: Chromium OS +# - :0: Chromium OS X11 display (non-aura window) +# - :1-9: chroot displays +# s: informs of a Chromium OS window change (called from extension): +# - cros: any Chromium OS window +# - :1-9: kiwi window: X11 display number +# force [command]: Force switching display, even if it does not appear +# to be necessary. + +force='' +if [ "${1#[Ff]}" != "$1" ]; then + force='y' + shift +fi case "$1" in [Ll]*) cmd='l';; @@ -25,9 +38,10 @@ case "$1" in [Cc]*) cmd='0';; [Pp]*) cmd='p';; [Nn]*) cmd='n';; -:*) cmd=":$((${1#:}))";; +[Ss]*) cmd='s' disp="${1#s}";; +:*) cmd="${1%%.*}"; cmd=":$((${cmd#:}))";; [0-9]*) cmd="$(($1))";; -*) echo "$USAGE" 1>&2; exit 2;; +*) error 2 "$USAGE";; esac # Returns the chroot name of an X11 display specified in $1 on stdout @@ -43,44 +57,83 @@ getname() { } # Only let one instance run at a time to avoid nasty race conditions -mkdir -m 775 -p /tmp/crouton-lock -exec 3>/tmp/crouton-lock/cycle +mkdir -m 775 -p "$CROUTONLOCKDIR" +exec 3>"$CROUTONLOCKDIR/cycle" flock 3 +# set display command from extension +if [ "$cmd" = 's' ]; then + echo "$disp" > "$CRIATDISPLAY" + if [ -s "$CROUTONLOCKDIR/clip" ]; then + kill -USR1 "`cat "$CROUTONLOCKDIR/clip"`" || true + fi + exit 0 +fi + # Ensure environment sanity export XAUTHORITY='' +# Set to y if there is any xiwi instance running +xiwiactive='' + # Prepare display list for easier looping displist='' for disp in /tmp/.X*-lock; do disp="${disp#*X}" disp=":${disp%-lock}" - # Only add VT-based chroots here + # Only add VT-based and xiwi-based chroots here (that excludes Xephyr) if [ "$disp" = ':0' ]; then continue elif DISPLAY="$disp" xprop -root 'XFree86_VT' 2>/dev/null \ | grep -q 'INTEGER'; then displist="$displist $disp" + elif DISPLAY="$disp" xprop -root 'CROUTON_XMETHOD' 2>/dev/null \ + | grep -q '= "xiwi'; then + displist="$displist $disp" + xiwiactive='y' fi done -# List windows on :0. Includes aura -winlist="`host-x11 croutonwmtools list nim | sort | awk '{ printf $NF " " }'`" + +# Set to the freon display owner if freon is used +freonowner='' +if [ ! -f "/sys/class/tty/tty0/active" ]; then + if [ -f "/tmp/crouton-lock/display" ]; then + read -r freonowner < "/tmp/crouton-lock/display" + fi + freonowner="${freonowner:-0}" + winlist="aura*" + aurawin="aura" + tty='' +else + # List windows on :0. Includes aura + winlist="`host-x11 croutonwmtools list nim | \ + sort | awk '{ printf $NF " " }'`" + aurawin="`host-x11 croutonwmtools list ni | \ + awk '$1 == "aura_root_0" { print $NF; exit }'`" + tty="`cat '/sys/class/tty/tty0/active'`" +fi # Combine the two fulllist="$winlist$displist" fulllist="${fulllist% }" -# Figure out the current display number -tty="`cat '/sys/class/tty/tty0/active'`" -if [ "$tty" = 'tty1' ]; then - # Either in Chromium OS, xephyr chroot, or window. Active window is starred. +if [ "$freonowner" = 0 -o "$tty" = 'tty1' ]; then + # Either in Chromium OS, xephyr/xiwi chroot, or window. + # Active window is starred. for disp in $winlist; do if [ "${disp%"*"}" != "$disp" ]; then curdisp="$disp" + if [ -n "$xiwiactive" -a "${disp%"*"}" = "$aurawin" -a \ + -s "$CRIATDISPLAY" ]; then + kiwidisp="`cat $CRIATDISPLAY`" + if [ "${kiwidisp#:[0-9]}" != "$kiwidisp" ]; then + curdisp="$kiwidisp" + fi + fi break fi done -else +elif [ -z "$freonowner" ]; then # Poll the displays to figure out which one owns this VT curdisp="$tty" for disp in $displist; do @@ -90,6 +143,14 @@ else break fi done +else + # Match the pid to the current freon owner + for lockfile in /tmp/.X*-lock; do + if grep -q "\\<$freonowner$" "$lockfile"; then + curdisp="${lockfile#*X}" + curdisp=":${curdisp%%-*}" + fi + done fi # List the displays if requested @@ -99,8 +160,15 @@ if [ "$cmd" = 'l' -o "$cmd" = 'd' ]; then chromiumos="`awk -F= '/_RELEASE_NAME=/{print $2}' \ '/var/host/lsb-release'`" fi - host-x11 croutonwmtools list nim | sort | while read -r line; do + ( + if [ -z "$freonowner" ]; then + host-x11 croutonwmtools list nim + else + echo "aura_root_0 aura*" + fi + ) | sort | while read -r line; do disp="${line##* }" + display="${disp%"*"}" line="${line% *}" number='0' active=' ' @@ -110,6 +178,7 @@ if [ "$cmd" = 'l' -o "$cmd" = 'd' ]; then if [ "${number#[0-9]}" = "$number" ]; then number='0' else + display=":$number" line="`getname "$number"`" fi fi @@ -117,7 +186,7 @@ if [ "$cmd" = 'l' -o "$cmd" = 'd' ]; then active='*' if [ "$cmd" = 'd' ]; then if [ "$line" = 'aura_root_0' ]; then - echo 'aura' + echo 'cros' else echo ":$number" fi @@ -127,13 +196,14 @@ if [ "$cmd" = 'l' -o "$cmd" = 'd' ]; then fi if [ "$line" = 'aura_root_0' ]; then line="$chromiumos" + display="cros" + window='' fi if [ "$cmd" = 'l' ]; then - echo "$number$active $line" + echo "$display$active $line" fi done for disp in $displist; do - number="${disp#:}" active=' ' if [ "$disp" = "$curdisp" ]; then active='*' @@ -144,7 +214,7 @@ if [ "$cmd" = 'l' -o "$cmd" = 'd' ]; then fi if [ "$cmd" = 'l' ]; then - echo -n "$number$active " + echo -n "$disp$active " getname "$disp" fi done @@ -155,6 +225,12 @@ fi if [ -n "${cmd#[pn]}" ]; then if [ "${cmd#:}" != "$cmd" ]; then destdisp="$cmd" + # Resolve a xephyr display into its ID + if DISPLAY="$destdisp" xprop -root 'CROUTON_XMETHOD' 2>/dev/null \ + | grep -q '= "xephyr"$'; then + destdisp="`host-x11 croutonwmtools list ni | \ + awk "/^Xephyr on $destdisp\.0/"' { print $NF; exit }'`" + fi else i=0 destdisp='' @@ -166,8 +242,7 @@ if [ -n "${cmd#[pn]}" ]; then i="$((i+1))" done if [ -z "$destdisp" ]; then - echo "Display number out of range." 1>&2 - exit 2 + error 2 "Display number out of range." fi fi elif [ "$cmd" = 'p' ]; then @@ -192,17 +267,16 @@ elif [ "$cmd" = 'n' ]; then destdisp="${fulllist%% *}" fi else - echo "Bad command $cmd." 1>&2 - exit 3 + error 3 "Bad command $cmd." fi # No-op on no-op -if [ "$destdisp" = "$curdisp" ]; then +if [ "$destdisp" = "$curdisp" -a -z "$force" ]; then exit 0 fi # Make sure tap-to-click is enabled -if hash xinput 2>/dev/null; then +if [ -z "$freonowner" ] && hash xinput 2>/dev/null; then for id in `host-x11 xinput --list --id-only`; do host-x11 xinput set-prop "$id" 'Tap Paused' 0 2>/dev/null || true done @@ -210,26 +284,67 @@ fi # Determine if the target display is on a VT if [ "${destdisp#:}" = "$destdisp" ]; then - eval "`host-x11`" - # Raise the right window after chvting, so that it can update - if [ "$tty" != 'tty1' ]; then - sudo -n chvt 1 - sleep .1 + if [ -z "$freonowner" ]; then + eval "`host-x11`" + # Raise the right window after chvting, so that it can update + if [ "$tty" != 'tty1' ]; then + sudo -n chvt 1 + sleep .1 + fi + croutonwmtools raise "${destdisp%"*"}" + elif [ "${freonowner:-0}" != 0 ]; then + kill -USR1 "$freonowner" + fi + + if [ -n "$xiwiactive" -a "${destdisp%"*"}" = "$aurawin" ]; then + STATUS="`echo -n "Xcros" | websocketcommand`" + if [ "$STATUS" != 'XOK' ]; then + error 1 "${STATUS#?}" + fi fi - croutonwmtools raise "${destdisp%"*"}" else export DISPLAY="$destdisp" - dest="`xprop -root 'XFree86_VT' 2>/dev/null`" - dest="${dest##* }" - if [ "${dest#[1-9]}" = "$dest" ]; then - dest='1' + xmethod="`xprop -root 'CROUTON_XMETHOD' 2>/dev/null \ + | sed -n 's/^.*\"\(.*\)\"/\1/p'`" + if [ "${xmethod%%-*}" = 'xiwi' ]; then + if [ -z "$freonowner" -a "$tty" != 'tty1' ]; then + sudo -n chvt 1 + sleep .1 + elif [ "${freonowner:-0}" != 0 ]; then + kill -USR1 "$freonowner" + fi + if [ -z "$freonowner" ]; then + host-x11 croutonwmtools raise "$aurawin" + fi + STATUS="`echo -n "X${destdisp} ${xmethod#*-}" | websocketcommand`" + if [ "$STATUS" != 'XOK' ]; then + error 1 "${STATUS#?}" + fi + elif [ -z "$freonowner" ]; then + dest="`xprop -root 'XFree86_VT' 2>/dev/null`" + dest="${dest##* }" + if [ "${dest#[1-9]}" = "$dest" ]; then + dest='1' + fi + # When the destination we are changing to is using fbdev driver in X, we + # need first a change to vt 2, else only the session switches and the + # display will be stuck on the old vt. + sudo -n chvt 2 + sudo -n chvt "$dest" + else + dest="/tmp/.X${destdisp#:}-lock" + if [ -f "$dest" ]; then + # Trigger the target before releasing the current owner + kill -USR1 `cat /tmp/.X${destdisp#:}-lock` + fi + if [ "${freonowner:-0}" != 0 ]; then + kill -USR1 "$freonowner" + fi fi - sudo -n chvt "$dest" fi -CROUTONPIDFILE='/tmp/crouton-lock/clip' -if [ -s "$CROUTONPIDFILE" ]; then - kill -USR1 "`cat "$CROUTONPIDFILE"`" || true +if [ -s "$CROUTONLOCKDIR/clip" ]; then + kill -USR1 "`cat "$CROUTONLOCKDIR/clip"`" || true fi # Wait a flip and then refresh the display for good measure diff --git a/chroot-bin/croutonfindnacl b/chroot-bin/croutonfindnacl new file mode 100755 index 000000000..80d54ea24 --- /dev/null +++ b/chroot-bin/croutonfindnacl @@ -0,0 +1,79 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# croutonfindnacl address signature ["pids"] +# +# This script is used by croutonfbserver to find the nacl_helper process it is +# connected to, and, in particular, the file descriptor corresponding to the shm +# memory that the nacl_helper process shares with Chromium. +# +# - address: NaCl-space address of the shared memory (hexadecimal). We assume +# that the NaCl/hardware memory mapping conserves the address, +# possibly with a prefix in the MSBs. +# - signature: random 8 byte pattern (hexadecimal, machine byte order) that is +# written at the beginning of the shared buffer by the NaCl +# application. The first 8 bytes of each candidate buffer is read, +# guaranteeing that the correct buffer is returned. +# - pids: (normally ununsed, defaults to all processes named "nacl_helper") +# Space-separated list of PIDs to scan for. +# +# On success, prints "pid:filename" and exits with code 0. +# On error (shm not found, invalid parameters), exits with code >0. + +set -e + +VERBOSE= + +if [ "$#" -lt 2 -o "$#" -gt 3 ]; then + echo "Invalid parameters" + exit 2 +fi + +ADDRESS="$1" +PATTERN="$2" +PIDS="${3:-"`pgrep nacl_helper`"}" + +MATCH="" + +# Iterate over all NaCl helper processes +for pid in $PIDS; do + [ -n "$VERBOSE" ] && echo "pid:$pid" 1>&2 + # Find candidate mappings + file="`awk '$1 ~ /^[0-9a-f]*'"$ADDRESS"'-/ && $2 == "rw-s" \ + && $6 ~ /\/shm\/\.(com\.google\.Chrome|org\.chromium\.Chromium)/ \ + { print $6 } + ' "/proc/$pid/maps"`" + [ -n "$VERBOSE" ] && echo "file:$file" 1>&2 + if [ -z "$file" ]; then + continue + fi + + # Iterate over mappings, and check signature + for fd in "/proc/$pid/fd"/*; do + link="`readlink -- "$fd"`" + link="${link% (deleted)}" + if [ "$link" = "$file" ]; then + # Check if signature matches + pattern="`od -An -t x1 -N8 "$fd" | tr -d ' '`" + [ -n "$VERBOSE" ] && echo "FD:$fd ($pattern)" 1>&2 + if [ "$pattern" = "$PATTERN" ]; then + # Second match? This should never happen + if [ -n "$MATCH" ]; then + echo -n "-1:ambiguous" + exit 1 + fi + MATCH="$pid:$fd" + fi + fi + done +done + +if [ -n "$MATCH" ]; then + echo -n "$MATCH" + exit 0 +else + echo -n "-1:no match" + exit 1 +fi diff --git a/chroot-bin/croutonnotify b/chroot-bin/croutonnotify new file mode 100755 index 000000000..f86d31bcd --- /dev/null +++ b/chroot-bin/croutonnotify @@ -0,0 +1,104 @@ +#!/bin/sh -e +# Copyright (c) 2015 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +USAGE="${0##*/} -j +${0##*/} -t title [-m message] [-d] [-i icon] [-I id] +Raises a notification in Chromium OS (requires crouton extension). + +When -j is specified, reads JSON data from stdin, in the format required +by chrome.notifications API, with 2 additional fields, \"crouton_id\" and +\"crouton_display\", corresponding respectively to the notification id, and the +display to switch to when the notification is clicked (croutoncycle parameter). + +Otherwise, constructs a \"basic\" notification with the requested fields. + +Options: + -j Read JSON data from stdin (see example below) + -t Title to display (chrome.notifications field: \"title\") + -m Message to display (chrome.notifications field: \"message\") + -d Switch to display in environment variable DISPLAY ($DISPLAY) when + the notification is clicked. Default: Do not switch display. + -i Small icon to display on the left of the notification + (chrome.notifications field: \"iconUrl\") + -I ID to pass to chrome.notifications.create: only the last + notification with a given ID is shown. + Default: Display a new notification. + +Example JSON data: +"'{ + "type": "basic", + "title": "Primary Title", + "message": "Primary message to display", + "crouton_display": ":1", + "iconUrl": "data:image/png;base64," +}' + +. "$(dirname "$0")/../installer/functions" + +SWITCHDISPLAY="" +MESSAGE="" +TITLE="" +ID="" +ICON="" +JSON="" + +# Process arguments +while getopts 'di:I:jm:t:' f; do + case "$f" in + d) SWITCHDISPLAY="$DISPLAY";; + i) ICON="$OPTARG";; + I) ID="$OPTARG";; + j) JSON="y";; + m) MESSAGE="$OPTARG";; + t) TITLE="$OPTARG";; + \?) error 2 "$USAGE";; + esac +done + +# No extra parameters, -j precludes other parameters, and at least title +# must be specified (or -j) +if [ "$#" != "$((OPTIND-1))" ] || + [ "$JSON" = 'y' -a -n "$SWITCHDISPLAY$ICON$ID$MESSAGE$TITLE" ] || + [ -z "$JSON$TITLE" ]; then + error 2 "$USAGE" +fi + +if [ -n "$ICON" ] && [ ! -r "$ICON" -o ! -f "$ICON" ]; then + error 2 "Cannot open $ICON." +fi + +# Escape json string (backslash and double quotes) +json_escape() { + echo -n "$1" | sed -e 's/\\/\\u005C/g;s/"/\\u0022/g;2~1s/^/\\u000A/;' \ + | tr -d '\n' +} + +STATUS="$({ + echo -n "N" + if [ -z "$JSON" ]; then + echo -n '{ + "type": "basic", + "title": "'"$(json_escape "$TITLE")"'", + "message": "'"$(json_escape "$MESSAGE")"'", + "crouton_display": "'"$(json_escape "$SWITCHDISPLAY")"'", + "crouton_id": "'"$(json_escape "$ID")"'"' + if [ -n "$ICON" ]; then + ext="${ICON##*.}" + if grep -Iq '>"$CROUTONLOCKDIR/xbindkeys" +if ! flock -n 3; then + echo "Another instance of ${0##*/} running, waiting..." + flock 3 +fi + +# Reset event variables to handle strange environments +unset `set | grep -o '^event[0-9]*'` 2>/dev/null || true + +# Poll for new event files and dump the output +while :; do + # Clean up old hexdumps and start new ones + for event in `set | grep -o '^event[0-9]*'` /dev/input/event*; do + # Check if the event file is already monitored + eval "pid=\"\${${event##*/}:-0}\"" + if [ "$pid" != 0 ]; then + # Check if it's still running + if kill -0 "$pid" 2>/dev/null; then + continue + fi + wait "$pid" || true + fi + # Clean up old variables + if [ "${event#/}" = "$event" ]; then + unset "$event" + else + # Read in the event files and split into input_event fields + stdbuf -oL hexdump -e "$HEXDUMP_FMT" "$event" & + eval "${event##*/}='$!'" + fi + done + # Avoid picking up the event variable + unset event + # Kill all event daemons + pids="`set | sed -n 's/^event[0-9]*=.\(.*\).$/\1/p' | tr '\n' ' '`" + settrap "kill $pids 2>/dev/null;" + # Wait for next poll + sleep "$EVENT_DEV_POLL" +done | unbuffered_awk " + function update() { + c = lc || rc; s = ls || rs; a = la || ra + if (!cmd && c && s && a && p) { + cmd = \"p\" + } else if (!cmd && c && s && a && n) { + cmd = \"n\" + } else if (cmd && !c && !s && !a && !p && !n) { + system(\"/usr/local/bin/croutoncycle \" cmd) + cmd = \"\" + } + } + $TYPE == $TYPE_EV_KEY && $KEY == $KEY_LEFTCTRL { lc = $STATE; update() } + $TYPE == $TYPE_EV_KEY && $KEY == $KEY_LEFTSHIFT { ls = $STATE; update() } + $TYPE == $TYPE_EV_KEY && $KEY == $KEY_LEFTALT { la = $STATE; update() } + $TYPE == $TYPE_EV_KEY && $KEY == $KEY_RIGHTCTRL { rc = $STATE; update() } + $TYPE == $TYPE_EV_KEY && $KEY == $KEY_RIGHTSHIFT { rs = $STATE; update() } + $TYPE == $TYPE_EV_KEY && $KEY == $KEY_RIGHTALT { ra = $STATE; update() } + $TYPE == $TYPE_EV_KEY && $KEY == $KEY_F1 { p = $STATE; update() } + $TYPE == $TYPE_EV_KEY && $KEY == $KEY_F2 { n = $STATE; update() } +" diff --git a/chroot-bin/croutonurlhandler b/chroot-bin/croutonurlhandler index b86d5ae59..d28e63362 100755 --- a/chroot-bin/croutonurlhandler +++ b/chroot-bin/croutonurlhandler @@ -7,25 +7,7 @@ USAGE="${0##*/} [-n] URL Open an URL in Chromium OS (requires crouton extension). Switches back to Chromium OS unless -n is specified." -PIPEDIR='/tmp/crouton-ext' -PIPEIN="$PIPEDIR/in" -PIPEOUT="$PIPEDIR/out" -PIPELOCK="$PIPEDIR/lock" - -# Write a command to croutonwebsocket, and read back response -websocketcommand() { - # Check that $PIPEDIR and the FIFO pipes exist - if ! [ -d "$PIPEDIR" -a -p "$PIPEIN" -a -p "$PIPEOUT" ]; then - echo "EError $PIPEIN or $PIPEOUT are not pipes." - exit 0 - fi - - ( - flock 5 - cat > "$PIPEIN" - cat "$PIPEOUT" - ) 5>"$PIPELOCK" -} +. "`dirname "$0"`/../installer/functions" noswitch='' if [ "$1" = '-n' ]; then @@ -34,14 +16,13 @@ if [ "$1" = '-n' ]; then fi if [ -z "$*" ]; then - echo "$USAGE" 1>&2; exit 2; + error 2 "$USAGE" fi STATUS="`echo -n U"$*" | websocketcommand`" if [ ! "$STATUS" = 'UOK' ]; then - echo "${STATUS#?}" 1>&2 - exit 1 + error 1 "${STATUS#?}" fi if [ -z "$noswitch" ]; then diff --git a/chroot-bin/croutonversion b/chroot-bin/croutonversion index 5289f83c8..08ce30a70 100755 --- a/chroot-bin/croutonversion +++ b/chroot-bin/croutonversion @@ -72,6 +72,10 @@ if [ -z "$CHANGES$DOWNLOAD$UPDATES" ]; then echo "crouton: version $VERSION" echo "release: $RELEASE" echo "architecture: $ARCH" + xmethodfile='/etc/crouton/xmethod' + if [ -r "$xmethodfile" ]; then + echo "xmethod: `cat "$xmethodfile"`" + fi targetfile='/etc/crouton/targets' if [ -r "$targetfile" ]; then echo "targets: `sed 's/^,//' "$targetfile"`" @@ -81,6 +85,12 @@ if [ -z "$CHANGES$DOWNLOAD$UPDATES" ]; then host="`awk -F= '/_RELEASE_DESCRIPTION=/{print $2}' "$hostrel"`" fi echo "host: version ${host:-unknown}" + echo "kernel: $(uname -a)" + freon="yes" + if [ -f /sys/class/tty/tty0/active ]; then + freon="no" + fi + echo "freon: $freon" exit 0 fi diff --git a/chroot-bin/croutonwheel b/chroot-bin/croutonwheel index 2513fb523..dc492baa3 100755 --- a/chroot-bin/croutonwheel +++ b/chroot-bin/croutonwheel @@ -50,8 +50,8 @@ fi eval "`host-x11`" # Only let one instance *really* run at a time -mkdir -m 775 -p /tmp/crouton-lock -exec 3>/tmp/crouton-lock/wheel +mkdir -m 775 -p "$CROUTONLOCKDIR" +exec 3>"$CROUTONLOCKDIR/wheel" flock 3 # Monitor xinput2 events, reacting only to scroll events (axes 2 and 3). diff --git a/chroot-bin/croutonxinitrc-wrapper b/chroot-bin/croutonxinitrc-wrapper index 928b3d1bd..c69942b8d 100755 --- a/chroot-bin/croutonxinitrc-wrapper +++ b/chroot-bin/croutonxinitrc-wrapper @@ -11,6 +11,7 @@ cmd='' extraargs='' binary='' +ret=0 # This part is a translation of what is found in xorg's xinit.c @@ -43,11 +44,29 @@ fi # Run crouton-specific commands: +# Show chroot specifics for troubleshooting +croutonversion 1>&2 + +if [ -z "$XMETHOD" ]; then + if [ -f '/etc/crouton/xmethod' ]; then + read -r XMETHOD _ < /etc/crouton/xmethod + export XMETHOD + else + echo 'X11 backend not set.' 1>&2 + exit 1 + fi +fi +xmethodtype="${XMETHOD%%-*}" +xmethodargs="${XMETHOD#*-}" + # Record the name of the chroot in the root window properties if [ -f '/etc/crouton/name' ] && hash xprop 2>/dev/null; then xprop -root -f CROUTON_NAME 8s -set CROUTON_NAME "`cat '/etc/crouton/name'`" fi +# Record the crouton XMETHOD in the root window properties +xprop -root -f CROUTON_XMETHOD 8s -set CROUTON_XMETHOD "$XMETHOD" + # Launch the powerd poker daemon croutonpowerd --daemon & @@ -56,29 +75,8 @@ if hash croutonclip 2>/dev/null; then croutonclip & fi -# Apply the Chromebook keyboard map if installed. -if [ -f '/usr/share/X11/xkb/compat/chromebook' ]; then - setxkbmap -model chromebook -fi - -# Launch key binding daemon -xmethod="`readlink -f '/etc/X11/xinit/xserverrc'`" -xmethod="${xmethod##*-}" - -XMETHOD="$xmethod" xbindkeys -fg /etc/crouton/xbindkeysrc.scm - -# Launch xbindkeys for the Chromium OS X server if it isn't running -mkdir -m 775 -p /tmp/crouton-lock -{ - # Only let one instance *really* run at a time - flock 3 - XMETHOD='' host-x11 xbindkeys -n -fg /etc/crouton/xbindkeysrc.scm & - trap "kill '$!' 2>/dev/null" HUP INT TERM - wait "$!" || true -} 3>/tmp/crouton-lock/xbindkeys & - # Pass through the host cursor and correct mousewheels on xephyr -if [ "$xmethod" = 'xephyr' ]; then +if [ "$xmethodtype" = 'xephyr' ]; then host-x11 croutoncursor "$DISPLAY" & if [ -z "$CROUTON_WHEEL_PARAMS" -a -r "$HOME/.croutonwheel" ]; then CROUTON_WHEEL_PARAMS="`head -n1 "$HOME/.croutonwheel"`" @@ -86,62 +84,99 @@ if [ "$xmethod" = 'xephyr' ]; then croutonwheel $CROUTON_WHEEL_PARAMS & fi -# Launch touchegg if it is requested. -toucheggconf='/etc/touchegg.conf' -if [ -f "$toucheggconf" ]; then - mkdir -p "$HOME/.config/touchegg" - ln -sf "$toucheggconf" "$HOME/.config/touchegg/" - touchegg 2>/dev/null & -fi +# Launch system-wide trigger daemon +croutontriggerd & + +# Input-related stuff is not needed for kiwi +if [ "$xmethodtype" != "xiwi" ]; then + # Apply the Chromebook keyboard map if installed. + if [ -f '/usr/share/X11/xkb/compat/chromebook' ]; then + setxkbmap -model chromebook + fi + + # Launch X-server-local key binding daemon + xbindkeys -fg /etc/crouton/xbindkeysrc.scm -# Configure trackpad settings if needed -if synclient >/dev/null 2>&1; then - case "`awk -F= '/_RELEASE_BOARD=/{print $2}' '/var/host/lsb-release'`" in - butterfly*|falco*) - SYNCLIENT="FingerLow=1 FingerHigh=5 $SYNCLIENT";; - parrot*|peppy*|wolf*) - SYNCLIENT="FingerLow=5 FingerHigh=10 $SYNCLIENT";; - esac - if [ -n "$SYNCLIENT" ]; then - synclient $SYNCLIENT + # Launch touchegg if it is requested. + toucheggconf='/etc/touchegg.conf' + if [ -f "$toucheggconf" ]; then + mkdir -p "$HOME/.config/touchegg" + ln -sf "$toucheggconf" "$HOME/.config/touchegg/" + touchegg 2>/dev/null & + fi + + # Configure trackpad settings if needed + if synclient >/dev/null 2>&1; then + case "`awk -F= '/_RELEASE_BOARD=/{print $2}' '/var/host/lsb-release'`" in + butterfly*|falco*) + SYNCLIENT="FingerLow=1 FingerHigh=5 $SYNCLIENT";; + parrot*|peppy*|wolf*) + SYNCLIENT="FingerLow=5 FingerHigh=10 $SYNCLIENT";; + esac + if [ -n "$SYNCLIENT" ]; then + synclient $SYNCLIENT + fi + fi + + # Make sure tap-to-click is enabled + if hash xinput 2>/dev/null; then + for id in `host-x11 xinput --list --id-only`; do + host-x11 xinput set-prop "$id" 'Tap Paused' 0 2>/dev/null || true + done fi fi -# Make sure tap-to-click is enabled -if hash xinput 2>/dev/null; then - for id in `host-x11 xinput --list --id-only`; do - host-x11 xinput set-prop "$id" 'Tap Paused' 0 2>/dev/null || true +# Crouton-in-a-tab: Start fbserver and launch display +if [ "$xmethodtype" = 'xiwi' ]; then + # The extension sends evdev key codes: fix the keyboard mapping rules + setxkbmap -rules evdev + # Reapply xkb map: This fixes autorepeat mask in "xset q" + xkbcomp "$DISPLAY" - | xkbcomp - "$DISPLAY" 2>/dev/null + + # Set resolution to a default 1024x768, this is important so that the DPI + # looks reasonable when the WM/DE start. + setres 1024 768 > /dev/null + croutonfbserver "$DISPLAY" & + + try=1 + while ! croutoncycle force "$DISPLAY"; do + echo "Cannot connect to extension, retrying..." + if [ "$try" -ge 10 ]; then + echo "\ +Unable to start display, make sure the crouton extension is installed +and enabled, and up to date. (download from http://goo.gl/OVQOEt)" 1>&2 + ret=1 + break + fi + sleep 1 + try="$((try+1))" done + if [ "$ret" -eq 0 ]; then + echo "Connected to extension, launched crouton in a window." 1>&2 + fi fi -# Shell is the leader of a process group, so signals sent to this process are -# propagated to its children. We ignore signals in this process, but the child -# handles them and exits. We use a no-op handler, as "" causes the signal to be -# ignored in children as well (see NOTES in "man 2 sigaction" for details). -# This process then runs exit commands, and terminates. -trap "true" HUP INT TERM +# Only run if no error occured before (e.g. cannot connect to extension) +if [ "$ret" -eq 0 ]; then + # Shell is the leader of a process group, so signals sent to this process + # are propagated to its children. We ignore signals in this process, but the + # child handles them and exits. We use a no-op handler, as "" causes the + # signal to be ignored in children as well (see NOTES in "man 2 sigaction" + # for details). This process then runs exit commands, and terminates. + trap "true" HUP INT TERM + + # Run the client itself if it is executable, otherwise run it in a shell. + if [ -n "$binary" -o -x "$cmd" ]; then + "$cmd" $extraargs "$@" || ret=$? + else + /bin/sh "$cmd" $extraargs "$@" || ret=$? + fi -# Run the client itself if it is executable, otherwise run it in a shell. -ret=0 -if [ -n "$binary" -o -x "$cmd" ]; then - "$cmd" $extraargs "$@" || ret=$? -else - /bin/sh "$cmd" $extraargs "$@" || ret=$? + trap - HUP INT TERM fi -trap - HUP INT TERM - # Run crouton-specific commands before the server exits: echo "Running exit commands..." 1>&2 -# Restore framebuffer compression if there are no other non-Chromium X servers -fbc='/sys/module/i915/parameters/i915_enable_fbc' -if [ -w "$fbc" ]; then - # There is at least 2 servers running (the current one and Chromium OS) - if [ "`ps -CX -CXorg -CXephyr -opid= | wc -l`" -le 2 ]; then - echo 1 > "$fbc" - fi -fi - exit "$ret" diff --git a/chroot-bin/host-x11 b/chroot-bin/host-x11 index e2e640e95..4e2977af3 100755 --- a/chroot-bin/host-x11 +++ b/chroot-bin/host-x11 @@ -6,9 +6,20 @@ # Either runs the specified command with the environment set to use the host's # X11 server, or prints out the environment changes required. -export DISPLAY=':0' XAUTHORITY='/var/host/Xauthority' -if [ "$#" = 0 ]; then - echo "export DISPLAY='$DISPLAY' XAUTHORITY='$XAUTHORITY'" +# If tty0 does not exit, no Chromium OS X11 server exist +if [ ! -f "/sys/class/tty/tty0/active" ]; then + err="No Chromium OS X server is available." + if [ "$#" = 0 ]; then + echo "echo '$err' 1>&2" + else + echo "$err" 1>&2 + exit 1 + fi else - exec "$@" + export DISPLAY=':0' XAUTHORITY='/var/host/Xauthority' + if [ "$#" = 0 ]; then + echo "export DISPLAY='$DISPLAY' XAUTHORITY='$XAUTHORITY'" + else + exec "$@" + fi fi diff --git a/chroot-bin/setres b/chroot-bin/setres index 642442094..ce100ee07 100755 --- a/chroot-bin/setres +++ b/chroot-bin/setres @@ -3,6 +3,16 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +# Changes the resolution of the current display. +# If XMETHOD is xiwi, tries to create a new exact resolution, and change mode +# to that. If that fails (e.g. non-patched xorg-dummy), take the closest, +# smaller, available resolution in xrandr, and if no smaller resolution is +# available, pick the closest one. +# If XMETHOD is anything else, set a resolution from cvt output. +# In all cases, outputs the applied resolution. + +set -e + if [ "$#" -lt 2 -o "$#" -gt 4 ]; then echo "USAGE: ${0##*/} x y [r [output]]" 1>&2 exit 2 @@ -14,12 +24,67 @@ o="${4}" if [ -z "$o" ]; then o="`xrandr -q | awk 'x{print $1;exit}/^Screen 0/{x=1}'`" fi -cvt "$x" "$y" "$r" | { - read -r _ - read -r _ mode data - mode="${mode#\"}" - mode="${mode%\"}" - xrandr --newmode "$mode" $data 2>/dev/null || true - xrandr --addmode "$o" "$mode" - xrandr --output "$o" --mode "$mode" + +xmethod="`xprop -root 'CROUTON_XMETHOD' | sed -n 's/^.*\"\(.*\)\"/\1/p'`" + +if [ "${xmethod%%-*}" != "xiwi" ]; then + cvt "$x" "$y" "$r" | { + read -r _ + read -r _ mode data + mode="${mode#\"}" + mode="${mode%\"}" + xrandr --newmode "$mode" $data 2>/dev/null || true + xrandr --addmode "$o" "$mode" + xrandr --output "$o" --mode "$mode" + echo "$mode" + } + exit 0 +fi + +# Replace mode $2 in output $1, with new data $3..$# +# Deletes the mode if $3 is not provided +replacemode() { + local o="$1" + local mode="$2" + shift 2 + xrandr --delmode "$o" "$mode" 2>/dev/null || true + xrandr --rmmode "$mode" 2>/dev/null || true + if [ "$#" -gt 0 ]; then + xrandr --newmode "$mode" "$@" + xrandr --addmode "$o" "$mode" + fi } + +# Try to change to arbitrary resolution +mhz="$((r*x*y/1000000))" +name="kiwi_${x}x${y}_${r}" + +# Try to switch mode, if it already exists. +if xrandr --output "$o" --mode "$name" 2>/dev/null; then + echo "${x}x${y}_${r}" + exit 0 +fi + +# Add the new mode +xrandr --newmode "$name" $mhz $x $x $x $x $y $y $y $y +xrandr --addmode "$o" "$name" + +# The next line fails on non-patched xorg-dummy +if xrandr --output "$o" --mode "$name"; then + # Success: remove old modes + others="`xrandr | sed -n 's/^.*\(kiwi[0-9x_]*\)[^*]*$/\1/p'`" + for othername in $others; do + xrandr --delmode "$o" "$othername" 2>/dev/null || true + xrandr --rmmode "$othername" 2>/dev/null || true + done + echo "${x}x${y}_${r}" + exit 0 +else + # Delete the new mode + xrandr --delmode "$o" "$name" 2>/dev/null || true + xrandr --rmmode "$name" 2>/dev/null || true +fi + +# Probably xorg-dummy got overwritten. Recommend an update. +echo "Failed to set custom resolution. Update your chroot and try again." 1>&2 +exit 1 diff --git a/chroot-bin/startgnome b/chroot-bin/startgnome index 26f65cb45..3b628d8e8 100755 --- a/chroot-bin/startgnome +++ b/chroot-bin/startgnome @@ -5,4 +5,4 @@ # Launches GNOME; automatically falls back to gnome-panel -exec gnome-session-wrapper gnome +exec crouton-noroot gnome-session-wrapper gnome diff --git a/chroot-bin/startkde b/chroot-bin/startkde new file mode 120000 index 000000000..95e64e85c --- /dev/null +++ b/chroot-bin/startkde @@ -0,0 +1 @@ +crouton-noroot \ No newline at end of file diff --git a/chroot-bin/startlxde b/chroot-bin/startlxde new file mode 120000 index 000000000..95e64e85c --- /dev/null +++ b/chroot-bin/startlxde @@ -0,0 +1 @@ +crouton-noroot \ No newline at end of file diff --git a/chroot-bin/startunity b/chroot-bin/startunity index 33571888e..c127f0737 100755 --- a/chroot-bin/startunity +++ b/chroot-bin/startunity @@ -9,4 +9,4 @@ export UBUNTU_MENUPROXY=1 export GTK_MODULES="unity-gtk-module" -exec gnome-session-wrapper ubuntu +exec crouton-noroot gnome-session-wrapper ubuntu diff --git a/chroot-bin/startxfce4 b/chroot-bin/startxfce4 new file mode 120000 index 000000000..95e64e85c --- /dev/null +++ b/chroot-bin/startxfce4 @@ -0,0 +1 @@ +crouton-noroot \ No newline at end of file diff --git a/chroot-bin/xinit b/chroot-bin/xinit index 3116ab072..8140fdb79 100755 --- a/chroot-bin/xinit +++ b/chroot-bin/xinit @@ -22,9 +22,13 @@ for arg in "$@"; do fi done -disp=0 +# Never use display :0 (confusing if aura does not use X11) +disp=1 while [ -f "/tmp/.X$disp-lock" ]; do disp=$((disp+1)) done +# If possible, switch to VT1 to avoid strangeness when launching from VT2 +chvt 1 2>/dev/null || true + exec /usr/bin/xinit /usr/local/bin/croutonxinitrc-wrapper "$@" $dash $xserverrc ":$disp" diff --git a/chroot-bin/xiwi b/chroot-bin/xiwi new file mode 100755 index 000000000..ac812ad36 --- /dev/null +++ b/chroot-bin/xiwi @@ -0,0 +1,119 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Runs the specified X11 application in its own X server in Chromium OS. + +USAGE="Usage: ${0##*/} [-f] [-F|-T] APPLICATION [PARAMETERS ...] +Launches a windowed session in Chromium OS for any graphical application. +All parameters are passed to the specified application. + +By default, the app is launched in a window. Passing -F will launch the app +full-screen, and passing -T will launch the app in a tab. + +xiwi will normally close when the application returns. Some gui applications +fork before or during normal operation, which can confuse xiwi and cause it to +quit prematurely. If your application does not have a parameter that prevents +it from forking, and crouton is unable to automatically detect the fork, you can +use -f to prevent xiwi from quitting automatically. +xiwi will quit if you close the Chromium OS window when nothing is displayed. + +A default window manager will full-screen all windows, unless APPLICATION begins +with 'start' or is 'xinit'. You can cycle through multiple windows inside the +application via Ctrl-Alt-Tab/Ctrl-Alt-Shift-Tab, or close them via +Ctrl-Alt-Shift-Escape. If APPLICATION begins with 'start' but you still want to +use the default window manager, specify the full path of the application." + +. "`dirname "$0"`/../installer/functions" +xiwicmd="`readlink -f "$0"`" +OPTSTRING='FfTt' + +if [ "$#" = 0 ]; then + error 2 "$USAGE" +elif [ "$1" = '/' ]; then + shift 1 + foreground='' + while getopts "$OPTSTRING" f; do + case "$f" in + f) foreground='y';; + t|T|F) :;; + \?) error 2 "$USAGE";; + esac + done + shift "$((OPTIND-1))" + xsetroot -cursor_name left_ptr + if [ "$1" != 'xinit' -a "${1#start}" = "$1" ]; then + i3 -c "/etc/crouton/xiwi.conf" & + # Wait for i3 to launch + xprop -spy -root | grep -q _NET_ACTIVE_WINDOW + # Launch the window title monitoring daemon + # _NET_ACTIVE_WINDOW is more reliable than _NET_CLIENT_LIST_STACKING for + # keeping track of the topmost window. + xprop -spy -notype -root 0i ' $0\n' '_NET_ACTIVE_WINDOW' 2>/dev/null | { + name="`cat /etc/crouton/name`" + monpid='' + monwid='' + while read _ wid; do + if [ "$wid" = "$monwid" ]; then + continue + fi + if [ -n "$monpid" ]; then + kill "$monpid" 2>/dev/null + fi + monwid="$wid" + (xprop -spy -notype -id "$wid" 'WM_NAME' 2>/dev/null || echo) \ + | while read _ title; do + title="${title%\"}" + xprop -root -f CROUTON_NAME 8s -set CROUTON_NAME \ + "$name/$1${title:+": "}${title#*\"}" + { + echo -n 'C' + croutoncycle l + } | websocketcommand >/dev/null + done & + monpid="$!" + done + if [ -n "$monpid" ]; then + kill "$monpid" 2>/dev/null + fi + } & + # Launch user init scripts + if [ -f "$HOME/.xiwirc" ]; then + /bin/sh "$HOME/.xiwirc" || true + fi + fi + starttime="$(date +%s)" + "$@" + endtime="$(date +%s)" + if [ -n "$foreground" -o "$(($endtime-$starttime))" -le 2 ]; then + xprop -spy -notype -root 0i ' $0\n' 'CROUTON_CONNECTED' \ + | while read _ connected; do + if [ "$connected" != 0 ]; then + continue + fi + # _NET_CLIENT_LIST_STACKING is more reliable than + # _NET_ACTIVE_WINDOW for detecting when no windows exist + if ! xprop -notype -root '_NET_CLIENT_LIST_STACKING' \ + | grep -q '0x'; then + kill "$$" + break + fi + done + fi +else + export XMETHOD='xiwi-window' + while getopts "$OPTSTRING" f; do + case "$f" in + f) :;; + F) export XMETHOD='xiwi-fullscreen';; + t|T) export XMETHOD='xiwi-tab';; + \?) error 2 "$USAGE";; + esac + done + eval "exe=\"\$$OPTIND\"" + if ! hash "$exe" 2>/dev/null; then + error 2 "${0##*/}: $exe: not found" + fi + exec /usr/local/bin/xinit "$xiwicmd" / "$@" +fi diff --git a/chroot-etc/xbindkeysrc.scm b/chroot-etc/xbindkeysrc.scm index ad06b55f1..1fa69ff4b 100644 --- a/chroot-etc/xbindkeysrc.scm +++ b/chroot-etc/xbindkeysrc.scm @@ -4,9 +4,15 @@ ;; Run xbindkeys -dg for some example configuration file with explanation -; Cycle chroots -(xbindkey '(control shift alt F1) "xte 'keyup F1'; croutoncycle prev") -(xbindkey '(control shift alt F2) "xte 'keyup F2'; croutoncycle next") +; Cycle chroots. On most systems, this is handled by the triggerhappy daemon. +; On freon, we have to do it ourselves since we currently grab the event device. +(if (access? "/sys/class/tty/tty0/active" F_OK) (begin + (xbindkey '(control shift alt F1) "") + (xbindkey '(control shift alt F2) "") +) (begin + (xbindkey '(control shift alt F1) "xte 'keyup F1'; croutoncycle prev") + (xbindkey '(control shift alt F2) "xte 'keyup F2'; croutoncycle next") +)) ; Extra bindings that must only be activated in chroot X11/Xephyr (if (not (string-null? (getenv "XMETHOD"))) diff --git a/chroot-etc/xbmc-cycle.py b/chroot-etc/xbmc-cycle.py new file mode 100644 index 000000000..914b76c36 --- /dev/null +++ b/chroot-etc/xbmc-cycle.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +# +# Python script to call croutoncycle. This is needed to let the +# hotkeys ctr-shift-alt F1/F2 work when xbmc is in fullscreen. +import subprocess +import sys + +if len(sys.argv) == 2 and sys.argv[1] in ("prev", "next"): + exitcode = subprocess.call(["/usr/local/bin/croutoncycle", sys.argv[1]]) +else: + sys.stderr.write("Usage: %s prev|next\n" % str(sys.argv[0])) + exitcode = 2 +sys.exit(exitcode) diff --git a/chroot-etc/xbmc-keyboard.xml b/chroot-etc/xbmc-keyboard.xml new file mode 100644 index 000000000..95ad439b7 --- /dev/null +++ b/chroot-etc/xbmc-keyboard.xml @@ -0,0 +1,13 @@ + + + + + + + + + RunScript(/etc/crouton/xbmc-cycle.py,prev) + RunScript(/etc/crouton/xbmc-cycle.py,next) + + + diff --git a/chroot-etc/xiwi.conf b/chroot-etc/xiwi.conf new file mode 100644 index 000000000..6adba9040 --- /dev/null +++ b/chroot-etc/xiwi.conf @@ -0,0 +1,27 @@ +# i3 config file (v4) +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Style +new_window none +new_float normal +workspace_layout tabbed +font pango:Sans 8 + +# Colors border backgr. text indicator +client.focused #8E8E8F #EAEAEB #000000 #8E8E8F +client.focused_inactive #8E8E8F #CACACB #525252 #8E8E8F +client.unfocused #8E8E8F #CACACB #525252 #8E8E8F +client.urgent #FF8E8E #FFCACB #520000 #FF8E8E +client.background #C3C3C4 + +# Interaction +focus_follows_mouse no +bindsym Mod1+Shift+Control+Escape kill +floating_modifier Mod1 +bindsym Mod1+Tab focus right +bindsym Mod1+Shift+Tab focus left +bindsym Mod1+Control+Tab focus right +bindsym Mod1+Shift+Control+Tab focus left +bindsym --release button2 kill diff --git a/chroot-etc/xorg-dummy.conf b/chroot-etc/xorg-dummy.conf new file mode 100644 index 000000000..83faefc5b --- /dev/null +++ b/chroot-etc/xorg-dummy.conf @@ -0,0 +1,41 @@ +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +Section "Monitor" + Identifier "Monitor0" + HorizSync 5.0-1000.0 + VertRefresh 5.0-200.0 +EndSection + +Section "Device" + Identifier "Card0" + Driver "dummy" + # Enough memory for 4096x2048 + VideoRam 32768 +EndSection + +Section "Screen" + DefaultDepth 24 + Identifier "Screen0" + Device "Card0" + Monitor "Monitor0" + SubSection "Display" + Depth 24 + Modes "1024x768" + EndSubSection +EndSection + +Section "ServerLayout" + Identifier "Layout0" + Screen "Screen0" +EndSection + +Section "ServerFlags" + Option "AutoAddDevices" "false" + Option "AutoAddGPU" "false" + Option "DontVTSwitch" "true" + Option "AllowMouseOpenFail" "true" + Option "PciForceNone" "true" + Option "AutoEnableDevices" "false" +EndSection diff --git a/chroot-etc/xorg-intel-sna.conf b/chroot-etc/xorg-intel-sna.conf new file mode 100644 index 000000000..3f2b369b9 --- /dev/null +++ b/chroot-etc/xorg-intel-sna.conf @@ -0,0 +1,12 @@ +# Copyright (c) 2015 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# On Intel platforms with FBC enabled, in order to see anything we need to use +# the SNA driver with the TearFree option. +Section "Device" + Identifier "Intel Graphics SNA+TearFree" + Driver "intel" + Option "AccelMethod" "sna" + Option "TearFree" "true" +EndSection diff --git a/chroot-etc/xserverrc b/chroot-etc/xserverrc new file mode 100644 index 000000000..eb3e1eed5 --- /dev/null +++ b/chroot-etc/xserverrc @@ -0,0 +1,21 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$XMETHOD" ]; then + if [ -f '/etc/crouton/xmethod' ]; then + read -r XMETHOD _ < /etc/crouton/xmethod + else + echo 'X11 backend not set.' 1>&2 + exit 1 + fi +fi + +xserverrc="/etc/crouton/xserverrc-${XMETHOD%%-*}" +if [ "${XMETHOD##*/}" != "$XMETHOD" -o ! -f "$xserverrc" ]; then + echo "Invalid X11 backend '$XMETHOD'" 1>&2 + exit 2 +fi + +. "$xserverrc" diff --git a/chroot-etc/xserverrc-local.example b/chroot-etc/xserverrc-local.example index 0872b6a1e..558b68be6 100644 --- a/chroot-etc/xserverrc-local.example +++ b/chroot-etc/xserverrc-local.example @@ -7,7 +7,7 @@ # /etc/crouton/xserverrc-local # # The file is sourced before invoking the X server with the variable -# XMETHOD set to x11 or xephyr and the variable XARGS containing the +# XMETHOD set to xephyr, xiwi, or xorg and the variable XARGS containing the # command line arguments that will be passed to the server. # # Uncoment if fonts look too big on machines with 1366x768 11.6" screen diff --git a/chroot-etc/xserverrc-x11 b/chroot-etc/xserverrc-x11 deleted file mode 100644 index 9f7f84f5f..000000000 --- a/chroot-etc/xserverrc-x11 +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -e -# Copyright (c) 2014 The crouton Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -# Disable framebuffer compression when displaying natively. -# TODO: test FBC with SNA enabled on Haswell to regain power savings -fbc='/sys/module/i915/parameters/i915_enable_fbc' -if [ -w "$fbc" ]; then - echo 0 > "$fbc" -fi - -XMETHOD='x11' -XARGS='-nolisten tcp' -if [ -f /etc/crouton/xserverrc-local ]; then - . /etc/crouton/xserverrc-local -fi - -exec /usr/bin/X $XARGS "$@" diff --git a/chroot-etc/xserverrc-xephyr b/chroot-etc/xserverrc-xephyr index 63f791217..b613fff9a 100644 --- a/chroot-etc/xserverrc-xephyr +++ b/chroot-etc/xserverrc-xephyr @@ -5,7 +5,7 @@ export DISPLAY=':0' XAUTHORITY='/var/host/Xauthority' -XMETHOD='xephyr' +export XMETHOD='xephyr' XARGS='-fullscreen -host-cursor -nolisten tcp' if [ -f /etc/crouton/xserverrc-local ]; then . /etc/crouton/xserverrc-local diff --git a/chroot-etc/xserverrc-xiwi b/chroot-etc/xserverrc-xiwi new file mode 100644 index 000000000..8932d9192 --- /dev/null +++ b/chroot-etc/xserverrc-xiwi @@ -0,0 +1,22 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +logfile="/tmp/Xorg.crouton.$$.log" +for arg in "$@"; do + disp="`echo "$arg" | sed -n 's/^\:\([0-9]*\)$/\1/p'`" + if [ -n "$disp" ]; then + logfile="/tmp/Xorg.crouton.$disp.log" + fi +done + +if [ "${XMETHOD%%-*}" != 'xiwi' ]; then + export XMETHOD='xiwi' +fi +XARGS="-nolisten tcp -config xorg-dummy.conf -logfile $logfile" +if [ -f /etc/crouton/xserverrc-local ]; then + . /etc/crouton/xserverrc-local +fi + +exec /usr/bin/Xorg $XARGS "$@" diff --git a/chroot-etc/xserverrc-xorg b/chroot-etc/xserverrc-xorg new file mode 100644 index 000000000..c0fc926ae --- /dev/null +++ b/chroot-etc/xserverrc-xorg @@ -0,0 +1,46 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ "${XMETHOD%%-*}" != 'xorg' ]; then + export XMETHOD='xorg' +fi +XARGS='-nolisten tcp' +if [ -f /etc/crouton/xserverrc-local ]; then + . /etc/crouton/xserverrc-local +fi + +X=/usr/bin/X + +# Handle Freon systems +if [ ! -f "/sys/class/tty/tty0/active" ]; then + # We won't be able to launch properly if running from frecon + ppid="$$" + while [ -n "$ppid" -a "$ppid" -ne 1 ]; do + ppid="`ps -p "$ppid" -o 'ppid=' 2>/dev/null | sed 's/ //g'`" + if ps -p "$ppid" -o 'comm=' | grep -q '^frecon$'; then + echo 'Xorg X11 servers cannot be launched from Frecon.' 1>&2 + echo 'Return to Chromium OS and use crosh to launch X.' 1>&2 + exit 2 + fi + done + # Use the daemon to kill frecon. + # FIXME: remove when crbug.com/457075 is resolved + if [ -w '/tmp/crouton-lock/frecon' ]; then + echo >> '/tmp/crouton-lock/frecon' + fi + # Freon necessitates the preload hack for X to coexist + X=/usr/bin/Xorg + logfile="/tmp/Xorg.crouton.$$.log" + for arg in "$@"; do + disp="`echo "$arg" | sed -n 's/^\:\([0-9]*\)$/\1/p'`" + if [ -n "$disp" ]; then + logfile="/tmp/Xorg.crouton.$disp.log" + fi + done + XARGS="-logfile $logfile" + export LD_PRELOAD="/usr/local/lib/croutonfreon.so:$LD_PRELOAD" +fi + +exec "$X" $XARGS "$@" diff --git a/host-bin/edit-chroot b/host-bin/edit-chroot index effadd406..d449242b4 100755 --- a/host-bin/edit-chroot +++ b/host-bin/edit-chroot @@ -10,12 +10,16 @@ ALLCHROOTS='' BACKUP='' BINDIR="`dirname "\`readlink -f "$0"\`"`" CHROOTS="`readlink -m "$BINDIR/../chroots"`" +CHROOTSSET='' +MOUNTPOINT='/var/crouton' DELETE='' ENCRYPT='' KEYFILE='' LISTDETAILS='' MOVE='' +NEEDS_UNMOUNT='' RESTORE='' +SPLIT='' TARBALL='' YES='' YESPARAM='' @@ -53,38 +57,46 @@ Options: directory, or an absolute path to move it entirely. DEST can be a directory, in which case it must end in a slash. If multiple chroots are specified, DEST must be a directory. + If you are moving a chroot to a SD card/USB drive, make sure the + storage is formatted to ext2/3/4. -r Restores a chroot from a tarball. The tarball path can be specified with -f or detected from name. If both are specified, restores to that name instead of the one in the tarball. Will not overwrite a chroot when restoring unless -r is specified twice. + -s SPLIT Force a backup archive to be split into SPLIT-sized chunks. + SPLIT is specified in megabytes (1048576 bytes), and cannot be + smaller than 10. + FAT32 filesystems are split by default to fit within 4GB. -y Do all actions without confirmation." # Common functions . "$BINDIR/../installer/functions" # Process arguments -while getopts 'abc:def:k:lm:ry' f; do - case "$f" in +getopts_string='abc:def:k:lm:rs:y' +while getopts_nextarg; do + case "$getopts_var" in a) ALLCHROOTS='y';; - b) BACKUP='y';; - c) CHROOTS="`readlink -m "$OPTARG"`";; - d) DELETE='y';; - e) ENCRYPT='y';; - f) TARBALL="$OPTARG";; - k) KEYFILE="$OPTARG";; + b) BACKUP='y'; NEEDS_UNMOUNT='y';; + c) CHROOTS="`readlink -m -- "$getopts_arg"`"; CHROOTSSET='y';; + d) DELETE='y'; NEEDS_UNMOUNT='y';; + e) ENCRYPT='y'; NEEDS_UNMOUNT='y';; + f) TARBALL="$getopts_arg";; + k) KEYFILE="$getopts_arg";; l) LISTDETAILS=$(($LISTDETAILS+1));; - m) MOVE="$OPTARG";; - r) RESTORE=$(($RESTORE+1));; + m) MOVE="$getopts_arg"; NEEDS_UNMOUNT='y';; + r) RESTORE=$(($RESTORE+1)); NEEDS_UNMOUNT='y';; + s) SPLIT="$getopts_arg";; y) YES='a'; YESPARAM='-y';; \?) error 2 "$USAGE";; esac done -shift "$((OPTIND-1))" # If the executable name is delete*, assume DELETE. if [ ! "${APPLICATION#delete}" = "$APPLICATION" ]; then DELETE='y' + NEEDS_UNMOUNT='y' fi # At least one command must be specified @@ -117,6 +129,11 @@ if [ -n "$DELETE" -a -n "$BACKUP$ENCRYPT$KEYFILE$MOVE$RESTORE" ]; then error 2 "$USAGE" fi +# Make sure SPLIT is reasonable +if [ -n "$SPLIT" ] && [ "$SPLIT" -lt 10 -o -z "$BACKUP" ]; then + error 2 "$USAGE" +fi + # If we specified -a option, bring in all chroots. if [ -n "$ALLCHROOTS" ]; then if [ ! -d "$CHROOTS" ]; then @@ -132,6 +149,12 @@ if [ -n "$ALLCHROOTS" ]; then fi fi +# If TARBALL is unspecified and we're in /, put the tarball in ~/Downloads +if [ -n "$BACKUP$RESTORE" -a -z "$TARBALL" -a "$PWD" = '/' \ + -a -d '/home/chronos/user/Downloads' ]; then + TARBALL="/home/chronos/user/Downloads/" +fi + # If multiple chroots are listed, KEYFILE and MOVE must be empty or directories. if [ $# -gt 1 -a -f "$KEYFILE" -a ! "$KEYFILE" = '-' ]; then error 2 "Multiple chroots specified, but $KEYFILE is not a directory." @@ -141,11 +164,48 @@ elif [ $# -gt 1 -a -f "$TARBALL" ]; then error 2 "Multiple chroots specified, but $TARBALL is not a directory." fi +# Don't allow moving to non-ext filesystems (but don't check if just renaming) +if [ -n "$MOVE" -a "${MOVE#*/}" != "$MOVE" ] && \ + df -T "`getmountpoint "$MOVE"`" | awk '$2~"^ext"{exit 1}'; then + error 2 "Chroots can only be moved to ext filesystems." +fi + +# Don't allow restoring to non-ext filesystems +if [ -n "$RESTORE" ] && \ + df -T "`getmountpoint "$CHROOTS"`" | awk '$2~"^ext"{exit 1}'; then + error 2 "Chroots can only be restored to ext filesystems." +fi + +# Don't allow backing up to tmpfs filesystems +if [ -n "$BACKUP" ] && ! df -T "`getmountpoint "${TARBALL:-.}"`" \ + | awk '$2=="tmpfs"{exit 1}'; then + error 2 "Chroots cannot be backed up to temporary filesystems." +fi + # We need to run as root if [ ! "$USER" = root -a ! "$UID" = 0 ]; then error 2 "$APPLICATION must be run as root." fi +# Strongly advise against moving a keyfile to a tmpfs path +if [ "${KEYFILE:--}" != '-' ] && \ + ! df -T "`getmountpoint "$KEYFILE"`" | awk '$2=="tmpfs"{exit 1}'; then + echo -n '\ +Moving a keyfile to a temporary filesystem is a really good way to permanently +lose access to your chroot. If you still want to do this, wait for 15 seconds. +Otherwise: HIT CTRL-C RIGHT NOW > ' 1>&2 + sleep 15 + echo \ +'...okay. Be sure to put the keyfile somewhere safe before you reboot.' 1>&2 +fi + +# Try to mount the crouton partition if it exists, CHROOTS has not been set +# manually, and this script base directory is /usr/local/bin. +if [ -z "$CHROOTSSET" -a "$BINDIR" = "/usr/local/bin" ] && \ + mountcrouton "$MOUNTPOINT"; then + CHROOTS="$MOUNTPOINT/chroots" +fi + # If we're restoring and specified a tarball and no name, detect the name. if [ -n "$RESTORE" -a -n "$TARBALL" -a $# = 0 ]; then echo 'Detecting chroot name...' 1>&2 @@ -201,28 +261,12 @@ if [ -n "$TARBALL" ] && \ mkdir -p "$TARBALL" fi -# If TARBALL is unspecified and we're in /, put the tarball in ~/Downloads -if [ -n "$BACKUP$RESTORE" -a -z "$TARBALL" -a "$PWD" = '/' \ - -a -d '/home/chronos/user/Downloads' ]; then - TARBALL="/home/chronos/user/Downloads/" -fi - # Avoid kernel panics due to slow I/O disablehungtask # Make sure we always exit with echo on the tty. addtrap "stty echo 2>/dev/null" -# Returns the mountpoint a path is on. The path doesn't need to exist. -# $1: the path to check -# outputs on stdout -getmountpoint() { - mp="`readlink -m "$1"`" - while ! stat -c '%m' "$mp" 2>/dev/null; do - mp="${mp%/*}" - done -} - # Prints out a fancy spinner that updates every time a line is fed in, unless # the output is not to a tty, in which case it just prints a new line. # $1: number of lines between each update of the spinner @@ -290,8 +334,11 @@ for NAME in "$@"; do error 2 "$CHROOT already exists! Specify a second -r to overwrite it (dangerous)." elif [ -n "$RESTORE" ]; then EXISTS='y' - else - sh -e "$BINDIR/unmount-chroot" $YESPARAM -c "$CHROOTS" -- "$NAME" + elif ! sh -e "$BINDIR/unmount-chroot" $YESPARAM \ + -c "$CHROOTS" -- "$NAME"; then + if [ -n "$NEEDS_UNMOUNT" ]; then + exit 1 + fi fi elif [ -n "$RESTORE" ]; then EXISTS='' @@ -333,15 +380,35 @@ for NAME in "$@"; do dest="$dest.gz" fi fi + # If we're writing to a fat32 filesystem, split the file at 4GB chunks + if ! df -T "`getmountpoint "$dest"`" | awk '$2~"^v?fat"{exit 1}'; then + SPLIT="${SPLIT:-4095}" + fi + if [ -n "$SPLIT" ]; then + tardest="`mktemp -d --tmpdir=/tmp 'crouton-backup.XXX'`" + addtrap "rm -rf '$tardest'" + tardest="$tardest/pipe.${dest##*/}" + mkfifo -m 600 "$tardest" + split -b "${SPLIT}m" -a 4 "$tardest" "$dest.part-" & + splitpid="$!" + else + tardest="$dest" + splitpid='' + fi echo -n " Backing up $CHROOT to $dest..." 1>&2 - addtrap "echo 'Deleting partial archive.' 1>&2; rm -f '$dest'" + addtrap "echo 'Deleting partial archive.' 1>&2; \ + kill '$splitpid' 2>/dev/null; rm -f '$dest' '$dest.part-'*" ret=0 spinner 1 tar --checkpoint=100 --checkpoint-action=exec=echo \ --one-file-system -V "crouton:backup.${date%-*}${date#*-}-$NAME" \ - -caf "$dest" -C "$CHROOTS" "$NAME" || ret=$? - if [ "$ret" != 0 ]; then + -caf "$tardest" -C "$CHROOTS" "$NAME" || ret="$?" + if [ -n "$SPLIT" ]; then + wait "$splitpid" || ret="$?" + mv -f "$dest.part-aaaa" "$dest" || ret="$?" + fi + if [ "$ret" -ne 0 ]; then echo "Unable to backup $CHROOT." 1>&2 - exit $ret + exit "$ret" fi # Make sure filesystem is sync'ed sync @@ -358,7 +425,8 @@ for NAME in "$@"; do # Search for the alphabetically last tarball with src. # Dated tarballs take precedence over undated tarballs. for file in "$file."* "$file-"*; do - if [ ! -f "$file" ]; then + if [ "${file%.part-[a-z][a-z][a-z][a-z]}" != "$file" \ + -o ! -f "$file" ]; then continue fi # Confirm it's a tarball @@ -383,8 +451,34 @@ for NAME in "$@"; do fi echo -n " Restoring $src to $CHROOT..." 1>&2 mkdir -p "$CHROOT" - spinner 1 tar --checkpoint=200 --checkpoint-action=exec=echo \ - --one-file-system -xaf "$src" -C "$CHROOT" --strip-components=1 + if [ -f "$src.part-aaab" ]; then + # Detect the type of compression before sending it through a fifo + for tarparam in -z -j -J -Z --no-auto-compress fail; do + if [ "$tarparam" = 'fail' ]; then + error 2 "Unable to detect compression method of $src" + elif tar $tarparam --test-label -f "$src" >/dev/null 2>&1; then + break + fi + done + # Don't let tar get tripped up by cat's incomplete writes + tarparam="$tarparam -B" + tarsrc="`mktemp -d --tmpdir=/tmp 'crouton-restore.XXX'`" + addtrap "rm -rf '$tarsrc'" + tarsrc="$tarsrc/pipe" + mkfifo -m 600 "$tarsrc" + cat "$src" "$src.part"* >> "$tarsrc" & + catpid="$!" + addtrap "kill '$catpid' 2>/dev/null" + else + tarsrc="$src" + tarparam='-a' + catpid='' + fi + spinner 1 tar --checkpoint=200 --checkpoint-action=exec=echo $tarparam \ + --one-file-system -xf "$tarsrc" -C "$CHROOT" --strip-components=1 + if [ -n "$catpid" ]; then + wait "$catpid" + fi # Make sure filesystem is sync'ed sync echo "Finished restoring $src to $CHROOT" 1>&2 @@ -410,8 +504,8 @@ for NAME in "$@"; do if [ -d "$newkeyfile" -o ! "${newkeyfile%/}" = "$newkeyfile" ]; then newkeyfile="${newkeyfile%/}/$NAME" fi - oldkeyfile="`readlink -m "$oldkeyfile"`" - keyfilecanon="`readlink -m "$newkeyfile"`" + oldkeyfile="`readlink -m -- "$oldkeyfile"`" + keyfilecanon="`readlink -m -- "$newkeyfile"`" if [ ! -f "$oldkeyfile" ]; then # If there is no old keyfile, make sure we've requested encryption. if [ -z "$ENCRYPT" ]; then @@ -475,7 +569,7 @@ for NAME in "$@"; do # safety. We don't do this when encrypting a chroot (see mount-chroot), # because that would require 2x the space on one device. When switching # filesystems like this, however, that isn't a concern. - if [ ! "`getmountpoint "$target"`" = "`getmountpoint "$CHROOT"`" ]; then + if [ "`getmountpoint "$target"`" != "`getmountpoint "$CHROOT"`" ]; then echo "Moving $CHROOT across filesystems to $target" 1>&2 echo 'This will take a while.' 1>&2 echo "If the operation gets interrupted, you can safely delete $target" 1>&2 diff --git a/host-bin/enter-chroot b/host-bin/enter-chroot index c270c982d..88b548292 100755 --- a/host-bin/enter-chroot +++ b/host-bin/enter-chroot @@ -9,11 +9,14 @@ APPLICATION="${0##*/}" BACKGROUND='' BINDIR="`dirname "\`readlink -f "$0"\`"`" CHROOTS="`readlink -m "$BINDIR/../chroots"`" +CHROOTSSET='' +MOUNTPOINT='/var/crouton' KEYFILE='' LOGIN='' NAME='' TARGET='' USERNAME='1000' +TMPXMETHOD='' NOLOGIN='' SETUPSCRIPT='/prepare.sh' @@ -33,6 +36,7 @@ Options: -n NAME Name of the chroot to enter. Default: first one found in CHROOTS -t TARGET Only enter the chroot if it contains the specified TARGET. -u USERNAME Username (or UID) to log into. Default: 1000 (the primary user) + -X XMETHOD Override the auto-detected XMETHOD for this session. -x Does not log in, but directly executes the command instead. Note that the environment will be empty (sans TERM). Specify -x a second time to run the $SETUPSCRIPT script." @@ -57,7 +61,7 @@ chrootcmd() { # Process arguments prevoptind=1 -while getopts 'bc:k:ln:t:u:x' f; do +while getopts 'bc:k:ln:t:u:X:x' f; do # Disallow empty string as option argument if [ "$((OPTIND-prevoptind))" = 2 -a -z "$OPTARG" ]; then error 2 "$USAGE" @@ -65,12 +69,13 @@ while getopts 'bc:k:ln:t:u:x' f; do prevoptind="$OPTIND" case "$f" in b) BACKGROUND='y';; - c) CHROOTS="`readlink -m "$OPTARG"`";; + c) CHROOTS="`readlink -m -- "$OPTARG"`"; CHROOTSSET='y';; k) KEYFILE="$OPTARG";; l) LOGIN='y';; n) NAME="$OPTARG";; t) TARGET="$OPTARG";; u) USERNAME="$OPTARG";; + X) TMPXMETHOD="$OPTARG";; x) NOLOGIN="$((NOLOGIN+1))" [ "$NOLOGIN" -gt 2 ] && NOLOGIN=2;; \?) error 2 "$USAGE";; @@ -105,6 +110,13 @@ if [ "$NOLOGIN" = 2 ]; then set -- "$SETUPSCRIPT" fi +# Try to mount the crouton partition if it exists, CHROOTS has not been set +# manually, and this script base directory is /usr/local/bin. +if [ -z "$CHROOTSSET" -a "$BINDIR" = "/usr/local/bin" ] && \ + mountcrouton "$MOUNTPOINT"; then + CHROOTS="$MOUNTPOINT/chroots" +fi + # Select the first chroot available if one hasn't been specified if [ -z "$NAME" ]; then haschroots='' @@ -133,17 +145,16 @@ elif [ -n "$TARGET" ]; then fi fi +# Check to ensure that the XMETHOD requested has been installed +if [ -n "$TMPXMETHOD" ]; then + if ! grep -q "^${TMPXMETHOD%%-*}$" "$CHROOTS/$NAME/.crouton-targets"; then + error 1 "$CHROOTS/$NAME does not contain XMETHOD '${TMPXMETHOD%%-*}'" + fi +fi + # Avoid kernel panics due to slow I/O disablehungtask -# Enable control of framebuffer compression (if used) by video users -for fbc in '/sys/kernel/debug/dri/0/'*fbc*; do :; done -if [ -f "$fbc" ] && ! grep -q 'disabled per module param' "$fbc"; then - fbc='/sys/module/i915/parameters/i915_enable_fbc' - chgrp video "$fbc" - chmod g+w "$fbc" -fi - # Allow X server running as normal user to set/drop DRM master drm_relax_file="/sys/kernel/debug/dri/drm_master_relax" if [ -f "$drm_relax_file" ]; then @@ -352,7 +363,6 @@ tmpfsmount /var/run 'noexec,nosuid,mode=0755,size=10%' tmpfsmount /var/run/lock 'noexec,nosuid,nodev,size=5120k' bindmount /var/run/dbus /var/host/dbus bindmount /var/run/shill /var/host/shill -bindmount /var/run/cras /var/host/cras bindmount /var/lib/timezone /var/host/timezone for m in /lib/modules/*; do if [ -d "$m" ]; then @@ -373,8 +383,14 @@ else ln -sfT /dev/.udev "`fixabslinks '/var/run'`/udev" fi -# Add a /var/host/cras symlink for CRAS clients -ln -sfT /var/host/cras "`fixabslinks '/var/run'`/cras" +if [ -d /var/run/cras ]; then + bindmount /var/run/cras /var/host/cras + # Add a /var/host/cras symlink for CRAS clients + ln -sfT /var/host/cras "$(fixabslinks '/var/run')/cras" +else + echo "\ +WARNING: CRAS not running in Chromium OS. Audio forwarding will not work." 1>&2 +fi # Bind-mount /media, specifically the removable directory destmedia="`fixabslinks '/var/host/media'`" @@ -385,10 +401,134 @@ if ! mountpoint -q "$destmedia"; then mount --rbind /media "$destmedia" fi -# Bind-mount ~/Downloads if we're logged in as a user -localdownloads='/home/chronos/user/Downloads' -if [ -z "$NOLOGIN" -a -n "$CHROOTHOME" -a -d "$localdownloads" ]; then - bindmount "$localdownloads" "$CHROOTHOME/Downloads" exec +# Provide a default /etc/crouton/shares file +shares="`fixabslinks '/etc/crouton/shares'`" +if [ -e "$shares" -a ! -f "$shares" ]; then + echo "Not mounting shares: /etc/crouton/shares is not a file." 1>&2 +elif [ ! -f "$shares" ]; then + cat > "$shares" < "/dev/stderr" + } + ' "$shares" | while read -r src && read -r dest && read -r opts; do + line="\n \"$src\" \"$dest\" $opts" + # Expand src + case "${src%%/*}" in + download|downloads) + if [ -z "$localdownloads" ]; then + echo "Not mounting share (no Chromium OS user):$line" 1>&2 + continue + fi + src="$localdownloads/${src#*/}";; + encrypt|encrypted) + if [ -z "$localencrypted" ]; then + echo "Not mounting share (no Chromium OS user):$line" 1>&2 + continue + fi + src="$localencrypted/${src#*/}";; + share|shares|shared) + src="$localshare/${src#*/}";; + *) + echo "Invalid share:$line" 1>&2 + continue;; + esac + # Expand dest for homedirs + if [ "${dest#~}" != "$dest" ]; then + destuser="${dest%%/*}" + if [ "$destuser" = '~' ]; then + if [ -z "$CHROOTHOME" ]; then + echo "Not mounting share (no chroot user):$line" 1>&2 + continue + fi + dest="$CHROOTHOME/${dest#*/}" + else + dest="/home/${destuser#~}/${dest#*/}" + fi + fi + # Do the bindmount + mkdir -m 700 -p "$src" + if ! bindmount "$src" "$dest" "${opts:-exec}"; then + echo "Failed to mount share:$line" 1>&2 + fi + done fi # Bind-mount /sys recursively, making it a slave in the chroot @@ -515,6 +655,27 @@ if [ ! "$NOLOGIN" = 1 ] && grep -q '^root:' "$passwd" 2>/dev/null; then # systemd-logind doesn't fork chrootcmd "/lib/systemd/systemd-logind >/dev/null 2>&1 /dev/null 2>&1 > '$CROUTONLOCKDIR/frecon') & :" + fi fi # Start the chroot and any specified command @@ -547,6 +708,9 @@ else else # Escape out the command cmd="export SHELL='$CHROOTSHELL';" + if [ -n "$TMPXMETHOD" ]; then + cmd="$cmd export XMETHOD='$TMPXMETHOD';" + fi for param in "$@"; do cmd="$cmd'`echo -n "$param" | sed "s/'/'\\\\\\''/g"`' " done diff --git a/host-bin/mount-chroot b/host-bin/mount-chroot index dfaab295b..895b3b979 100755 --- a/host-bin/mount-chroot +++ b/host-bin/mount-chroot @@ -8,6 +8,8 @@ set -e APPLICATION="${0##*/}" BINDIR="`dirname "\`readlink -f "$0"\`"`" CHROOTS="`readlink -m "$BINDIR/../chroots"`" +CHROOTSSET='' +MOUNTPOINT='/var/crouton' CREATE='' ENCRYPT='' KEYFILE='' @@ -33,17 +35,17 @@ Options: . "$BINDIR/../installer/functions" # Process arguments -while getopts 'c:ek:np' f; do - case "$f" in - c) CHROOTS="`readlink -m "$OPTARG"`";; +getopts_string='c:ek:np' +while getopts_nextarg; do + case "$getopts_var" in + c) CHROOTS="`readlink -m -- "$getopts_arg"`"; CHROOTSSET='y';; e) ENCRYPT="$((ENCRYPT+1))";; - k) KEYFILE="$OPTARG";; + k) KEYFILE="$getopts_arg";; n) CREATE='y';; p) PRINT='y';; \?) error 2 "$USAGE";; esac done -shift "$((OPTIND-1))" # Need at least one chroot listed if [ $# = 0 ]; then @@ -55,6 +57,13 @@ if [ ! "$USER" = root -a ! "$UID" = 0 ]; then error 2 "$APPLICATION must be run as root." fi +# Try to mount the crouton partition if it exists, CHROOTS has not been set +# manually, and this script base directory is /usr/local/bin. +if [ -z "$CHROOTSSET" -a "$BINDIR" = "/usr/local/bin" ] && \ + mountcrouton "$MOUNTPOINT"; then + CHROOTS="$MOUNTPOINT/chroots" +fi + # Make sure we always exit with echo on the tty. addtrap "stty echo 2>/dev/null" @@ -141,7 +150,7 @@ for NAME in "$@"; do if [ ! -f '/mnt/stateful_partition/etc/devmode.passwd' ]; then echo 'You must have a root password in Chromium OS to mount encrypted chroots.' 1>&2 if [ -z "$CROUTON_PASSPHRASE$CROUTON_NEW_PASSPHRASE" ]; then - chromeos-setdevpasswd + while ! chromeos-setdevpasswd; do :; done fi fi @@ -180,8 +189,8 @@ for NAME in "$@"; do random="/dev/urandom" echo 'Generating keys from /dev/urandom...' 1>&2 fi - key="`hexdump -v -n32 -e'"%02x"' "$random"`" - fnek="`hexdump -v -n32 -e'"%02x"' "$random"`" + key="`hexdump -v -n32 -e'32/1 "%02x"' "$random"`" + fnek="`hexdump -v -n32 -e'32/1 "%02x"' "$random"`" echo 'done' 1>&2 # Create key file @@ -296,12 +305,12 @@ no -- You don't want to decide one way or another quite yet. -not -wholename './.crouton-targets' \ -exec mkdir -p "$tmp/{}" ';' \ -exec rmdir "$tmp/{}" ';' \ - '(' -prune , -exec mv -f '{}' "$tmp/{}" ';' ')' 1>&2 + '(' -prune , -exec mv -fT '{}' "$tmp/{}" ';' ')' 1>&2 for tmp in ECRYPTFS_MOVE_STAGING_*; do ( cd "$tmp" find '!' '(' -type d -exec test -d "$CHROOT/{}" ';' ')' \ - '(' -prune , -exec mv -f '{}' "$CHROOT/{}" ';' ')' \ + '(' -prune , -exec mv -fT '{}' "$CHROOT/{}" ';' ')' \ -exec echo -n . ';' 1>&2 find -depth -type d -not -wholename . \ -exec test -d "$CHROOT/{}" ';' \ diff --git a/host-bin/unmount-chroot b/host-bin/unmount-chroot index 17a7c3ba8..65ae3d32d 100755 --- a/host-bin/unmount-chroot +++ b/host-bin/unmount-chroot @@ -9,6 +9,8 @@ APPLICATION="${0##*/}" ALLCHROOTS='' BINDIR="`dirname "\`readlink -f "$0"\`"`" CHROOTS="`readlink -m "$BINDIR/../chroots"`" +CHROOTSSET='' +MOUNTPOINT='/var/crouton' EXCLUDEROOT='' FORCE='' PRINT='' @@ -38,29 +40,24 @@ Options: -y Signal any remaining processes without confirmation. Automatically escalates from SIGTERM to SIGKILL." -# Function to exit with exit code $1, spitting out message $@ to stderr -error() { - local ecode="$1" - shift - echo "$*" 1>&2 - exit "$ecode" -} +# Common functions +. "$BINDIR/../installer/functions" # Process arguments -while getopts 'ac:fkpt:xy' f; do - case "$f" in +getopts_string='ac:fkpt:xy' +while getopts_nextarg; do + case "$getopts_var" in a) ALLCHROOTS='y';; - c) CHROOTS="`readlink -m "$OPTARG"`";; + c) CHROOTS="`readlink -m -- "$getopts_arg"`"; CHROOTSSET='y';; f) FORCE='y';; k) SIGNAL="KILL";; p) PRINT='y';; - t) TRIES="$OPTARG";; + t) TRIES="$getopts_arg";; x) EXCLUDEROOT='y';; y) YES='a';; \?) error 2 "$USAGE";; esac done -shift "$((OPTIND-1))" # Need at least one chroot listed, or -a; not both. if [ $# = 0 -a -z "$ALLCHROOTS" ] || [ ! $# = 0 -a -n "$ALLCHROOTS" ]; then @@ -79,19 +76,30 @@ if [ ! "$USER" = root -a ! "$UID" = 0 ]; then error 2 "$APPLICATION must be run as root." fi +# Try to mount the crouton partition if it exists, CHROOTS has not been set +# manually, and this script base directory is /usr/local/bin. +# This may not appear to be necessary in unmount-chroot, but mountcrouton both +# detects and mounts the partition. We only really need the detection part here, +# but mounting does not hurt (the partition most likely already is mounted). +if [ -z "$CHROOTSSET" -a "$BINDIR" = "/usr/local/bin" ] && \ + mountcrouton "$MOUNTPOINT"; then + CHROOTS="$MOUNTPOINT/chroots" +fi + # Check if a chroot is running with this directory. We detect the # appropriate commands by checking if the command's parent root is not equal # to the pid's root. This avoids not unmounting due to a lazy-quitting # background application within the chroot. We also don't consider processes -# that have a parent PID of 1 (which would mean an orphaned process in this -# case), as enter-chroot never orphans its children. +# that have a parent PID of 1 or that of session_manager's (which would mean an +# orphaned process in this case), as enter-chroot never orphans its children. # $1: $base; the canonicalized base path of the chroot # Returns: non-zero if the chroot is in use. checkusage() { if [ -n "$FORCE" ]; then return 0 fi - local b="${1%/}/" pid ppid proot prootdir root rootdir + local b="${1%/}/" pid ppid proot prootdir root rootdir pids='' + local smgrpid="`pgrep -o -u 0 -x session_manager || echo 1`" for root in /proc/*/root; do if [ ! -r "$root" ]; then continue @@ -104,7 +112,7 @@ checkusage() { pid="${root#/proc/}" pid="${pid%/root}" ppid="`ps -p "$pid" -o ppid= 2>/dev/null | sed 's/ //g'`" - if [ -z "$ppid" ] || [ "$ppid" -eq 1 ]; then + if [ -z "$ppid" ] || [ "$ppid" -eq 1 -o "$ppid" -eq "$smgrpid" ]; then continue fi proot="/proc/$ppid/root" @@ -115,10 +123,15 @@ checkusage() { fi fi if [ -n "$PRINT" ]; then - ps -p "$pid" -o pid= -o cmd= || true + pids="$pids $pid" + continue fi return 1 done + if [ -n "$PRINT" -a -n "$pids" ]; then + ps -p "${pids# }" -o pid= -o cmd= || true + return 1 + fi return 0 } diff --git a/host-ext/.gitignore b/host-ext/.gitignore index aa3cfaa96..ba608417e 100644 --- a/host-ext/.gitignore +++ b/host-ext/.gitignore @@ -1,3 +1,4 @@ crouton.crx crouton.zip crouton.pem +crouton/kiwi.pexe diff --git a/host-ext/crouton/background.js b/host-ext/crouton/background.js index 38f9f7136..6c754089b 100644 --- a/host-ext/crouton/background.js +++ b/host-ext/crouton/background.js @@ -1,21 +1,23 @@ // Copyright (c) 2014 The crouton Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +'use strict'; /* Constants */ var URL = "ws://localhost:30001/"; -var VERSION = 1; /* Note: the extension must always be backward compatible */ +var VERSION = 2; /* Note: the extension must always be backward compatible */ var MAXLOGGERLEN = 20; var RETRY_TIMEOUT = 5; var UPDATE_CHECK_INTERVAL = 15*60; /* Check for updates every 15' at most */ +var WINDOW_UPDATE_INTERVAL = 15; /* Update window list every 15" at most */ /* String to copy to the clipboard if it should be empty */ var DUMMY_EMPTYSTRING = "%"; -LogLevel = { +var LogLevel = Object.freeze({ ERROR : "error", - INFO : "info", + INFO : "info", DEBUG : "debug" -} +}); /* Global variables */ var clipboardholder_; /* textarea used to hold clipboard content */ @@ -24,6 +26,8 @@ var websocket_ = null; /* Active connection */ /* State variables */ var debug_ = false; +var showlog_ = false; /* true if extension log should be shown */ +var hidpi_ = false; /* true if kiwi windows should be opened in HiDPI mode */ var enabled_ = true; /* true if we are trying to connect */ var active_ = false; /* true if we are connected to a server */ var error_ = false; /* true if there was an error during the last connection */ @@ -31,9 +35,19 @@ var dummystr_ = false; /* true if the last string we copied was the dummy string var update_ = false; /* true if an update to the extension is available */ var lastupdatecheck_ = null; +var lastwindowlistupdate_ = null; var status_ = ""; +var sversion_ = 0; /* Version of the websocket server */ var logger_ = []; /* Array of status messages: [LogLevel, time, message] */ +var windows_ = []; /* Array of windows. (.display, .name) */ + +var kiwi_win_ = {}; /* Map of kiwi windows. Key is display, value is object + (.id, .isTab, .window: window element) */ +var focus_win_ = -1; /* Focused kiwi window. -1 if no kiwi window focused. */ + +var notifications_ = {}; /* Map of notification id to function to be called when + the notification is clicked. */ /* Set the current status string. * active is a boolean, true if the WebSocket connection is established. */ @@ -78,32 +92,66 @@ function checkUpdate(force) { } } +function updateWindowList(force) { + if (!active_ || sversion_ < 2) { + windows_ = []; + return; + } + + var currenttime = new Date().getTime(); + + if (force || lastwindowlistupdate_ == null || + (currenttime-lastwindowlistupdate_) > 1000*WINDOW_UPDATE_INTERVAL) { + lastwindowlistupdate_ = currenttime; + printLog("Sending window list request", LogLevel.DEBUG); + websocket_.send("Cs" + focus_win_); + websocket_.send("Cl"); + } +} + +/* Called from kiwi (window.js), so we can directly access each window */ +function registerKiwi(displaynum, window) { + var display = ":" + displaynum; + if (kiwi_win_[display] && kiwi_win_[display].id >= -1) { + kiwi_win_[display].window = window; + } +} + +/* Close the popup window */ +function closePopup() { + var views = chrome.extension.getViews({type: "popup"}); + for (var i = 0; i < views.length; views++) { + views[i].close(); + } +} + /* Update the icon, and refresh the popup page */ function refreshUI() { + updateWindowList(false); + + var icon = "disconnected"; if (error_) - icon = "error" + icon = "error"; else if (!enabled_) - icon = "disabled" + icon = "disabled"; else if (active_) icon = "connected"; - else - icon = "disconnected"; chrome.browserAction.setIcon( - {path: {'19': icon + '-19.png', '38': icon + '-38.png'}} + {path: {19: icon + '-19.png', 38: icon + '-38.png'}} ); chrome.browserAction.setTitle({title: 'crouton: ' + icon}); var views = chrome.extension.getViews({type: "popup"}); for (var i = 0; i < views.length; views++) { + var view = views[i]; /* Make sure page is ready */ - if (document.readyState === "complete") { + if (view.document.readyState === "complete") { /* Update "help" link */ - helplink = views[i].document.getElementById("help"); + var helplink = view.document.getElementById("help"); helplink.onclick = showHelp; /* Update enable/disable link. */ - /* FIXME: Sometimes, there is a little box coming around the link */ - enablelink = views[i].document.getElementById("enable"); + var enablelink = view.document.getElementById("enable"); if (enabled_) { enablelink.textContent = "Disable"; enablelink.onclick = function() { @@ -127,38 +175,108 @@ function refreshUI() { } /* Update debug mode according to checkbox state. */ - debugcheck = views[i].document.getElementById("debugcheck"); + var debugcheck = view.document.getElementById("debugcheck"); debugcheck.onclick = function() { debug_ = debugcheck.checked; refreshUI(); + var disps = Object.keys(kiwi_win_); + for (var i = 0; i < disps.length; i++) { + var win = kiwi_win_[disps[i]]; + if (win.window) { + if (win.isTab) { + chrome.tabs.sendMessage(win.id, + {func: 'setDebug', param: debug_?1:0}); + } else { + win.window.setDebug(debug_?1:0); + } + } + } } debugcheck.checked = debug_; + /* Update hidpi mode according to checkbox state. */ + var hidpicheck = view.document.getElementById("hidpicheck"); + if (window.devicePixelRatio > 1) { + hidpicheck.onclick = function() { + hidpi_ = hidpicheck.checked; + refreshUI(); + var disps = Object.keys(kiwi_win_); + for (var i = 0; i < disps.length; i++) { + var win = kiwi_win_[disps[i]]; + if (win.window) { + if (win.isTab) { + chrome.tabs.sendMessage(win.id, + {func: 'setHiDPI', param: hidpi_?1:0}); + } else { + win.window.setHiDPI(hidpi_?1:0); + } + } + } + } + hidpicheck.disabled = false; + } else { + hidpicheck.disabled = true; + } + hidpicheck.checked = hidpi_; + /* Update status box */ - views[i].document.getElementById("info").textContent = status_; + view.document.getElementById("info").textContent = status_; + + /* Update window table */ + /* FIXME: Improve UI */ + var windowlist = view.document.getElementById("windowlist"); + + while (windowlist.rows.length > 0) { + windowlist.deleteRow(0); + } + + for (var i = 0; i < windows_.length; i++) { + var row = windowlist.insertRow(-1); + var cell1 = row.insertCell(0); + var cell2 = row.insertCell(1); + cell1.className = "display"; + cell1.innerHTML = windows_[i].display; + cell2.className = "name"; + cell2.innerHTML = windows_[i].name; + cell2.onclick = (function(i) { return function() { + if (active_) { + websocket_.send("C" + windows_[i].display); + closePopup(); + } + } })(i); + } /* Update logger table */ - loggertable = views[i].document.getElementById("logger"); + var loggertable = view.document.getElementById("logger"); /* FIXME: only update needed rows */ while (loggertable.rows.length > 0) { loggertable.deleteRow(0); } - for (i = 0; i < logger_.length; i++) { - value = logger_[i]; - - if (value[0] == LogLevel.DEBUG && !debug_) - continue; - - var row = loggertable.insertRow(-1); - var cell1 = row.insertCell(0); - var cell2 = row.insertCell(1); - var levelclass = value[0]; - cell1.className = "time " + levelclass; - cell2.className = "value " + levelclass; - cell1.innerHTML = value[1]; - cell2.innerHTML = value[2]; + /* Only update if "show log" is enabled */ + var logcheck = view.document.getElementById("logcheck"); + logcheck.onclick = function() { + showlog_ = logcheck.checked; + refreshUI(); + } + logcheck.checked = showlog_; + if (showlog_) { + for (var i = 0; i < logger_.length; i++) { + var value = logger_[i]; + + if (value[0] == LogLevel.DEBUG && !debug_) + continue; + + var row = loggertable.insertRow(-1); + var cell1 = row.insertCell(0); + var cell2 = row.insertCell(1); + var levelclass = value[0]; + cell1.className = "time " + levelclass; + cell2.className = "value " + levelclass; + cell1.innerHTML = value[1]; + cell2.innerHTML = value[2]; + } } } } @@ -170,8 +288,22 @@ function clipboardStart() { LogLevel.INFO); setStatus("Started...", false); + /* Monitor window/tab focus changes/removals and report to croutonclip */ + chrome.windows.onFocusChanged.addListener( + function(id) { onFocusChanged(id, false); }); + chrome.windows.onRemoved.addListener( + function(id) { onRemoved(id, false); }); + chrome.tabs.onActivated.addListener( + function(data) { onFocusChanged(data.tabId, true); }); + chrome.tabs.onRemoved.addListener( + function(id, data) { onRemoved(id, true); }); + clipboardholder_ = document.getElementById("clipboardholder"); + /* Notification event handlers */ + chrome.notifications.onClosed.addListener(notificationClosed); + chrome.notifications.onClicked.addListener(notificationClicked); + websocketConnect(); } @@ -235,14 +367,17 @@ function websocketMessage(evt) { /* Only accept version packets until we have received one. */ if (!active_) { if (cmd == 'V') { /* Version */ - if (payload < 1 || payload > VERSION) { + sversion_ = payload; + if (sversion_ < 1 || sversion_ > VERSION) { websocket_.send("EInvalid version (> " + VERSION + ")"); - error("Invalid server version " + payload + " > " + VERSION, + error("Invalid server version " + sversion_ + " > " + VERSION, false); } - /* Set active_ to true */ - setStatus("Connected", true); websocket_.send("VOK"); + /* Set active_ to true */ + setStatus(sversion_ >= 2 ? "" : "Connected", true); + /* Force a window list update */ + updateWindowList(true); return; } else { error("Received frame while waiting for version", false); @@ -295,6 +430,181 @@ function websocketMessage(evt) { websocket_.send("EError: URL must be absolute"); } + break; + case 'N': /* Raise a notification */ + /* Payload in JSON format, compatible with chrome.extensions specifications */ + try { + var data = JSON.parse(payload); + + if (!data.type) + data.type = "basic"; + if (!data.iconUrl) + data.iconUrl = "icon-128.png"; + + /* Strip off crouton fields */ + var id = ""; + var display = null; + + if (data.crouton_id) { + id = data.crouton_id; + } + delete data.crouton_id; + + /* Set context message with chroot name/display */ + delete data.contextMessage; + if (data.crouton_display) { + display = data.crouton_display; + var win = windows_.filter(function(x) { + return x.display == display })[0]; + var name = win ? (win.name + " (" + display + ")") : display; + data.contextMessage = "Switch to " + name; + } + delete data.crouton_display; + + chrome.notifications.create(id, data, + function(id) { + printLog("Raised notification " + id, LogLevel.DEBUG); + notifications_[id] = function() { + if (display) + websocket_.send("C" + display); + /* Remove the notification. */ + chrome.notifications.clear(id, function(_) {}); + } + }); + websocket_.send("NOK"); + } catch(e) { + printLog("Notification parsing error: " + e + + " (payload: '" + payload + "').", LogLevel.ERROR); + websocket_.send("EError: invalid payload."); + } + break; + case 'C': /* Returned data from a croutoncycle command */ + /* Non-zero length has a window list; otherwise it's a cycle signal */ + if (payload.length > 0) { + windows_ = payload.split('\n').map( + function(x) { + var m = x.match(/^([^ *]*)\*? +(.*)$/); + if (!m) + return null; + + /* Only display cros and X11 servers (no window) */ + if (m[1] != "cros" && !m[1].match(/^:([0-9]+)$/)) + return null; + + var k = new Object(); + k.display = m[1]; + k.name = m[2]; + return k; + } + ).filter( function(x) { return !!x; } ); + + windows_.forEach(function(k) { + var win = kiwi_win_[k.display]; + if (win && win.window) { + if (win.isTab) { + chrome.tabs.sendMessage(win.id, + {func: 'setTitle', param: k.name}); + } else { + win.window.setTitle(k.name); + } + } + }); + + lastwindowlistupdate_ = new Date().getTime(); + websocket_.send("COK"); + } + refreshUI(); + break; + case 'X': /* Ask to open a crouton window */ + var display = payload; + var match = display.match(/^:([0-9]+)([- ][^- ]*)*$/); + var displaynum = match ? match[1] : null; + var mode = null; + if (displaynum) { + display = ":" + displaynum; + mode = match[2] && match[2].length >= 2 ? match[2].charAt(1) : 'f'; + if ('fwt'.indexOf(mode) == -1) { + console.log('invalid xiwi mode: ' + mode); + mode = 'f'; + } + } + if (!displaynum) { + /* Minimize all kiwi windows */ + var disps = Object.keys(kiwi_win_); + for (var i = 0; i < disps.length; i++) { + if (kiwi_win_[disps[i]].isTab) { + continue; + } + var winid = kiwi_win_[disps[i]].id; + chrome.windows.update(winid, {focused: false}); + + var minimize = function(win) { + chrome.windows.update(winid, {state: 'minimized'}); }; + + chrome.windows.get(winid, function(win) { + /* To make restore nicer, first exit full screen, + * then minimize */ + if (win.state == "fullscreen") { + chrome.windows.update(winid, {state: 'maximized'}, + minimize); + } else { + minimize(); + } + }); + } + } else if (kiwi_win_[display] && kiwi_win_[display].id >= 0 && + (!kiwi_win_[display].window || + !kiwi_win_[display].window.closing)) { + /* focus/full screen an existing window */ + var winid = kiwi_win_[display].id; + if (kiwi_win_[display].isTab) { + chrome.tabs.update(winid, {active: true}); + chrome.tabs.get(winid, function(tab) { + chrome.windows.update(tab.windowId, {focused: true}); + }); + } else { + chrome.windows.update(winid, {focused: true}); + chrome.windows.get(winid, function(win) { + if (win.state == "maximized") + chrome.windows.update(winid, {state: 'fullscreen'}); + }); + } + } else { + /* Open a new window */ + kiwi_win_[display] = new Object(); + kiwi_win_[display].id = -1; + kiwi_win_[display].isTab = (mode == 't'); + kiwi_win_[display].window = null; + + var win = windows_.filter(function(x){return x.display == display})[0]; + var name = win ? win.name : "crouton in a window"; + var create = chrome.windows.create; + var data = {}; + + if (kiwi_win_[display].isTab) { + name = win ? win.name : "crouton in a tab"; + create = chrome.tabs.create; + } else { + data['type'] = "popup"; + } + + data['url'] = "window.html?display=" + displaynum + + "&debug=" + (debug_ ? 1 : 0) + + "&hidpi=" + (hidpi_ ? 1 : 0) + + "&title=" + encodeURIComponent(name) + + "&mode=" + mode; + + create(data, function(newwin) { + kiwi_win_[display].id = newwin.id; + focus_win_ = display; + if (active_ && sversion_ >= 2) + websocket_.send("Cs" + focus_win_); + }); + } + websocket_.send("XOK"); + closePopup(); + /* Force a window list update */ + updateWindowList(true); break; case 'P': /* Ping */ websocket_.send(received_msg); @@ -333,6 +643,55 @@ function websocketClose() { checkUpdate(false); } +/* Called when window/tab in focus changes: feedback to the extension so the + * clipboard can be transfered. */ +function onFocusChanged(id, isTab) { + var disps = Object.keys(kiwi_win_); + var nextfocus_win = "cros"; + for (var i = 0; i < disps.length; i++) { + if (kiwi_win_[disps[i]].isTab == isTab + && kiwi_win_[disps[i]].id == id) { + nextfocus_win = disps[i]; + break; + } + } + if (focus_win_ != nextfocus_win) { + focus_win_ = nextfocus_win; + if (active_ && sversion_ >= 2) + websocket_.send("Cs" + focus_win_); + printLog("Window " + focus_win_ + " focused", LogLevel.DEBUG); + } +} + +/* Called when a window/tab is removed, so we can delete its reference. */ +function onRemoved(id, isTab) { + var disps = Object.keys(kiwi_win_); + for (var i = 0; i < disps.length; i++) { + if (kiwi_win_[disps[i]].isTab == isTab + && kiwi_win_[disps[i]].id == id) { + kiwi_win_[disps[i]].id = -2; + kiwi_win_[disps[i]].isTab = false; + kiwi_win_[disps[i]].window = null; + printLog("Window " + disps[i] + " removed", LogLevel.DEBUG); + } + } +} + +/* Called when a notification is clicked */ +function notificationClicked(id) { + printLog("Notification " + id + " clicked.", LogLevel.DEBUG); + if (notifications_[id]) { + notifications_[id](); + } +} + +/* Called when a notification is closed */ +function notificationClosed(id, byUser) { + printLog("Notification " + id + " closed (byUser: " + byUser + ").", + LogLevel.DEBUG); + delete notifications_[id]; +} + function padstr0(i) { var s = i + ""; if (s.length < 2) @@ -343,13 +702,13 @@ function padstr0(i) { /* Add a message in the log. */ function printLog(str, level) { - date = new Date; - datestr = padstr0(date.getHours()) + ":" + - padstr0(date.getMinutes()) + ":" + - padstr0(date.getSeconds()); + var date = new Date; + var datestr = padstr0(date.getHours()) + ":" + + padstr0(date.getMinutes()) + ":" + + padstr0(date.getSeconds()); - if (str.length > 80) - str = str.substring(0, 77) + "..."; + if (str.length > 200) + str = str.substring(0, 197) + "..."; console.log(datestr + ": " + str); /* Add messages to logger */ @@ -368,7 +727,8 @@ function error(str, enabled) { enabled_ = enabled; error_ = true; refreshUI(); - websocket_.close(); + if (websocket != null) + websocket_.close(); /* Force check for extension update (possible reason for the error) */ checkUpdate(true); } @@ -388,7 +748,7 @@ chrome.runtime.onInstalled.addListener(function(details) { chrome.runtime.getPlatformInfo(function(platforminfo) { if (platforminfo.os == 'cros') { /* On error: disconnect WebSocket, then log errors */ - onerror = function(msg, url, line) { + var onerror = function(msg, url, line) { if (websocket_) websocket_.close(); error("Uncaught JS error: " + msg, false); diff --git a/host-ext/crouton/first.html b/host-ext/crouton/first.html index 4bf8eb82a..a0f004c70 100644 --- a/host-ext/crouton/first.html +++ b/host-ext/crouton/first.html @@ -26,10 +26,10 @@

crouton integration
extension<
Thank you for installing the crouton extension!
-
This extension provides clipboard synchronization and URL handler to crouton chroots.
+
This extension provides clipboard synchronization, URL handler, and an X11 window to crouton chroots.
If you have not done so yet, you should download the crouton installer.
Then follow instructions in the README to get your first chroot running.
-
Be sure to install the extension target with the chroot.
+
Be sure to install the extension or xiwi targets with the chroot.
Need more help? Check out the wiki!
diff --git a/host-ext/crouton/kiwi.nmf b/host-ext/crouton/kiwi.nmf new file mode 100644 index 000000000..eb12b933e --- /dev/null +++ b/host-ext/crouton/kiwi.nmf @@ -0,0 +1,9 @@ +{ + "program": { + "portable": { + "pnacl-translate": { + "url": "kiwi.pexe" + } + } + } +} diff --git a/host-ext/crouton/manifest.json b/host-ext/crouton/manifest.json index 4d18b6786..56904c72c 100644 --- a/host-ext/crouton/manifest.json +++ b/host-ext/crouton/manifest.json @@ -4,7 +4,7 @@ "name": "crouton integration", "short_name": "crouton", "description": "Improves integration with crouton chroots.", - "version": "1.0.0", + "version": "2.4.0", "icons": { "48": "icon-48.png", "128": "icon-128.png" @@ -24,6 +24,7 @@ }, "permissions": [ "clipboardRead", - "clipboardWrite" + "clipboardWrite", + "notifications" ] } diff --git a/host-ext/crouton/popup.html b/host-ext/crouton/popup.html index bee2d4bf6..c4f901e19 100644 --- a/host-ext/crouton/popup.html +++ b/host-ext/crouton/popup.html @@ -18,10 +18,9 @@ outline: 0; } - #scrolllogger { - width: 400px; - height: 200px; - overflow-y: auto; + #windowlist { + border: 0px; + border-spacing: 0; } #logger { @@ -33,7 +32,19 @@ font-family: "monospace"; font-size: 12px; vertical-align: top; - padding: 2px 15px; + } + + td.display { + padding: 2px 5px; + font-size: 20px; + color:#666666; + } + + td.name { + padding: 2px 5px; + font-size: 20px; + color:#000080; + cursor:pointer; } td.time { @@ -46,21 +57,32 @@ color: #000000; } + td.value { + padding: 2px 15px; + } td.value.debug { color: #808080; } td.value.info { color: #008000; } td.value.error { color: #800000; } + + input:disabled+label { color:#cccccc; } - +

crouton integration


Loading...
+
-
.
-
-
-
+
+ + + + + + +
diff --git a/host-ext/crouton/popup.js b/host-ext/crouton/popup.js index b3811cd53..4c4f4401a 100644 --- a/host-ext/crouton/popup.js +++ b/host-ext/crouton/popup.js @@ -2,6 +2,8 @@ * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ +'use strict'; + document.addEventListener('DOMContentLoaded', function() { chrome.extension.getBackgroundPage().refreshUI(); }); diff --git a/host-ext/crouton/window.html b/host-ext/crouton/window.html new file mode 100644 index 000000000..2ba7def7e --- /dev/null +++ b/host-ext/crouton/window.html @@ -0,0 +1,107 @@ + + + + + + + + Crouton in a tab + + + + +
+ +
+
Initializing...
+
+ + WARNING: +
+
ERROR:
+
+
+ +
+
+ + diff --git a/host-ext/crouton/window.js b/host-ext/crouton/window.js new file mode 100644 index 000000000..f19dc7484 --- /dev/null +++ b/host-ext/crouton/window.js @@ -0,0 +1,320 @@ +// Copyright (c) 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +'use strict'; + +var CLOSE_TIMEOUT = 0; /* Close window x seconds after disconnect */ +var DEBUG_LEVEL = 2; /* If debug is enabled, use this level in NaCl */ +var RESIZE_RATE_LIMIT = 300; /* No more than 1 resize query every x ms */ + +var KiwiModule_ = null; /* NaCl module */ +var listener_ = null; /* listener div element */ +var infodiv_ = null; /* info div (contains status, warning(s), error(s)) */ +var statusdiv_ = null; /* status div */ +var warningdiv_ = null; /* warning div */ +var errordiv_ = null; /* error div */ + +var debug_ = 0; /* Debuging level, passed to NaCl module */ +var hidpi_ = 0; /* HiDPI mode */ +var display_ = -1; /* Display number to use */ +var title_ = "crouton"; /* window title */ +var connected_ = false; +var closing_ = false; /* Disconnected, and waiting for the window to close */ +var error_ = false; /* An error has occured */ + +var prevstate_ = "maximized"; /* Previous window state (before full screen) */ + + /* Rate limit resize events */ +var resizePending_ = false; +var resizeLimited_ = false; + +function registerWindow(register) { + chrome.extension.getBackgroundPage(). + registerKiwi(display_, register ? window : null); +} + +/* NaCl module loaded */ +function moduleDidLoad() { + KiwiModule_ = document.getElementById('kiwi'); + setStatus('Starting...'); + KiwiModule_.postMessage('debug:' + debug_); + KiwiModule_.postMessage('hidpi:' + hidpi_); + /* Sending the display command triggers a connection: send it last. */ + KiwiModule_.postMessage('display:' + display_); + KiwiModule_.focus(); +} + +/* NaCl is loading... */ +function handleProgress(event) { + /* We could compute a percentage, but loading gets stuck at 89% (while + * translating?), so it's not very useful... */ + setStatus('Loading...'); +} + +/* NaCl module failed to load */ +function handleError(event) { + // We can't use common.naclModule yet because the module has not been + // loaded. + KiwiModule_ = document.getElementById('kiwi'); + showError(KiwiModule_.lastError); + registerWindow(false); +} + +/* NaCl module crashed */ +function handleCrash(event) { + if (KiwiModule_.exitStatus == -1) { + showError('NaCl module crashed.'); + } else { + showError('NaCl module exited: ' + KiwiModule_.exitStatus); + } + registerWindow(false); +} + +/* Handle requests from the background page (for tabs) */ +function handleRequest(message, sender, sendResponse) { + if (typeof(window[message.func]) == "function") { + window[message.func](message.param); + } +}; + +/* Change debugging level */ +function setDebug(debug) { + debug_ = (debug > 0) ? DEBUG_LEVEL : 0; + if (debug_ > 0) { + document.getElementById('content').style.paddingTop = "16px"; + document.getElementById('header').style.display = 'block'; + } else { + document.getElementById('content').style.paddingTop = "0px"; + document.getElementById('header').style.display = 'none'; + } + if (KiwiModule_) { + KiwiModule_.postMessage('debug:' + debug_); + kiwiResize(); + } +} + +/* Change HiDPI mode */ +function setHiDPI(hidpi) { + hidpi_ = hidpi; + if (KiwiModule_) { + KiwiModule_.postMessage('hidpi:' + hidpi_); + kiwiResize(); + } +} + +function setTitle(title) { + document.title = title; +} + +/* Set status message */ +function setStatus(message) { + if (message) { + statusdiv_.textContent = message; + statusdiv_.style.display = 'block'; + } else { + statusdiv_.style.display = 'none'; + } +} + +/* Set warning message */ +function showWarning(message) { + var div = addInfoLine(warningdiv_, message); + var warningclose = div.getElementsByClassName("close")[0]; + warningclose.onclick = function() { infodiv_.removeChild(div); }; +} + +/* Set error message */ +function showError(message) { + error_ = true; + setStatus(null); + addInfoLine(errordiv_, message); +} + +/* Adds warning/error line to info div. Returns duplicated element */ +function addInfoLine(div, message) { + var newdiv = div.cloneNode(true); + var divtext = newdiv.getElementsByClassName("text")[0]; + divtext.textContent = message; + /* Insert all warnings/errors before the status line. */ + infodiv_.insertBefore(newdiv, statusdiv_); + return newdiv; +} + +/* This function is called when a message is received from the NaCl module. */ +/* Message format is type:payload */ +function handleMessage(message) { + var str = message.data; + var type, payload, i; + if ((i = str.indexOf(":")) > 0) { + type = str.substr(0, i); + payload = str.substr(i+1); + } else { + type = "debug"; + payload = str; + } + + console.log(message.data); + + if (type == "debug") { + var debugEl = document.getElementById('debug'); + if (debugEl) + debugEl.textContent = message.data; + } else if (type == "status") { + setStatus(payload); + } else if (type == "warning") { + showWarning(payload); + } else if (type == "error") { + showError(payload); + } else if (type == "connected") { + connected_ = true; + setStatus(null); + } else if (type == "disconnected") { + connected_ = false; + if (debug_ < 1 && !error_) { + closing_ = true; + setStatus("Disconnected, closing window in " + + CLOSE_TIMEOUT + " seconds."); + setTimeout(function() { window.close() }, CLOSE_TIMEOUT*1000); + } else { + setStatus("Disconnected, please close the window."); + } + registerWindow(false); + } else if (type == "state" && payload == "fullscreen") { + /* Toggle full screen */ + chrome.windows.getCurrent(function(win) { + var newstate = prevstate_; + if (win.state != "fullscreen") { + prevstate_ = win.state; + newstate = "fullscreen"; + } + chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, + {state: newstate}, function(win) {}); + }); + } else if (type == "state" && payload == "hide") { + /* Hide window */ + chrome.windows.getCurrent(function(win) { + var minimize = function(win) { + chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, + {state: 'minimized'}, function(win) {})} + /* To make restore nicer, first exit full screen, then minimize */ + if (win.state == "fullscreen") { + chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, + {state: 'maximized'}, minimize); + } else { + minimize(); + } + }); + } else if (type == "resize") { + i = payload.indexOf("/"); + if (i < 0) return; + /* FIXME: Show scroll bars if the window is too small */ + var width = payload.substr(0, i); + var height = payload.substr(i+1); + var lwidth = listener_.clientWidth; + var lheight = listener_.clientHeight; + var marginleft = (lwidth-width)/2; + var margintop = (lheight-height)/2; + KiwiModule_.style.marginLeft = Math.max(marginleft, 0) + "px"; + KiwiModule_.style.marginTop = Math.max(margintop, 0) + "px"; + KiwiModule_.width = width; + KiwiModule_.height = height; + } +} + +/* Tell the module that the window was resized (this triggers a change of + * resolution, followed by a resize message. */ +function kiwiResize() { + console.log("resize! " + listener_.clientWidth + "/" + listener_.clientHeight); + if (KiwiModule_) + KiwiModule_.postMessage('resize:' + listener_.clientWidth + "/" + listener_.clientHeight); +} + +/* Window was resize, limit to one event per second */ +function handleResize() { + if (!resizeLimited_) { + kiwiResize(); + setTimeout(function() { + if (resizePending_) + kiwiResize(); + resizeLimited_ = resizePending_ = false; + }, RESIZE_RATE_LIMIT); + resizeLimited_ = true; + } else { + resizePending_ = true; + } +} + +/* Called when window changes focus/visiblity */ +function handleFocusBlur(evt) { + /* Unfortunately, hidden/visibilityState is not able to tell when a window + * is not visible at all (e.g. in the background). + * See http://crbug.com/403061 */ + console.log("focus/blur: " + evt.type + ", focus=" + document.hasFocus() + + ", hidden=" + document.hidden + "/" + document.visibilityState); + if (!KiwiModule_) + return; + + if (document.hasFocus()) { + KiwiModule_.postMessage("focus:"); + } else { + if (closing_) + window.close(); + + if (!document.hidden) + KiwiModule_.postMessage("blur:"); + else + KiwiModule_.postMessage("hide:"); + } + console.log("active: " + document.activeElement); + KiwiModule_.focus(); +} + +/* Parse arguments */ +location.search.substring(1).split('&').forEach(function(arg) { + var keyval = arg.split('='); + if (keyval[0] == "display") { + display_ = keyval[1]; + } else if (keyval[0] == "title") { + title_ = decodeURIComponent(keyval[1]); + } else if (keyval[0] == "debug") { + debug_ = keyval[1]; + } else if (keyval[0] == "hidpi") { + hidpi_ = keyval[1]; + } else if (keyval[0] == "mode") { + if (keyval[1] == 'f') { + chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, + {state: "fullscreen"}); + } + } +}); + +document.addEventListener('DOMContentLoaded', function() { + listener_ = document.getElementById('listener'); + listener_.addEventListener('load', moduleDidLoad, true); + listener_.addEventListener('progress', handleProgress, true); + listener_.addEventListener('error', handleError, true); + listener_.addEventListener('crash', handleCrash, true); + listener_.addEventListener('message', handleMessage, true); + window.addEventListener('resize', handleResize); + window.addEventListener('focus', handleFocusBlur); + window.addEventListener('blur', handleFocusBlur); + document.addEventListener('visibilitychange', handleFocusBlur); + chrome.runtime.onMessage.addListener(handleRequest); + + infodiv_ = document.getElementById('info'); + statusdiv_ = document.getElementById('status'); + warningdiv_ = document.getElementById('warning'); + errordiv_ = document.getElementById('error'); + + infodiv_.removeChild(warningdiv_); + infodiv_.removeChild(errordiv_); + + warningdiv_.style.display = 'block'; + errordiv_.style.display = 'block'; + + setDebug(debug_); + setHiDPI(hidpi_); + setTitle(title_); + + registerWindow(true); +}); diff --git a/host-ext/gencrx.sh b/host-ext/gencrx.sh index 142858ffd..1d442deb7 100755 --- a/host-ext/gencrx.sh +++ b/host-ext/gencrx.sh @@ -6,17 +6,17 @@ # This script generates a crx extension package, and is meant to be used by # developers: Users should download the extension from the Web Store. # -# We could leverage on Chrome to build the extension, using something like -# /opt/google/chrome/chrome --pack-extension=crouton -# However many versions cannot build the package without crashing: -# - 27.0.1453.116 aborts at the end of the process, but the package still -# looks fine. -# - 28.0.1500.95 aborts before creating the extension. +# Prerequistes: +# - NaCl SDK. Path specified with NACL_SDK_ROOT (e.g. ~/naclsdk/pepper_35) +# - zip, openssl # -# This code is loosely based a script found along the CRX file format +# This code is loosely based on a script found along the CRX file format # specification: http://developer.chrome.com/extensions/crx.html +set -e + EXTNAME="crouton" +CRIAT_PEXE="$EXTNAME/kiwi.pexe" cd "`dirname "$0"`" @@ -24,6 +24,15 @@ rm -f "$EXTNAME.crx" "$EXTNAME.zip" trap "rm -f '$EXTNAME.sig' '$EXTNAME.pub'" 0 +rm -f "$CRIAT_PEXE" +# Build NaCl module +make -C nacl_src clean +make -C nacl_src +if [ ! -f "$CRIAT_PEXE" ]; then + echo "$CRIAT_PEXE not created as expected" 1>&2 + exit 1 +fi + # Create zip file ( cd $EXTNAME; zip -qr -9 -X "../$EXTNAME.zip" . ) diff --git a/host-ext/nacl_src/.gitignore b/host-ext/nacl_src/.gitignore new file mode 100644 index 000000000..b370218a8 --- /dev/null +++ b/host-ext/nacl_src/.gitignore @@ -0,0 +1 @@ +pnacl diff --git a/host-ext/nacl_src/Makefile b/host-ext/nacl_src/Makefile new file mode 100644 index 000000000..ef96f9769 --- /dev/null +++ b/host-ext/nacl_src/Makefile @@ -0,0 +1,35 @@ +# Copyright (c) 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# GNU Makefile based on shared rules provided by the Native Client SDK. +# See README.Makefiles for more details. + +VALID_TOOLCHAINS := pnacl + +NACL_SDK_ROOT ?= $(shell find ../.. -type d -regex '.*/nacl_sdk/pepper_[0-9]+' \ + | sort | head -n1) + +../crouton/kiwi.pexe: pnacl/Release/kiwi.pexe + cp pnacl/Release/kiwi.pexe ../crouton/kiwi.pexe + +include $(NACL_SDK_ROOT)/tools/common.mk + + +TARGET = kiwi +LIBS = ppapi_cpp ppapi + +CFLAGS = -Wall -std=gnu++11 +SOURCES = kiwi.cc + +# Build rules generated by macros from common.mk: +$(foreach src,$(SOURCES),$(eval $(call COMPILE_RULE,$(src),$(CFLAGS)))) + +ifeq ($(CONFIG),Release) +$(eval $(call LINK_RULE,$(TARGET)_unstripped,$(SOURCES),$(LIBS),$(DEPS))) +$(eval $(call STRIP_RULE,$(TARGET),$(TARGET)_unstripped)) +else +$(eval $(call LINK_RULE,$(TARGET),$(SOURCES),$(LIBS),$(DEPS))) +endif + +$(eval $(call NMF_RULE,$(TARGET),)) diff --git a/host-ext/nacl_src/keycode_converter.h b/host-ext/nacl_src/keycode_converter.h new file mode 100644 index 000000000..2b4459ccb --- /dev/null +++ b/host-ext/nacl_src/keycode_converter.h @@ -0,0 +1,215 @@ +/* Copyright (c) 2015 The crouton Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * Translates NaCl pp::KeyboardInputEvent::KeyCode() strings to X11 keycodes. + */ + +#include +#include +#include + +/* Values in the keycode hashmap. */ +class KeyCode { +public: + KeyCode(uint8_t base, uint8_t search): + base_(base), search_(search) {} + + explicit KeyCode(uint8_t base): KeyCode(base, base) {} + + uint8_t GetCode(const bool search_on) const { + if (search_on) + return search_; + else + return base_; + } + +private: + const uint8_t base_; /* Basic keycode */ + /* Reverse translation of keycode when Search is pressed: e.g. + * Search+Left => Home. In this case: + * base_ = Home keycode (0x6e), search_ = Left keycode (0x71) */ + const uint8_t search_; +}; + +/* Class with static members only: converts KeyCode string to X11 keycode */ +class KeyCodeConverter { +public: + static uint8_t GetCode(const std::string& str, const bool search_on) { + if (!strcodemap_) + InitMap(); + + auto it = strcodemap_->find(str); + /* Not found */ + if (it == strcodemap_->end()) + return 0; + + return it->second.GetCode(search_on); + } + +private: + static void InitMap(); + /* static pointer member, to avoid static variable of class type. */ + static std::map* strcodemap_; +}; + +std::map* KeyCodeConverter::strcodemap_ = NULL; + +/* Initialize string to X11 keycode mapping. Must be called once only. + * + * FIXME: Fill in search_ fields in KeyCode. + * + * Most of this data can be generated from + * ui/events/keycodes/dom4/keycode_converter_data.h in the Chromium source + * tree, using something like: + * sed -n \ +'s/.*USB_KEYMAP([^,]*, \([^,]*\),.*, \("[^"]*"\).*$/{\2, KeyCode(\1)},/p' \ +keycode_converter_data.h | grep -v "0x00" >> keymap_data.h + */ +void KeyCodeConverter::InitMap() { + strcodemap_ = new std::map({ + {"Sleep", KeyCode(0x96)}, + {"WakeUp", KeyCode(0x97)}, + {"KeyA", KeyCode(0x26)}, + {"KeyB", KeyCode(0x38)}, + {"KeyC", KeyCode(0x36)}, + {"KeyD", KeyCode(0x28)}, + {"KeyE", KeyCode(0x1a)}, + {"KeyF", KeyCode(0x29)}, + {"KeyG", KeyCode(0x2a)}, + {"KeyH", KeyCode(0x2b)}, + {"KeyI", KeyCode(0x1f)}, + {"KeyJ", KeyCode(0x2c)}, + {"KeyK", KeyCode(0x2d)}, + {"KeyL", KeyCode(0x2e)}, + {"KeyM", KeyCode(0x3a)}, + {"KeyN", KeyCode(0x39)}, + {"KeyO", KeyCode(0x20)}, + {"KeyP", KeyCode(0x21)}, + {"KeyQ", KeyCode(0x18)}, + {"KeyR", KeyCode(0x1b)}, + {"KeyS", KeyCode(0x27)}, + {"KeyT", KeyCode(0x1c)}, + {"KeyU", KeyCode(0x1e)}, + {"KeyV", KeyCode(0x37)}, + {"KeyW", KeyCode(0x19)}, + {"KeyX", KeyCode(0x35)}, + {"KeyY", KeyCode(0x1d)}, + {"KeyZ", KeyCode(0x34)}, + {"Digit1", KeyCode(0x0a)}, + {"Digit2", KeyCode(0x0b)}, + {"Digit3", KeyCode(0x0c)}, + {"Digit4", KeyCode(0x0d)}, + {"Digit5", KeyCode(0x0e)}, + {"Digit6", KeyCode(0x0f)}, + {"Digit7", KeyCode(0x10)}, + {"Digit8", KeyCode(0x11)}, + {"Digit9", KeyCode(0x12)}, + {"Digit0", KeyCode(0x13)}, + {"Enter", KeyCode(0x24)}, + {"Escape", KeyCode(0x09)}, + {"Backspace", KeyCode(0x16)}, + {"Tab", KeyCode(0x17)}, + {"Space", KeyCode(0x41)}, + {"Minus", KeyCode(0x14)}, + {"Equal", KeyCode(0x15)}, + {"BracketLeft", KeyCode(0x22)}, + {"BracketRight", KeyCode(0x23)}, + {"Backslash", KeyCode(0x33)}, + {"IntlHash", KeyCode(0x33)}, + {"Semicolon", KeyCode(0x2f)}, + {"Quote", KeyCode(0x30)}, + {"Backquote", KeyCode(0x31)}, + {"Comma", KeyCode(0x3b)}, + {"Period", KeyCode(0x3c)}, + {"Slash", KeyCode(0x3d)}, + {"CapsLock", KeyCode(0x42)}, + {"F1", KeyCode(0x43)}, + {"F2", KeyCode(0x44)}, + {"F3", KeyCode(0x45)}, + {"F4", KeyCode(0x46)}, + {"F5", KeyCode(0x47)}, + {"F6", KeyCode(0x48)}, + {"F7", KeyCode(0x49)}, + {"F8", KeyCode(0x4a)}, + {"F9", KeyCode(0x4b)}, + {"F10", KeyCode(0x4c)}, + {"F11", KeyCode(0x5f)}, + {"F12", KeyCode(0x60)}, + {"PrintScreen", KeyCode(0x6b)}, + {"ScrollLock", KeyCode(0x4e)}, + {"Pause", KeyCode(0x7f)}, + {"Insert", KeyCode(0x76)}, + {"Home", KeyCode(0x6e)}, + {"PageUp", KeyCode(0x70)}, + {"Delete", KeyCode(0x77)}, + {"End", KeyCode(0x73)}, + {"PageDown", KeyCode(0x75)}, + {"ArrowRight", KeyCode(0x72)}, + {"ArrowLeft", KeyCode(0x71)}, + {"ArrowDown", KeyCode(0x74)}, + {"ArrowUp", KeyCode(0x6f)}, + {"NumLock", KeyCode(0x4d)}, + {"NumpadDivide", KeyCode(0x6a)}, + {"NumpadMultiply", KeyCode(0x3f)}, + {"NumpadSubtract", KeyCode(0x52)}, + {"NumpadAdd", KeyCode(0x56)}, + {"NumpadEnter", KeyCode(0x68)}, + {"Numpad1", KeyCode(0x57)}, + {"Numpad2", KeyCode(0x58)}, + {"Numpad3", KeyCode(0x59)}, + {"Numpad4", KeyCode(0x53)}, + {"Numpad5", KeyCode(0x54)}, + {"Numpad6", KeyCode(0x55)}, + {"Numpad7", KeyCode(0x4f)}, + {"Numpad8", KeyCode(0x50)}, + {"Numpad9", KeyCode(0x51)}, + {"Numpad0", KeyCode(0x5a)}, + {"NumpadDecimal", KeyCode(0x5b)}, + {"IntlBackslash", KeyCode(0x5e)}, + {"ContextMenu", KeyCode(0x87)}, + {"Power", KeyCode(0x7c)}, + {"NumpadEqual", KeyCode(0x7d)}, + {"Help", KeyCode(0x92)}, + {"Again", KeyCode(0x89)}, + {"Undo", KeyCode(0x8b)}, + {"Cut", KeyCode(0x91)}, + {"Copy", KeyCode(0x8d)}, + {"Paste", KeyCode(0x8f)}, + {"Find", KeyCode(0x90)}, + {"VolumeMute", KeyCode(0x79)}, + {"VolumeUp", KeyCode(0x7b)}, + {"VolumeDown", KeyCode(0x7a)}, + {"IntlRo", KeyCode(0x61)}, + {"KanaMode", KeyCode(0x65)}, + {"IntlYen", KeyCode(0x84)}, + {"Convert", KeyCode(0x64)}, + {"NonConvert", KeyCode(0x66)}, + {"Lang1", KeyCode(0x82)}, + {"Lang2", KeyCode(0x83)}, + {"Lang3", KeyCode(0x62)}, + {"Lang4", KeyCode(0x63)}, + {"Abort", KeyCode(0x88)}, + {"NumpadParenLeft", KeyCode(0xbb)}, + {"NumpadParenRight", KeyCode(0xbc)}, + {"ControlLeft", KeyCode(0x25)}, + {"ShiftLeft", KeyCode(0x32)}, + {"AltLeft", KeyCode(0x40)}, + {"OSLeft", KeyCode(0x85)}, + {"ControlRight", KeyCode(0x69)}, + {"ShiftRight", KeyCode(0x3e)}, + {"AltRight", KeyCode(0x6c)}, + {"OSRight", KeyCode(0x86)}, + {"BrightnessUp", KeyCode(0xe9)}, + {"BrightnessDown", KeyCode(0xea)}, + {"LaunchApp2", KeyCode(0x94)}, + {"LaunchApp1", KeyCode(0xa5)}, + {"BrowserBack", KeyCode(0xa6)}, + {"BrowserForward", KeyCode(0xa7)}, + {"BrowserRefresh", KeyCode(0xb5)}, + {"BrowserFavorites", KeyCode(0xa4)}, + {"MailReply", KeyCode(0xf0)}, + {"MailForward", KeyCode(0xf1)}, + {"MailSend", KeyCode(0xef)}, + }); +} diff --git a/host-ext/nacl_src/kiwi.cc b/host-ext/nacl_src/kiwi.cc new file mode 100644 index 000000000..7df041122 --- /dev/null +++ b/host-ext/nacl_src/kiwi.cc @@ -0,0 +1,1114 @@ +/* Copyright (c) 2014 The crouton Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * This is a NaCl module, used by the crouton extension, to provide + * a display for crouton-in-a-tab. + * On one end, it communicates with the Javascript module window.js, on the + * other, it requests, via WebSocket, frames from croutonfbserver, and sends + * inputs events. + * + */ + +#include +#include + +#include "ppapi/cpp/graphics_2d.h" +#include "ppapi/cpp/image_data.h" +#include "ppapi/cpp/input_event.h" +#include "ppapi/cpp/instance.h" +#include "ppapi/cpp/message_loop.h" +#include "ppapi/cpp/module.h" +#include "ppapi/cpp/mouse_cursor.h" +#include "ppapi/cpp/point.h" +#include "ppapi/cpp/var.h" +#include "ppapi/cpp/var_array_buffer.h" +#include "ppapi/cpp/websocket.h" +#include "ppapi/utility/completion_callback_factory.h" + +/* Protocol data structures */ +#include "../../src/fbserver-proto.h" + +#include "keycode_converter.h" + +class KiwiInstance : public pp::Instance { +public: + explicit KiwiInstance(PP_Instance instance): pp::Instance(instance) {} + + virtual ~KiwiInstance() {} + + /* Registers events */ + virtual bool Init(uint32_t argc, const char* argn[], const char* argv[]) { + RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE | + PP_INPUTEVENT_CLASS_WHEEL | + PP_INPUTEVENT_CLASS_TOUCH | + PP_INPUTEVENT_CLASS_IME); + RequestFilteringInputEvents(PP_INPUTEVENT_CLASS_KEYBOARD); + + srand(pp::Module::Get()->core()->GetTime()); + + return true; + } + + /** Interface with Javascript **/ +public: + /* Handles message from Javascript + * Format: : */ + virtual void HandleMessage(const pp::Var& var_message) { + if (!var_message.is_string()) + return; + + std::string message = var_message.AsString(); + + LogMessage(2) << "message=" << message; + + size_t pos = message.find(':'); + if (pos != std::string::npos) { + std::string type = message.substr(0, pos); + if (type == "resize") { + size_t pos2 = message.find('/', pos+1); + if (pos2 != std::string::npos) { + int width = stoi(message.substr(pos+1, pos2-pos-1)); + int height = stoi(message.substr(pos2+1)); + ChangeResolution(width*scale_*view_css_scale_ + 0.5, + height*scale_*view_css_scale_ + 0.5); + } + } else if (type == "display") { + int display = stoi(message.substr(pos+1)); + if (display != display_) { + display_ = display; + SocketConnect(); + } + } else if (type == "blur" || type == "hide") { + /* Release all keys */ + SocketSend(pp::Var("Q"), false); + /* Throttle/stop refresh */ + SetTargetFPS((type == "blur") ? kBlurFPS : kHiddenFPS); + } else if (type == "focus") { + /* Force refresh and ask for next frame */ + SetTargetFPS(kFullFPS); + } else if (type == "debug") { + debug_ = stoi(message.substr(pos+1)); + } else if (type == "hidpi") { + bool newhidpi = stoi(message.substr(pos+1)); + if (newhidpi != hidpi_) { + hidpi_ = newhidpi; + InitContext(); + } + } + } + } + +private: + /* Message class that allows C++-style logging/status messages. + * The message is flushed when the object gets out of scope. */ + class Message { + public: + Message(pp::Instance* inst, const std::string& type, bool dummy): + inst_(inst) { + if (!dummy) { + out_.reset(new std::ostringstream()); + *out_ << type << ":"; + } + } + + virtual ~Message() { + if (out_) inst_->PostMessage(out_->str()); + } + + template Message& operator<<(const T& val) { + if (out_) *out_ << val; + return *this; + } + + Message(Message&& other) = default; /* Steals the unique_ptr */ + + /* The next 2 functions cannot be implemented correctly, make sure we + * cannot call them */ + Message(const Message& other) = delete; + Message& operator =(const Message&) = delete; + + private: + std::unique_ptr out_; + pp::Instance* inst_; + }; + + /* Sends a status message to Javascript */ + Message StatusMessage() { + return Message(this, "status", false); + } + + /* Sends a warning message to Javascript */ + Message WarningMessage() { + return Message(this, "warning", false); + } + + /* Sends an error message to Javascript: all errors are fatal and a + * disconnect message will be sent soon after. */ + Message ErrorMessage() { + return Message(this, "error", false); + } + + /* Sends a logging message to Javascript */ + Message LogMessage(int level) { + if (level <= debug_) { + double delta = 1000 * + (pp::Module::Get()->core()->GetTime() - lasttime_); + Message m(this, "debug", false); + m << "(" << level << ") " << (int)delta << " "; + return m; + } else { + return Message(this, "debug", true); + } + } + + /* Sends a resize message to Javascript, divide width & height by scale */ + void ResizeMessage(int width, int height, float scale) { + Message(this, "resize", false) << (int)(width/scale + 0.5) << "/" + << (int)(height/scale + 0.5); + } + + /* Sends a control message to Javascript + * Format: : */ + void ControlMessage(const std::string& type, const std::string& str) { + Message(this, type, false) << str; + } + + /** WebSocket interface **/ +private: + /* Connects to WebSocket server + * Parameter is ignored: used for callbacks */ + void SocketConnect(int32_t /*result*/ = 0) { + if (display_ < 0) { + ErrorMessage() << "SocketConnect: No display defined yet."; + return; + } + + std::ostringstream url; + url << "ws://localhost:" << (PORT_BASE + display_) << "/"; + websocket_.reset(new pp::WebSocket(this)); + websocket_->Connect(pp::Var(url.str()), NULL, 0, + callback_factory_.NewCallback( + &KiwiInstance::OnSocketConnectCompletion)); + StatusMessage() << "Connecting..."; + } + + /* Called when WebSocket is connected (or failed to connect) */ + void OnSocketConnectCompletion(int32_t result) { + if (result != PP_OK) { + retry_++; + if (retry_ < kMaxRetry) { + StatusMessage() << "Connection failed with code " << result + << ", " << retry_ << " attempt(s). Retrying..."; + pp::Module::Get()->core()->CallOnMainThread(1000, + callback_factory_.NewCallback(&KiwiInstance::SocketConnect)); + } else { + ErrorMessage() << "Connection failed (code: " << result << ")."; + ControlMessage("disconnected", "Connection failed"); + } + + return; + } + + cursor_cache_.clear(); + + SocketReceive(); + + StatusMessage() << "Connected."; + } + + /* Closes the WebSocket connection. */ + void SocketClose(const std::string& reason) { + websocket_->Close(0, pp::Var(reason), + callback_factory_.NewCallback(&KiwiInstance::OnSocketClosed)); + } + + /* Called when WebSocket is closed */ + void OnSocketClosed(int32_t result) { + StatusMessage() << "Disconnected..."; + ControlMessage("disconnected", "Socket closed"); + connected_ = false; + screen_flying_ = false; + Paint(true); + } + + /* Checks if a WebSocket request size is valid: + * - length: payload length + * - target: expected length for request type + * - type: request type, to be printed on error + */ + bool CheckSize(int length, int target, const std::string& type) { + if (length == target) + return true; + + ErrorMessage() << "Invalid " << type << " request (" << length + << " != " << target << ")."; + return false; + } + + /* Receives and handles a version request */ + bool SocketParseVersion(const char* data, int datalen) { + if (connected_) { + ErrorMessage() << "Received a version while already connected."; + return false; + } + + server_version_ = data; + + if (server_version_ != VERSION) { + /* TODO: Remove VF1 compatiblity */ + if (server_version_ == "VF1") { + WarningMessage() << "Outdated server version (" + << server_version_ << "), expecting " << VERSION + << ". Please update your chroot."; + } else { + ErrorMessage() << "Invalid server version (" + << server_version_ << "), expecting " << VERSION + << ". Please update your chroot."; + return false; + } + } + + connected_ = true; + SocketSend(pp::Var("VOK"), false); + ControlMessage("connected", "Version received"); + ChangeResolution(size_.width(), size_.height()); + /* Start requesting frames */ + OnFlush(); + return true; + } + + /* Receives and handles a screen_reply request */ + bool SocketParseScreen(const char* data, int datalen) { + if (!CheckSize(datalen, sizeof(struct screen_reply), "screen_reply")) + return false; + + struct screen_reply* reply = (struct screen_reply*)data; + if (reply->updated) { + if (!reply->shmfailed) { + Paint(false); + } else { + /* Blank the frame if shm failed */ + Paint(true); + force_refresh_ = true; + } + } else { + screen_flying_ = false; + /* No update: Ask for next frame in 1000/target_fps_ */ + if (target_fps_ > 0) { + pp::Module::Get()->core()->CallOnMainThread( + 1000/target_fps_, + callback_factory_.NewCallback( + &KiwiInstance::RequestScreen), + request_token_); + } + } + + if (reply->cursor_updated) { + /* Cursor updated: find it in cache */ + std::unordered_map::iterator it = + cursor_cache_.find(reply->cursor_serial); + if (it == cursor_cache_.end()) { + /* No cache entry, ask for data. */ + SocketSend(pp::Var("P"), false); + } else { + LogMessage(2) << "Cursor use cache for " + << reply->cursor_serial; + pp::MouseCursor::SetCursor(this, PP_MOUSECURSOR_TYPE_CUSTOM, + it->second.img, it->second.hot); + } + } + return true; + } + + /* Receives and handles a cursor_reply request */ + bool SocketParseCursor(const char* data, int datalen) { + if (datalen < sizeof(struct cursor_reply)) { + ErrorMessage() << "Invalid cursor_reply packet (" << datalen + << " < " << sizeof(struct cursor_reply) << ")."; + return false; + } + + struct cursor_reply* cursor = (struct cursor_reply*)data; + if (!CheckSize(datalen, + sizeof(struct cursor_reply) + + 4*cursor->width*cursor->height, + "cursor_reply")) + return false; + + LogMessage(0) << "Cursor " + << (cursor->width) << "/" << (cursor->height) + << " " << (cursor->xhot) << "/" << (cursor->yhot) + << " " << (cursor->cursor_serial); + + /* Scale down if needed */ + int scale = 1; + while (cursor->width/scale > 32 || cursor->height/scale > 32) + scale *= 2; + + int w = cursor->width/scale; + int h = cursor->height/scale; + pp::ImageData img(this, pp::ImageData::GetNativeImageDataFormat(), + pp::Size(w, h), true); + uint32_t* imgdata = (uint32_t*)img.data(); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + /* Nearest neighbour is least ugly */ + imgdata[y*w + x] = cursor->pixels[scale*y*scale*w + scale*x]; + } + } + pp::Point hot(cursor->xhot/scale, cursor->yhot/scale); + + cursor_cache_[cursor->cursor_serial].img = img; + cursor_cache_[cursor->cursor_serial].hot = hot; + pp::MouseCursor::SetCursor(this, PP_MOUSECURSOR_TYPE_CUSTOM, + img, hot); + return true; + } + + /* Receives and handles a resolution request */ + bool SocketParseResolution(const char* data, int datalen) { + if (!CheckSize(datalen, sizeof(struct resolution), "resolution")) + return false; + struct resolution* r = (struct resolution*)data; + /* Tell Javascript so that it can center us on the page */ + ResizeMessage(r->width, r->height, scale_*view_css_scale_); + force_refresh_ = true; + return true; + } + + /* Called when a frame is received from WebSocket server */ + void OnSocketReceiveCompletion(int32_t result) { + LogMessage(5) << "ReadCompletion: " << result << "."; + + if (result == PP_ERROR_INPROGRESS) { + LogMessage(0) << "Receive error INPROGRESS (should not happen)."; + /* We called SocketReceive too many times. */ + /* Not fatal: just wait for next call */ + return; + } else if (result != PP_OK) { + /* FIXME: Receive error is "normal" when fbserver exits. */ + LogMessage(-1) << "Receive error."; + SocketClose("Receive error."); + return; + } + + /* Get ready to receive next frame */ + pp::Module::Get()->core()->CallOnMainThread(0, + callback_factory_.NewCallback(&KiwiInstance::SocketReceive)); + + /* Convert binary/text to char* */ + const char* data; + int datalen; + std::string str; + if (receive_var_.is_array_buffer()) { + pp::VarArrayBuffer array_buffer(receive_var_); + data = static_cast(array_buffer.Map()); + datalen = array_buffer.ByteLength(); + LogMessage(data[0] == 'S' ? 3 : 2) << "receive (binary): " + << data[0]; + } else { + str = receive_var_.AsString(); + LogMessage(3) << "receive (text): " << str; + data = str.c_str(); + datalen = str.length(); + } + + if (data[0] == 'V') { /* Version */ + if (!SocketParseVersion(data, datalen)) + SocketClose("Incorrect version."); + + return; + } + + if (connected_) { + switch (data[0]) { + case 'S': /* Screen */ + if (SocketParseScreen(data, datalen)) return; + break; + case 'P': /* New cursor data is received */ + if (SocketParseCursor(data, datalen)) return; + break; + case 'R': /* Resolution request reply */ + if (SocketParseResolution(data, datalen)) return; + break; + default: + ErrorMessage() << "Invalid request. First char: " + << (int)data[0]; + /* Fall-through: disconnect. */ + } + } else { + ErrorMessage() << "Got some packet before version..."; + } + + SocketClose("Invalid payload."); + } + + /* Asks to receive the next WebSocket frame + * Parameter is ignored: used for callbacks */ + void SocketReceive(int32_t /*result*/ = 0) { + websocket_->ReceiveMessage(&receive_var_, callback_factory_.NewCallback( + &KiwiInstance::OnSocketReceiveCompletion)); + } + + /* Sends a WebSocket request, possibly flushing current mouse position + * first */ + void SocketSend(const pp::Var& var, bool flushmouse) { + if (!connected_) { + LogMessage(-1) << "SocketSend: not connected!"; + return; + } + + if (pending_mouse_move_ && flushmouse) { + struct mousemove* mm; + pp::VarArrayBuffer array_buffer(sizeof(*mm)); + mm = static_cast(array_buffer.Map()); + mm->type = 'M'; + mm->x = mouse_pos_.x(); + mm->y = mouse_pos_.y(); + array_buffer.Unmap(); + websocket_->SendMessage(array_buffer); + pending_mouse_move_ = false; + } + + websocket_->SendMessage(var); + } + + /** UI functions **/ +public: + /* Called when the NaCl module view changes (size, visibility) */ + virtual void DidChangeView(const pp::View& view) { + view_device_scale_ = view.GetDeviceScale(); + view_css_scale_ = view.GetCSSScale(); + view_rect_ = view.GetRect(); + InitContext(); + } + + /* Called when an input event is received */ + virtual bool HandleInputEvent(const pp::InputEvent& event) { + if (event.GetType() == PP_INPUTEVENT_TYPE_KEYDOWN || + event.GetType() == PP_INPUTEVENT_TYPE_KEYUP) { + pp::KeyboardInputEvent key_event(event); + + uint32_t jskeycode = key_event.GetKeyCode(); + std::string keystr = key_event.GetCode().AsString(); + bool down = event.GetType() == PP_INPUTEVENT_TYPE_KEYDOWN; + + if (jskeycode == 183) { /* Fullscreen => toggle fullscreen */ + if (!down) + ControlMessage("state", "fullscreen"); + return PP_TRUE; + } else if (jskeycode == 182) { /* Page flipper => minimize window */ + if (!down) + ControlMessage("state", "hide"); + return PP_TRUE; + } + + /* TODO: Reverse Search key translation when appropriate */ + uint8_t keycode = KeyCodeConverter::GetCode(keystr, false); + /* TODO: Remove VF1 compatibility */ + uint32_t keysym = 0; + if (server_version_ == "VF1") + keysym = KeyCodeToKeySym(jskeycode, keystr); + + LogMessage(keycode == 0 ? 0 : 1) + << "Key " << (down ? "DOWN" : "UP") + << ": C:" << keystr + << ", JSKC:" << std::hex << jskeycode + << " => KC:" << (int)keycode + << (keycode == 0 ? " (KEY UNKNOWN!)" : "") + << " searchstate:" << search_state_; + + if (keycode == 0 && keysym == 0) { + return PP_TRUE; + } + + /* We delay sending Super-L, and only "press" it on mouse clicks and + * letter keys (a-z). This way, Home (Search+Left) appears without + * modifiers (instead of Super_L+Home) */ + if (keystr == "OSLeft") { + if (down) { + search_state_ = kSearchUpFirst; + } else { + if (search_state_ == kSearchUpFirst) { + /* No other key was pressed: press+release */ + SendSearchKey(1); + SendSearchKey(0); + } else if (search_state_ == kSearchDown) { + SendSearchKey(0); + } + search_state_ = kSearchInactive; + } + return PP_TRUE; /* Ignore key */ + } + + if (jskeycode >= 65 && jskeycode <= 90) { /* letter */ + /* Search is active, send Super_L if needed */ + if (down && (search_state_ == kSearchUpFirst || + search_state_ == kSearchUp)) { + SendSearchKey(1); + search_state_ = kSearchDown; + } + } else { /* non-letter */ + /* Release Super_L if needed */ + if (search_state_ == kSearchDown) { + SendSearchKey(0); + search_state_ = kSearchUp; + } else if (search_state_ == kSearchUpFirst) { + /* Switch from UpFirst to Up */ + search_state_ = kSearchUp; + } + } + if (server_version_ == "VF1") + SendKeySym(keysym, down ? 1 : 0); + else + SendKeyCode(keycode, down ? 1 : 0); + } else if (event.GetType() == PP_INPUTEVENT_TYPE_MOUSEDOWN || + event.GetType() == PP_INPUTEVENT_TYPE_MOUSEUP || + event.GetType() == PP_INPUTEVENT_TYPE_MOUSEMOVE) { + pp::MouseInputEvent mouse_event(event); + pp::Point mouse_event_pos( + mouse_event.GetPosition().x() * scale_, + mouse_event.GetPosition().y() * scale_); + bool down = event.GetType() == PP_INPUTEVENT_TYPE_MOUSEDOWN; + + if (mouse_pos_.x() != mouse_event_pos.x() || + mouse_pos_.y() != mouse_event_pos.y()) { + pending_mouse_move_ = true; + mouse_pos_ = mouse_event_pos; + } + + Message m = LogMessage(3); + m << "Mouse " << mouse_event_pos.x() << "x" + << mouse_event_pos.y(); + + if (event.GetType() != PP_INPUTEVENT_TYPE_MOUSEMOVE) { + m << " " << (down ? "DOWN" : "UP") + << " " << (mouse_event.GetButton()); + + /* SendClick calls SocketSend, which flushes the mouse position + * before sending the click event. + * Also, Javascript button numbers are 0-based (left=0), while + * X11 numbers are 1-based (left=1). */ + SendClick(mouse_event.GetButton() + 1, down ? 1 : 0); + } + } else if (event.GetType() == PP_INPUTEVENT_TYPE_WHEEL) { + pp::WheelInputEvent wheel_event(event); + + mouse_wheel_x += wheel_event.GetDelta().x(); + mouse_wheel_y += wheel_event.GetDelta().y(); + + LogMessage(2) << "MWd " << wheel_event.GetDelta().x() << "x" + << wheel_event.GetDelta().y() + << "MWt " << wheel_event.GetTicks().x() << "x" + << wheel_event.GetTicks().y() + << "acc " << mouse_wheel_x << "x" + << mouse_wheel_y; + + while (mouse_wheel_x <= -16) { + SendClick(6, 1); SendClick(6, 0); + mouse_wheel_x += 16; + } + while (mouse_wheel_x >= 16) { + SendClick(7, 1); SendClick(7, 0); + mouse_wheel_x -= 16; + } + + while (mouse_wheel_y <= -16) { + SendClick(5, 1); SendClick(5, 0); + mouse_wheel_y += 16; + } + while (mouse_wheel_y >= 16) { + SendClick(4, 1); SendClick(4, 0); + mouse_wheel_y -= 16; + } + } else if (event.GetType() == PP_INPUTEVENT_TYPE_TOUCHSTART || + event.GetType() == PP_INPUTEVENT_TYPE_TOUCHMOVE || + event.GetType() == PP_INPUTEVENT_TYPE_TOUCHEND) { + /* FIXME: This is a very primitive implementation: + * we only handle single touch */ + + pp::TouchInputEvent touch_event(event); + + int count = touch_event.GetTouchCount( + PP_TOUCHLIST_TYPE_CHANGEDTOUCHES); + + Message m = LogMessage(2); + m << "TOUCH " << count << " "; + + /* We only care about the first touch (when count goes from 0 + * to 1), and record the id in touch_id_. */ + switch (event.GetType()) { + case PP_INPUTEVENT_TYPE_TOUCHSTART: + if (touch_count_ == 0 && count == 1) { + touch_id_ = touch_event.GetTouchByIndex( + PP_TOUCHLIST_TYPE_CHANGEDTOUCHES, 0).id(); + } + touch_count_ += count; + m << "START"; + break; + case PP_INPUTEVENT_TYPE_TOUCHMOVE: + m << "MOVE"; + break; + case PP_INPUTEVENT_TYPE_TOUCHEND: + touch_count_ -= count; + m << "END"; + break; + default: + break; + } + + /* FIXME: Is there a better way to figure out if a touch id + * is present? (GetTouchById is unhelpful and returns a TouchPoint + * full of zeros, which may well be valid...) */ + bool has_tpid = false; + for (int i = 0; i < count; i++) { + pp::TouchPoint tp = touch_event.GetTouchByIndex( + PP_TOUCHLIST_TYPE_CHANGEDTOUCHES, i); + m << "\n " << tp.id() << "//" + << tp.position().x() << "/" << tp.position().y() + << "@" << tp.pressure(); + if (tp.id() == touch_id_) + has_tpid = true; + } + + if (has_tpid) { + /* Emulate a click: only care about touch at id touch_id_ */ + pp::TouchPoint tp = touch_event.GetTouchById( + PP_TOUCHLIST_TYPE_CHANGEDTOUCHES, touch_id_); + + pp::Point touch_event_pos( + tp.position().x() * scale_, + tp.position().y() * scale_); + bool down = event.GetType() == PP_INPUTEVENT_TYPE_TOUCHSTART; + + if (mouse_pos_.x() != touch_event_pos.x() || + mouse_pos_.y() != touch_event_pos.y()) { + pending_mouse_move_ = true; + mouse_pos_ = touch_event_pos; + } + + m << "\nEmulated mouse "; + + if (event.GetType() != PP_INPUTEVENT_TYPE_TOUCHMOVE) { + m << (down ? "DOWN" : "UP"); + SendClick(1, down ? 1 : 0); + } else { + m << "MOVE"; + } + + m << " " << touch_event_pos.x() << "/" << touch_event_pos.y(); + } + } else if (event.GetType() == PP_INPUTEVENT_TYPE_IME_TEXT) { + /* FIXME: There are other IME event types... */ + pp::IMEInputEvent ime_event(event); + + /* FIXME: Do something with these events. We probably need to "type" + * the letters one by one... */ + + LogMessage(0) << "IME TEXT: " << ime_event.GetText().AsString(); + } + + return PP_TRUE; + } + +private: + /* Initializes Graphics context */ + void InitContext() { + if (view_rect_.width() <= 0 || view_rect_.height() <= 0) + return; + + scale_ = hidpi_ ? view_device_scale_ : 1.0f; + pp::Size new_size = pp::Size(view_rect_.width() * scale_, + view_rect_.height() * scale_); + + LogMessage(0) << "InitContext " + << new_size.width() << "x" << new_size.height() + << "s" << scale_ + << " (device scale: " << view_device_scale_ + << ", zoom level: " << view_css_scale_ << ")"; + + const bool kIsAlwaysOpaque = true; + context_ = pp::Graphics2D(this, new_size, kIsAlwaysOpaque); + context_.SetScale(1.0f / scale_); + if (!BindGraphics(context_)) { + LogMessage(0) << "Unable to bind 2d context!"; + context_ = pp::Graphics2D(); + return; + } + + size_ = new_size; + force_refresh_ = true; + } + + /* Requests the server for a resolution change. */ + void ChangeResolution(int width, int height) { + LogMessage(1) << "Asked for resolution " << width << "x" << height; + + if (connected_) { + struct resolution* r; + pp::VarArrayBuffer array_buffer(sizeof(*r)); + r = static_cast(array_buffer.Map()); + r->type = 'R'; + r->width = width; + r->height = height; + array_buffer.Unmap(); + SocketSend(array_buffer, false); + } else { /* Just assume we can take up the space */ + ResizeMessage(width, height, scale_*view_css_scale_); + } + } + + /* Converts "IE"/JavaScript keycode to X11 KeySym. + * See http://unixpapa.com/js/key.html + * TODO: Drop support for VF1 */ + uint32_t KeyCodeToKeySym(uint32_t keycode, const std::string& code) { + if (keycode >= 65 && keycode <= 90) /* A to Z */ + return keycode + 32; + if (keycode >= 48 && keycode <= 57) /* 0 to 9 */ + return keycode; + if (keycode >= 96 && keycode <= 105) /* KP 0 to 9 */ + return keycode - 96 + 0xffb0; + if (keycode >= 112 && keycode <= 123) /* F1-F12 */ + return keycode - 112 + 0xffbe; + + switch (keycode) { + case 8: return 0xff08; // backspace + case 9: return 0xff09; // tab + case 12: return 0xff9d; // num 5 + case 13: return 0xff0d; // enter + case 16: // shift + if (code == "ShiftRight") return 0xffe2; + return 0xffe1; // (left) + case 17: // control + if (code == "ControlRight") return 0xffe4; + return 0xffe3; // (left) + case 18: // alt + if (code == "AltRight") return 0xffea; + return 0xffe9; // (left) + case 19: return 0xff13; // pause + case 20: return 0; // caps lock. FIXME: reenable (0xffe5) + case 27: return 0xff1b; // esc + case 32: return 0x20; // space + case 33: return 0xff55; // page up + case 34: return 0xff56; // page down + case 35: return 0xff57; // end + case 36: return 0xff50; // home + case 37: return 0xff51; // left + case 38: return 0xff52; // top + case 39: return 0xff53; // right + case 40: return 0xff54; // bottom + case 42: return 0xff61; // print screen + case 45: return 0xff63; // insert + case 46: return 0xffff; // delete + case 91: return 0xffeb; // super + case 106: return 0xffaa; // num multiply + case 107: return 0xffab; // num plus + case 109: return 0xffad; // num minus + case 110: return 0xffae; // num dot + case 111: return 0xffaf; // num divide + case 144: return 0xff7f; // num lock + case 145: return 0xff14; // scroll lock + case 151: return 0x1008ff95; // WLAN + case 166: return 0x1008ff26; // back + case 167: return 0x1008ff27; // forward + case 168: return 0x1008ff73; // refresh + case 182: return 0x1008ff51; // page flipper ("F5") + case 183: return 0x1008ff59; // fullscreen/display + case 186: return 0x3b; // ; + case 187: return 0x3d; // = + case 188: return 0x2c; // , + case 189: return 0x2d; // - + case 190: return 0x2e; // . + case 191: return 0x2f; // / + case 192: return 0x60; // ` + case 219: return 0x5b; // [ + case 220: return 0x5c; // '\' + case 221: return 0x5d; // ] + case 222: return 0x27; // ' + case 229: return 0; // dead key ('`~). FIXME: no way of knowing which + } + + return 0x00; + } + + /* Changes the target FPS: avoid unecessary refreshes to save CPU */ + void SetTargetFPS(int new_target_fps) { + /* When increasing the fps, immediately ask for a frame, and force + * refresh the display (we probably just gained focus). */ + if (new_target_fps > target_fps_) { + force_refresh_ = true; + RequestScreen(request_token_); + } + target_fps_ = new_target_fps; + } + + /* Sends a mouse click. + * - button is a X11 button number (e.g. 1 is left click) + * SocketSend flushes the mouse position before the click is sent. */ + void SendClick(int button, int down) { + struct mouseclick* mc; + + if (down && (search_state_ == kSearchUpFirst || + search_state_ == kSearchUp)) { + SendSearchKey(1); + search_state_ = kSearchDown; + } + + pp::VarArrayBuffer array_buffer(sizeof(*mc)); + mc = static_cast(array_buffer.Map()); + mc->type = 'C'; + mc->down = down; + mc->button = button; + array_buffer.Unmap(); + SocketSend(array_buffer, true); + + /* That means we have focus */ + SetTargetFPS(kFullFPS); + } + + void SendSearchKey(int down) { + /* TODO: Drop support for VF1 */ + if (server_version_ == "VF1") + SendKeySym(0xffeb, down); + else + SendKeyCode(KeyCodeConverter::GetCode("OSLeft", false), down); + } + + /* Sends a keysym (VF1) */ + /* TODO: Drop support for VF1 */ + void SendKeySym(uint32_t keysym, int down) { + struct key_vf1* k; + pp::VarArrayBuffer array_buffer(sizeof(*k)); + k = static_cast(array_buffer.Map()); + k->type = 'K'; + k->down = down; + k->keysym = keysym; + array_buffer.Unmap(); + SocketSend(array_buffer, true); + + /* That means we have focus */ + SetTargetFPS(kFullFPS); + } + + /* Sends a keycode */ + void SendKeyCode(uint8_t keycode, int down) { + struct key* k; + pp::VarArrayBuffer array_buffer(sizeof(*k)); + k = static_cast(array_buffer.Map()); + k->type = 'K'; + k->down = down; + k->keycode = keycode; + array_buffer.Unmap(); + SocketSend(array_buffer, true); + + /* That means we have focus */ + SetTargetFPS(kFullFPS); + } + + /* Requests the next framebuffer grab. + * The parameter is a token that must be equal to request_token_. + * This makes sure only one screen requests is waiting at one time + * (e.g. when changing frame rate), since we have no way of cancelling + * scheduled callbacks. */ + void RequestScreen(int32_t token) { + LogMessage(3) << "OnWaitEnd " << token << "/" << request_token_; + + if (!connected_) { + LogMessage(-1) << "!connected"; + return; + } + + /* Check that this request is up to date, and that no other + * request is flying */ + if (token != request_token_ || screen_flying_) { + LogMessage(2) << "Old token, or screen flying..."; + return; + } + screen_flying_ = true; + request_token_++; + + struct screen* s; + pp::VarArrayBuffer array_buffer(sizeof(*s)); + s = static_cast(array_buffer.Map()); + + s->type = 'S'; + s->shm = 1; + s->refresh = force_refresh_; + force_refresh_ = false; + s->width = image_data_.size().width(); + s->height = image_data_.size().height(); + s->paddr = (uint64_t)image_data_.data(); + uint64_t sig = ((uint64_t)rand() << 32) ^ rand(); + uint64_t* data = static_cast(image_data_.data()); + *data = sig; + s->sig = sig; + + array_buffer.Unmap(); + SocketSend(array_buffer, true); + } + + /* Called when the last frame was displayed (Vsync-ed): allocates next + * buffer and requests next frame. + * Parameter is ignored: used for callbacks */ + void OnFlush(int32_t /*result*/ = 0) { + PP_Time time_ = pp::Module::Get()->core()->GetTime(); + PP_Time deltat = time_-lasttime_; + + double delay = (target_fps_>0) ? (1.0/target_fps_ - deltat) : INFINITY; + + double cfps = deltat > 0 ? 1.0/deltat : 1000; + lasttime_ = time_; + k_++; + + avgfps_ = 0.9*avgfps_ + 0.1*cfps; + if ((k_ % ((int)avgfps_+1)) == 0 || debug_ >= 1) { + LogMessage(0) << "fps: " << (int)(cfps+0.5) + << " (" << (int)(avgfps_+0.5) << ")" + << " delay: " << (int)(delay*1000) + << " deltat: " << (int)(deltat*1000) + << " target fps: " << (int)(target_fps_) + << " " << size_.width() << "x" << size_.height(); + } + + LogMessage(5) << "OnFlush"; + + screen_flying_ = false; + + /* Allocate next image. If size_ is the same, the previous buffer will + * be reused. */ + PP_ImageDataFormat format = pp::ImageData::GetNativeImageDataFormat(); + image_data_ = pp::ImageData(this, format, size_, false); + + /* Request for next frame */ + if (isinf(delay)) { + return; + } else if (delay >= 0) { + pp::Module::Get()->core()->CallOnMainThread( + delay*1000, + callback_factory_.NewCallback(&KiwiInstance::RequestScreen), + request_token_); + } else { + RequestScreen(request_token_); + } + } + + /* Paints the frame. In our context, simply replace the front buffer + * content with image_data_. */ + void Paint(bool blank) { + if (context_.is_null()) { + /* The current Graphics2D context is null, so updating and rendering + * is pointless. */ + flush_context_ = context_; + return; + } + + if (blank) { + uint32_t* data = (uint32_t*)image_data_.data(); + int size = image_data_.size().width()*image_data_.size().height(); + for (int i = 0; i < size; i++) { + if (debug_ == 0) + data[i] = 0xFF000000; + else + data[i] = 0xFF800000 + i; + } + } + + /* Using Graphics2D::ReplaceContents is the fastest way to update the + * entire canvas every frame. */ + context_.ReplaceContents(&image_data_); + + /* Store a reference to the context that is being flushed; this ensures + * the callback is called, even if context_ changes before the flush + * completes. */ + flush_context_ = context_; + context_.Flush( + callback_factory_.NewCallback(&KiwiInstance::OnFlush)); + } + +private: + /* Constants */ + const int kFullFPS = 30; /* Maximum fps */ + const int kBlurFPS = 5; /* fps when window is possibly hidden */ + const int kHiddenFPS = 0; /* fps when window is hidden */ + + const int kMaxRetry = 3; /* Maximum number of connection attempts */ + + /* Class members */ + pp::CompletionCallbackFactory callback_factory_{this}; + pp::Graphics2D context_; + pp::Graphics2D flush_context_; + pp::Rect view_rect_; + float view_device_scale_ = 1.0f; + float view_css_scale_ = 1.0f; + pp::Size size_; + float scale_ = 1.0f; + + pp::ImageData image_data_; + int k_ = 0; + + std::unique_ptr websocket_; + int retry_ = 0; + bool connected_ = false; + std::string server_version_ = ""; + bool screen_flying_ = false; + pp::Var receive_var_; + int target_fps_ = kFullFPS; + int request_token_ = 0; + bool force_refresh_ = false; + + bool pending_mouse_move_ = false; + pp::Point mouse_pos_{-1, -1}; + /* Mouse wheel accumulators */ + int mouse_wheel_x = 0; + int mouse_wheel_y = 0; + + /* Search key state: + * - active/inactive: Key is pushed on Chromium OS side + * - down/up: Key is pushed on xiwi side */ + enum { + kSearchInactive, /* Inactive (up) */ + kSearchUpFirst, /* Active, up, no other key (yet) */ + kSearchUp, /* Active, up */ + kSearchDown /* Active, down */ + } search_state_ = kSearchInactive; + + /* Touch */ + int touch_count_; /* Number of points currently pressed */ + int touch_id_; /* First touch id */ + + /* Performance metrics */ + PP_Time lasttime_; + double avgfps_ = 0.0; + + /* Cursor cache */ + class Cursor { +public: + pp::ImageData img; + pp::Point hot; + }; + std::unordered_map cursor_cache_; + + /* Display to connect to */ + int display_ = -1; + int debug_ = 0; + bool hidpi_ = false; +}; + +class KiwiModule : public pp::Module { +public: + KiwiModule() : pp::Module() {} + virtual ~KiwiModule() {} + + virtual pp::Instance* CreateInstance(PP_Instance instance) { + return new KiwiInstance(instance); + } +}; + +namespace pp { + +Module* CreateModule() { + return new KiwiModule(); +} + +} /* namespace pp */ diff --git a/installer/functions b/installer/functions index 98c565af9..9f24ca2c7 100644 --- a/installer/functions +++ b/installer/functions @@ -68,6 +68,54 @@ undotrap() { settrap "$TRAP" } +# Works mostly like built-in getopts but silently coalesces positional arguments. +# Does not take parameters. Set getopts_string prior to calling. +# Sets getopts_var and getopts_arg. +# $@ will be left with the positional arguments, so you should NOT shift at all. +# In bash, enables alias expansion, but that shouldn't impact anything. +shopt -q -s expand_aliases 2>/dev/null || true +# Running count of the number of positional arguments +# We're done processing if all of the remaining arguments are positional. +getopts_npos=0 +getopts_dashdash='' +alias getopts_nextarg='getopts_ret=1 + while [ "$#" -gt "$getopts_npos" ]; do + if [ -z "$getopts_dashdash" ] && getopts "$getopts_string" getopts_var; then + if [ "$(($#+1-OPTIND))" -lt "$getopts_npos" ]; then + # Bad parameter usage ate a positional argument. + # Generate the proper error message by abusing getopts. + set -- "-$getopts_var" + getopts "$getopts_var:" getopts_var + shift + fi + getopts_arg="$OPTARG" + getopts_ret=0 + # Avoid -- confusion by shifting if OPTARG is set + if [ -n "$OPTARG" ]; then + shift "$((OPTIND-1))" + OPTIND=1 + fi + break + fi + # Do not let getopts consume a -- + if [ "$OPTIND" -gt 1 ]; then + shift "$((OPTIND-2))" + if [ "$1" != "--" ]; then + shift + fi + fi + OPTIND=1 + if [ -z "$getopts_dashdash" -a "$1" = "--" ]; then + # Still need to loop through to fix the ordering + getopts_dashdash=y + else + set -- "$@" "$1" + getopts_npos="$((getopts_npos+1))" + fi + shift + done + [ "$getopts_ret" = 0 ]' + # Compares $RELEASE to the specified releases, assuming $DISTRODIR/releases is # sorted oldest to newest. Every two parameters are considered criteria that are # ORed together. The first parameter is the comparator, as provided to "test". @@ -152,3 +200,84 @@ validate_name() { fi return 0 } + +# Returns the mountpoint a path is on. The path doesn't need to exist. +# $1: the path to check +# outputs on stdout +getmountpoint() { + mp="`readlink -m -- "$1"`" + while ! stat -c '%m' "${mp:-/}" 2>/dev/null; do + mp="${mp%/*}" + done +} + +# Websocket interface +PIPEDIR='/tmp/crouton-ext' +CRIATDISPLAY="$PIPEDIR/kiwi-display" +CROUTONLOCKDIR='/tmp/crouton-lock' + +# Write a command to croutonwebsocket, and read back response +websocketcommand() { + # Check that $PIPEDIR and the FIFO pipes exist + if ! [ -d "$PIPEDIR" -a -p "$PIPEDIR/in" -a -p "$PIPEDIR/out" ]; then + echo "EError $PIPEDIR/in or $PIPEDIR/out are not pipes." + exit 0 + fi + + if ! timeout 3 \ + sh -c "flock 5; cat > '$PIPEDIR/in'; + cat '$PIPEDIR/out'" 5>"$PIPEDIR/lock"; then + echo "EError timeout" + fi +} + +# Find the root device +# Sets: +# - $ROOTDEVICE as the root device (e.g. /dev/sda or /dev/mmcblk0) +# - $ROOTDEVICEPREFIX as a prefix for partitions (/dev/sda, /dev/mmcblk0p) +findrootdevice() { + ROOTDEVICE="`rootdev -d -s`" + + if [ -z "$ROOTDEVICE" ]; then + error 1 "Cannot find root device." + fi + + if [ ! -b "$ROOTDEVICE" ]; then + error 1 "$ROOTDEVICE is not a block device." + fi + + # If $ROOTDEVICE ends with a number (e.g. mmcblk0), partitions are named + # ${ROOTDEVICE}pX (e.g. mmcblk0p1). If not (e.g. sda), they are named + # ${ROOTDEVICE}X (e.g. sda1). + ROOTDEVICEPREFIX="$ROOTDEVICE" + if [ "${ROOTDEVICE%[0-9]}" != "$ROOTDEVICE" ]; then + ROOTDEVICEPREFIX="${ROOTDEVICE}p" + fi +} + +# Try to mount the CROUTON partition, if it exists, on $MOUNTPOINT. +mountcrouton() { + if [ -z "$ROOTDEVICE" ]; then + findrootdevice + fi + + local croutonpart="`cgpt find -n -l CROUTON "$ROOTDEVICE"`" + if [ -z "$croutonpart" ]; then + return 1 + fi + + # Check if crouton is mounted already + if grep -q "^${ROOTDEVICEPREFIX}$croutonpart " /proc/mounts; then + # If mounted, it must be mounted to $mountpoint + + if ! grep -q "^${ROOTDEVICEPREFIX}$croutonpart $MOUNTPOINT " \ + /proc/mounts; then + error 1 "Error: CROUTON partition is not mounted on $MOUNTPOINT." + fi + else + mkdir -p "$MOUNTPOINT" || error 1 "Cannot create $MOUNTPOINT." + mount "${ROOTDEVICEPREFIX}$croutonpart" "$MOUNTPOINT" || \ + error 1 "Cannot mount $MOUNTPOINT" + fi + return 0 +} diff --git a/installer/main.sh b/installer/main.sh index b513935ef..4292ec640 100755 --- a/installer/main.sh +++ b/installer/main.sh @@ -27,6 +27,7 @@ NAME='' PREFIX='/usr/local' PREFIXSET='' CHROOTSLINK='/mnt/stateful_partition/crouton/chroots' +MOUNTPOINT='/var/crouton' PROXY='unspecified' RELEASE='' RESTORE='' @@ -41,6 +42,7 @@ UPDATEIGNOREEXISTING='' USAGE="$APPLICATION [options] -t targets $APPLICATION [options] -f backup_tarball $APPLICATION [options] -d -f bootstrap_tarball +$APPLICATION -S [partition_options] Constructs a chroot for running a more standard userspace alongside Chromium OS. @@ -51,6 +53,11 @@ If run with -d, a bootstrap tarball is created to speed up chroot creation in the future. You can use bootstrap tarballs generated this way by passing them to -f the next time you create a chroot with the same architecture and release. +If run with -S, a separate partition is created for crouton, immune from +accidental wiping when switching back and forth between developer/normal mode, +as well as powerwashes. +Run $APPLICATION -S -h for additional help. + $APPLICATION must be run as root unless -d is specified AND fakeroot is installed AND /tmp is mounted exec and dev. @@ -60,7 +67,9 @@ Options: -a ARCH The architecture to prepare a new chroot or bootstrap for. Default: autodetected for the current chroot or system. -b Restore crouton scripts in PREFIX/bin, as required by the - chroots currently installed in PREFIX/chroots. + chroots currently installed in PREFIX/chroots (or + $MOUNTPOINT/chroots if -p is not specified, and the crouton + partition exists). -d Downloads the bootstrap tarball but does not prepare the chroot. -e Encrypt the chroot with ecryptfs using a passphrase. If specified twice, prompt to change the encryption passphrase. @@ -81,7 +90,9 @@ Options: -p PREFIX The root directory in which to install the bin and chroot subdirectories and data. Default: $PREFIX, with $PREFIX/chroots linked to - $CHROOTSLINK. + $CHROOTSLINK, unless the crouton partition + is detected, in which case the partition is mounted, and chroots + are installed in $MOUNTPOINT. -P PROXY Set an HTTP proxy for the chroot; effectively sets http_proxy. Specify an empty string to remove a proxy when updating. -r RELEASE Name of the distribution release. Default: $DEFAULTRELEASE, @@ -107,6 +118,14 @@ secure as the passphrases you assign to them." # Targets specified with -t will be installed, but not recorded # for future updates. +# Pass parameters to mkpart.sh +if [ "$1" = "-S" ]; then + shift + APPLICATION="$APPLICATION -S" + . "$SCRIPTDIR/installer/mkpart.sh" + exit 0 +fi + # Common functions . "$SCRIPTDIR/installer/functions" @@ -122,7 +141,7 @@ while getopts 'a:bdef:k:m:M:n:p:P:r:s:t:T:uUV' f; do m) MIRROR="$OPTARG";; M) MIRROR2="$OPTARG";; n) NAME="$OPTARG";; - p) PREFIX="`readlink -m "$OPTARG"`"; PREFIXSET='y';; + p) PREFIX="`readlink -m -- "$OPTARG"`"; PREFIXSET='y';; P) PROXY="$OPTARG";; r) RELEASE="$OPTARG";; t) TARGETS="$TARGETS${TARGETS:+","}$OPTARG";; @@ -309,7 +328,7 @@ if [ -z "$DOWNLOADONLY" ]; then exit 2 elif [ ! "${TARGET%common}" = "$TARGET" ] || \ [ ! -r "$TARGETSDIR/$TARGET" ] || \ - ! (TARGETNOINSTALL='y'; . "$TARGETSDIR/$TARGET"); then + ! (TARGETNOINSTALL="${UPDATE:-c}"; . "$TARGETSDIR/$TARGET"); then error 2 "Invalid target \"$TARGET\"." fi done @@ -364,7 +383,16 @@ addtrap "stty echo 2>/dev/null" # Determine directories BIN="$PREFIX/bin" -CHROOTS="$PREFIX/chroots" + +# Try to mount the crouton partition, if it exists, and no prefix is set. +if [ "$USER" = root -o "$UID" = 0 ] && \ + [ -z "$PREFIXSET" ] && mountcrouton "$MOUNTPOINT"; then + # If the crouton partition exists, install binaries in /usr/local an + # chroots in the partition + CHROOTS="$MOUNTPOINT/chroots" +else + CHROOTS="$PREFIX/chroots" +fi if [ -z "$RESTOREBIN" ]; then # Fix NAME if it was not specified. @@ -381,27 +409,21 @@ if [ -z "$RESTOREBIN$DOWNLOADONLY" ]; then error 2 "Invalid chroot name '$NAME'." fi - # If no prefix is set, check that /usr/local/chroots ($CHROOTS) is a + # If no prefix is set and the crouton partition is not being used, check that /usr/local/chroots ($CHROOTS) is a # symbolic link to /mnt/stateful_partition/crouton/chroots ($CHROOTSLINK) - if [ -z "$PREFIXSET" -a ! -h "$CHROOTS" ]; then + # /mnt/stateful_partition/dev_image is bind-mounted to /usr/local, so mv + # does not understand that they are on the same filesystem + # Instead, use the direct path, and confirm that they're actually the same + # to catch situations where things are bind-mounted over /usr/local + truechroots="/mnt/stateful_partition/dev_image/chroots" + if [ -z "$PREFIXSET" -a ! -h "$CHROOTS" -a "$CHROOTS" != "$MOUNTPOINT/chroots" ] \ + && [ "$CHROOTS" -ef "$truechroots" ]; then # Detect if chroots are left in the old chroots directory, and move them # to the new directory. if [ -e "$CHROOTS" ] && ! rmdir "$CHROOTS" 2>/dev/null; then echo \ "Migrating data from legacy chroots directory $CHROOTS to $CHROOTSLINK..." 1>&2 - # /mnt/stateful_partition/dev_image is bind-mounted to /usr/local, - # so mv does not understand that they are on the same filesystem - # Instead, use the direct path. - truechroots="/mnt/stateful_partition/dev_image/chroots" - - # Be extra careful and check both files are indeed the same - if [ "`stat -c '%i' "$truechroots"`" != \ - "`stat -c '%i' "$CHROOTS"`" ]; then - error 1 \ -"$truechroots and $CHROOTS are not the same file as expected." - fi - # Check that CHROOTSLINK is empty if [ -e "$CHROOTSLINK" ] && ! rmdir "$CHROOTSLINK" 2>/dev/null; then error 1 \ @@ -446,6 +468,11 @@ Valid chroots: `sh "$HOSTBINDIR/edit-chroot" -c "$CHROOTS" -a`" fi + # Chroot must be located on an ext filesystem + if df -T "`getmountpoint "$CHROOT"`" | awk '$2~"^ext"{exit 1}'; then + error 1 "$CHROOTSRC is not an ext filesystem." + fi + # Restore the chroot now if [ -n "$RESTORE" ]; then sh "$HOSTBINDIR/edit-chroot" -r -f "$TARBALL" -c "$CHROOTS" -- "$NAME" @@ -455,6 +482,11 @@ Valid chroots: CHROOT="`sh "$HOSTBINDIR/mount-chroot" -k "$KEYFILE" \ $create $ENCRYPT -p -c "$CHROOTS" -- "$NAME"`" + # Remove the directory if bootstrapping fails. Also delete if the only file + # there is .ecryptfs (valid chroots have far more than 1 file) + addtrap "[ \"\`ls -a '$CHROOTS/$NAME' 2>/dev/null | wc -l\`\" -le 3 ] \ + && rm -rf '$CHROOTS/$NAME'" + # Auto-unmount the chroot when the script exits addtrap "sh '$HOSTBINDIR/unmount-chroot' -y -c '$CHROOTS' -- '$NAME' 2>/dev/null" @@ -686,11 +718,26 @@ else fi echo -n '' > "$TARGETDEDUPFILE" +# Check if a target has defined PROVIDES, if we are not restoring host-bin. +if [ ! -n "$RESTOREHOSTBIN" ]; then + # Create temporary file to list PROVIDES=TARGET. + PROVIDESFILE="`mktemp --tmpdir=/tmp "$APPLICATION-provides.XXX"`" + addtrap "rm -f '$PROVIDESFILE'" + t="${TARGETS%,}," + while [ -n "$t" ]; do + TARGET="${t%%,*}" + t="${t#*,}" + if [ -n "$TARGET" ]; then + (TARGETNOINSTALL="p"; . "$TARGETSDIR/$TARGET") + fi + done +fi + # Run each target, appending stdout to the prepare script. unset SIMULATE TARGETNOINSTALL="$RESTOREHOSTBIN" if [ -n "$TARGETFILE" ]; then - TARGET="`readlink -f "$TARGETFILE"`" + TARGET="`readlink -f -- "$TARGETFILE"`" (. "$TARGET") >> "$PREPARE" fi t="${TARGETS%,},post-common," diff --git a/installer/mkpart.sh b/installer/mkpart.sh new file mode 100755 index 000000000..25cf76a02 --- /dev/null +++ b/installer/mkpart.sh @@ -0,0 +1,491 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +set -e + +if [ -z "$APPLICATION" -o -z "$PREFIX" ]; then + echo "Please do not call mkpart.sh directly: use main.sh -S." 1>&2 + exit 2 +fi + +# Partition number to use +CROUTONPARTNUMBER=13 +CROUTONPARTNUMBERSET='' +# Minimum stateful partition size in MB +MINIMUMSTATEFULSIZE=1500 +# Give at least 10%, and 200 MB, headroom in the current stateful partition +MINIMUMSTATEFULMARGINPERCENT=10 +MINIMUMSTATEFULMARGINABS=200 +# Minimum crouton partition size in MB +MINIMUMCROUTONSIZE=500 +# Stateful partition size +STATEFULSIZE=2500 +STATEFULSIZESET='' +# Crouton partition size +CROUTONSIZE='' +# Which VT to switch to +LOGVT='9' +NOCREATE='' +DELETE='' + +USAGE="$APPLICATION [-s size|-c size|-d|-x] [-i number] + +Create a separate partition for crouton, immune from accidental wiping when +switching back and forth to developer/normal mode. + +This script needs to log the user out, and reboot the system. Make sure no +unsaved document/form is left unsaved. + +Under normal circumstances, the stateful partition will not be damaged. +However, it is recommended to backup data as required (Downloads folder, +existing chroots in /usr/local). + +$APPLICATION must be run as root. + +It is highly recommended to run this from a crosh shell (Ctrl+Alt+T), not VT2. + +Basic options: + -s size Set the size in megabytes of the stateful partition, the rest + being allocated to crouton. + Default: $STATEFULSIZE MB + Minimum allowed size: $MINIMUMSTATEFULSIZE MB + -c size Set the size in megabytes of the crouton partition. + -d Delete a partition, and reset the stateful partition to its + maximum size. The partition to remove is autodetected using its + label (CROUTON), but it can be specified using '-i number'. + WARNING: This wipes the content of the crouton partition. + +Options for power users only: + -i number The partition number to use for crouton. This script will fail + if the partition exists and is larger than a single sector. + Default: $CROUTONPARTNUMBER + -x Do not create the new partition: only switch off all services, + switch to VT2, so that the partition layout can be edited + manually. +" + +# Common functions +. "$SCRIPTDIR/installer/functions" + +# Process arguments +while getopts 'c:dhi:s:x' f; do + case "$f" in + c) CROUTONSIZE="$OPTARG";; + d) DELETE='y';; + h) error 2 "$USAGE";; + i) CROUTONPARTNUMBER="$OPTARG"; CROUTONPARTNUMBERSET='y';; + s) STATEFULSIZE="$OPTARG"; STATEFULSIZESET='y';; + x) NOCREATE='y';; + \?) error 2 "$USAGE";; + esac +done +shift "$((OPTIND-1))" + +if [ ! "$USER" = root -a ! "$UID" = 0 ]; then + error 2 "$APPLICATION must be run as root." +fi + +test="${STATEFULSIZESET}${CROUTONSIZE:+y}${DELETE}${NOCREATE}" +if [ "${#test}" -gt 1 ]; then + error 2 "Only one of -c, -d, -s and -x can be set. +$USAGE" +fi + +if ! [ "$CROUTONPARTNUMBER" -gt 0 ] 2>/dev/null \ + || [ "$CROUTONPARTNUMBER" -gt 128 ]; then + error 2 "Partition number $CROUTONPARTNUMBER is not valid." +fi + +if ! [ "$STATEFULSIZE" -gt 0 ] 2>/dev/null; then + error 2 "Partition size $STATEFULSIZE is not valid." +fi + +if [ -n "$CROUTONSIZE" ] && ! [ "$CROUTONSIZE" -gt 0 ] 2>/dev/null; then + error 2 "Partition size $CROUTONSIZE is not valid." +fi + +# Avoid kernel panics due to slow I/O +disablehungtask + +# Set ROOTDEVICE +findrootdevice + +updatestatus="`update_engine_client --status 2>/dev/null | \ + sed -n "s/CURRENT_OP=\(.*\)$/\1/p"`" + +if [ "$updatestatus" != "UPDATE_STATUS_IDLE" ]; then + if [ "$updatestatus" = "UPDATE_STATUS_UPDATED_NEED_REBOOT" ]; then + error 1 "\ +A Chromium OS update is currently pending, please restart your Chromebook, +then launch this script again." + else + error 1 "\ +Chromium OS is currently being updated (status: $updatestatus). +Please wait for the update to complete (it can be monitored with +'update_engine_client --update'), then restart your Chromebook, and launch this +script again." + fi +fi + +# Restore stateful partition to its original size +if [ -n "$DELETE" ]; then + if [ -z "$CROUTONPARTNUMBERSET" ]; then + CROUTONPARTNUMBER="`cgpt find -n -l CROUTON "$ROOTDEVICE" || true`" + + if [ -z "$CROUTONPARTNUMBER" ]; then + error 1 "Cannot find CROUTON partition." + fi + fi + + name="`cgpt show -i "$CROUTONPARTNUMBER" -l "$ROOTDEVICE"`" + name=${name:-unknown} + + # Unit: sectors (512 bytes) + statestart="`cgpt show -i 1 -b "$ROOTDEVICE"`" + statesize="`cgpt show -i 1 -s "$ROOTDEVICE"`" + croutonstart="`cgpt show -i "$CROUTONPARTNUMBER" -b "$ROOTDEVICE"`" + croutonsize="`cgpt show -i "$CROUTONPARTNUMBER" -s "$ROOTDEVICE"`" + newstatesize="$((statesize+croutonsize))" + + echo "Stateful partition:" + cgpt show -i 1 "$ROOTDEVICE" + echo "'$name' partition:" + cgpt show -i "$CROUTONPARTNUMBER" "$ROOTDEVICE" + + if [ "$((statestart+statesize))" -ne "$croutonstart" ]; then + error 1 "Error: stateful and '$name' partitions are not contiguous." + fi + + echo -n " +WARNING: Removing '$name' partition: ALL DATA ON THAT PARTITION WILL BE LOST. + +Type 'delete' if you are sure that you want to do that: " + if [ -t 0 ]; then + read -r line + else + line="$CROUTON_MKPART_DELETE" + echo "$line" + fi + if [ "$line" != "delete" ]; then + error 2 "Aborting..." + fi + + # This could be done without reboot, as resize2fs can be done online. + # However, Chromium OS cannot re-read the partition table without reboot. +elif [ -z "$NOCREATE" ]; then + rootc="`cgpt find -n -l ROOT-C "$ROOTDEVICE"`" + if [ "`cgpt show -i "$rootc" -s "$ROOTDEVICE"`" -gt 1 ]; then + echo -n "ROOT-C is not empty (did you install ChrUbuntu?). +Using both ChrUbuntu and crouton partition is not recommended, especially if +your total storage space is only 16GB, as the space for each system (Chromium OS +stateful partition, crouton and ChrUbuntu) will be very limited. +Do you still want to continue? [y/N] " 1>&2 + read -r response + if [ "${response#[Yy]}" = "$response" ]; then + exit 1 + fi + fi + + if [ "`cgpt show -i "$CROUTONPARTNUMBER" -s "$ROOTDEVICE"`" -gt 1 ]; then + echo "Partition $CROUTONPARTNUMBER already exists:" 1>&2 + cgpt show -i "$CROUTONPARTNUMBER" "$ROOTDEVICE" + exit 1 + fi + + if cgpt find -n -l CROUTON "$ROOTDEVICE" > /dev/null; then + error 1 "CROUTON partition already exists." + fi + + if [ -n "`find $PREFIX/chroots/* \ + -type d -maxdepth 0 2>/dev/null || true`" ]; then + echo -n "$PREFIX/chroots is not empty. +It is recommended that you follow the migration guide to transfer existing +chroots to the new partition. +Do you still want to continue? [y/N] " 1>&2 + read -r response + if [ "${response#[Yy]}" = "$response" ]; then + exit 1 + fi + fi + + # All GPT sizes/offsets are expressed in sectors (512 bytes) + statestart="`cgpt show -i 1 -b "$ROOTDEVICE"`" + statesize="`cgpt show -i 1 -s "$ROOTDEVICE"`" + + if [ -n "$CROUTONSIZE" ]; then + STATEFULSIZE="$((statesize/(1024*2)-CROUTONSIZE))" + fi + + # Make sure the stateful partition can be resized to the requested size + # Unit: KiB + statefulallocated="`df -P -k ${ROOTDEVICEPREFIX}1 \ + | awk 'x{print $3;exit} {x=1}'`" + + if ! [ "$statefulallocated" -gt 0 ] 2>/dev/null; then + error 2 "Cannot obtain free space on stateful partition." + fi + + # Unit: MiB + statefulsafe=$(((statefulallocated+\ + statefulallocated*MINIMUMSTATEFULMARGINPERCENT/100)/1024)) + statefulsafe2=$((statefulallocated/1024+MINIMUMSTATEFULMARGINABS)) + if [ "$MINIMUMSTATEFULSIZE" -gt "$statefulsafe" ]; then + statefulsafe="$MINIMUMSTATEFULSIZE" + fi + if [ "$statefulsafe2" -gt "$statefulsafe" ]; then + statefulsafe="$statefulsafe2" + fi + + if [ "$STATEFULSIZE" -lt "$statefulsafe" ]; then + error 1 \ +"Cannot shrink the stateful partition under $statefulsafe MB (selected size: $STATEFULSIZE MB). +Free up some space, or choose a larger stateful partition size (-s) or smaller +crouton partition (-c)." + fi + + # Unit: 512 bytes block + newstatesize="$((STATEFULSIZE*1024*2))" + + if [ "$newstatesize" -gt "$statesize" ]; then + error 1 "Stateful partition size must be less than the current one ($((statesize/(1024*2))) MB)." + fi + + croutonsize=$((statesize-newstatesize)) + croutonstart=$((statestart+newstatesize)) + + if [ "$croutonsize" -lt "$((MINIMUMCROUTONSIZE*1024*2))" ]; then + error 1 "You must leave at least $MINIMUMCROUTONSIZE MB for the crouton partition." + fi + + echo "Overriding partion table entry $CROUTONPARTNUMBER:" + cgpt show -i "$CROUTONPARTNUMBER" "$ROOTDEVICE" + echo "----" + echo "New partition sizes:" + echo " Stateful partition (Chromium OS): $((newstatesize/(2*1024))) MB" + freespace=$((newstatesize/(2*1024)-statefulallocated/1024)) + echo " Leftover free space: $freespace MB" + echo " crouton: $((croutonsize/(2*1024))) MB" + echo "----" + + echo -n "Type 'yes' if you are you satisfied with these new sizes: " + if [ -t 0 ]; then + read -r line + else + line="$CROUTON_MKPART_YES" + echo "$line" + fi + if [ "$line" != "yes" ]; then + error 2 "Aborting..." + fi +fi + +if grep -q '^[^ ]* '"`readlink -m '/var/run/crouton'`/" /proc/mounts \ + || grep -q '^[^ ]* '"$PREFIX"'/chroots' /proc/mounts; then + error 1 \ +"Some chroots are mounted. Log out from these, and run this script again." +fi + +echo "====== WARNING ======" +if [ -n "$DELETE" ]; then + echo "This script will now log you off and wipe '$name' (${ROOTDEVICEPREFIX}$CROUTONPARTNUMBER)." +elif [ -z "$NOCREATE" ]; then + echo "This script will now log you off, setup the crouton partition, and reboot." +else + echo "This script will now log you off and unmount the stateful partition." +fi +echo "Make sure all your current work is saved." + +time=10 +while [ "$time" -gt 0 ]; do + printf "\\rYou have %2d seconds to press Ctrl-C to abort..." "$time" + sleep 1 + time="$((time-1))" +done +echo + +# Unmount crouton partition, if it exists +if [ -n "$DELETE" -o -n "$NOCREATE" ]; then + for mountpoint in `awk \ + '$1 == "'"${ROOTDEVICEPREFIX}$CROUTONPARTNUMBER"'" { print $2 }' \ + /proc/mounts`; do + if ! umount "$mountpoint"; then + error 1 \ +"Cannot unmount partition in '$mountpoint', make sure nothing is using it." + fi + done +fi + +( # Fork a subshell, with input/output in VT $LOGVT + clear || true + + # Redefine error function + error() { + local ecode="$1" + shift + chvt $LOGVT + echo "$*" 1>&2 + echo "Press enter to reboot..." + sync + ( sleep 30; reboot ) & + read -r line + settrap "" + reboot + exit "$ecode" # unreachable + } + + addtrap "chvt $LOGVT; echo 'Something went wrong, press enter to reboot.'; \ + sync; ( sleep 30; reboot ) & read -r line; \ + echo 'Rebooting...'; reboot" + + # Make sure screen does not blank out (setterm does not work in this + # context: send control sequence instead) + /bin/echo -n -e "\x1b[9;0]\x1b[14;0]" + + # Get out of the stateful partition + cd / + + echo "Logging user out..." + dbus-send --system --dest=org.chromium.SessionManager --type=method_call \ + --print-reply /org/chromium/SessionManager \ + org.chromium.SessionManagerInterface.StopSession \ + string:"crouton installer" + + # Detect when the user has been logged out (1 minute timeout) + tries=12 + while [ "$tries" -gt 0 ] && grep -q '^/home/.shadow' /proc/mounts; do + chvt $LOGVT + echo "Waiting for logout to complete..." + sleep 5 + # After 30s, be more forceful + if [ "$tries" -le 6 ]; then + echo "Some mounts are still active:" + grep '^/home/.shadow' /proc/mounts || true + echo "Trying to unmount..." + grep '^/home/.shadow' /proc/mounts | cut -f1 -d ' ' \ + | xargs --no-run-if-empty -d ' +' -n 50 umount || true + fi + tries="$((tries-1))" + done + + # Switch to log VT + chvt $LOGVT + + if [ "$tries" = 0 ]; then + error 1 "Cannot log user out. Your system has not been modified." + fi + + echo "Stopping all services..." + + # Stop all services except tty2. || true is needed as sometimes services + # stop dependencies before we can stop them ourselves. + initctl list | grep process | cut -f1 -d' ' | grep -v tty2 | \ + xargs -I{} stop {} || true + + # Make sure we stay on the right VT (some services switch VT when stopped) + chvt $LOGVT + + echo "Unmounting stateful partition..." + # Try to unmount directories where ${ROOTDEVICEPREFIX}1 and + # /dev/mapper/encstateful are mounted. Order by length so that + # subdirectories are removed first. + for device in "/dev/mapper/encstateful" "${ROOTDEVICEPREFIX}1"; do + echo "Unmounting $device..." + for path in `awk ' + $1 == "'"$device"'" { + sub(/\\\\040\(deleted\)$/, "", $2) + print length($2)":"$2 + }' /proc/mounts | sort -nr | cut -d: -f 2`; do + echo "Unmounting $path and subdirectories..." + for mnt in `awk ' + $2 ~ "^'"$path"'($|/)" { + sub(/\\\\040\(deleted\)$/, "", $2) + print length($2)":"$2 + }' /proc/mounts | sort -nr | cut -d: -f 2`; do + # Replace \040 by space + mnt="`echo -n "$mnt" | sed -e 's/\\\\040/ /g'`" + pid="`lsof -t +D "$mnt" || true`" + if [ -n "$pid" ]; then + # These processes should have terminated already, KILL them + echo "Killing processes in $mnt..." + kill -9 $pid || true + fi + echo "Unmounting $mnt..." + umount "$mnt" 2>/dev/null + done + done + + if [ "$device" = "/dev/mapper/encstateful" ]; then + dmsetup remove "/dev/mapper/encstateful" 2>/dev/null + losetup -D 2>/dev/null + fi + done + + # Just in case, really. + chvt $LOGVT + + if grep -q "^${ROOTDEVICEPREFIX}1" /proc/mounts; then + error 1 "Cannot unmount stateful partition. Your system has not been modified." + fi + + if [ -n "$NOCREATE" ]; then + echo "Services were stopped, and stateful partition is unmounted." + echo "Press enter to switch back to VT2 (this is VT$LOGVT)." + echo "Then login as root, and type reboot when you are done." + read -r line + sync + chvt 2 + settrap "" + exit 0 + fi + + # For each partition table change, call partx on the relevant partition + # number to update the kernel view + if [ -n "$DELETE" ]; then + echo "Removing partition $CROUTONPARTNUMBER..." + cgpt add -i "$CROUTONPARTNUMBER" -b 0 -s 1 -l "" -t unused "$ROOTDEVICE" + partx -v -d "$CROUTONPARTNUMBER" "$ROOTDEVICE" + + echo "Resizing stateful partition..." + cgpt add -i 1 -s "$newstatesize" "$ROOTDEVICE" + partx -v -u 1 "$ROOTDEVICE" + + # Resize the stateful partition + e2fsck -f -p "${ROOTDEVICEPREFIX}1" + resize2fs -p "${ROOTDEVICEPREFIX}1" + else + echo "Resizing stateful partition..." + e2fsck -f -p "${ROOTDEVICEPREFIX}1" + resize2fs -p "${ROOTDEVICEPREFIX}1" "${newstatesize}s" + + echo "Updating partition table..." + cgpt add -i 1 -b "$statestart" -s "$newstatesize" -l STATE "$ROOTDEVICE" + partx -v -u 1 "$ROOTDEVICE" + + cgpt add -i "$CROUTONPARTNUMBER" -t data \ + -b "$croutonstart" -s "$croutonsize" -l CROUTON "$ROOTDEVICE" + partx -v -a "$CROUTONPARTNUMBER" "$ROOTDEVICE" + + echo "Formatting crouton partition..." + mkfs.ext4 "${ROOTDEVICEPREFIX}${CROUTONPARTNUMBER}" + fi + + sync + + echo "New partition table:" + cgpt show "$ROOTDEVICE" -i 1 + cgpt show "$ROOTDEVICE" -i "$CROUTONPARTNUMBER" + + echo "Success! Press enter to reboot..." + sync + ( sleep 30; reboot ) & + read -r line + settrap "" + reboot +) >"/dev/tty$LOGVT" 2>&1 <"/dev/tty$LOGVT" & + +wait + +error 1 "Cannot start subshell." diff --git a/installer/prepare.sh b/installer/prepare.sh index 964b67487..5a992bca1 100755 --- a/installer/prepare.sh +++ b/installer/prepare.sh @@ -176,23 +176,146 @@ fixkeyboardmode() { # stdin to the specified output and strips it. Finally, removes whatever it # installed. This allows targets to provide on-demand binaries without # increasing the size of the chroot after install. -# $1: name; target is /usr/local/bin/crouton$1 +# $1: name; target is /usr/local/(bin|lib)/crouton$1[.so] # $2: linker flags, quoted together +# [$3]: if 'so', compiles as a shared object and installs it into lib # $3+: any package dependencies other than gcc and libc-dev, crouton-style. compile() { - local out="/usr/local/bin/crouton$1" linker="$2" - echo "Installing dependencies for $out..." 1>&2 + local out="/usr/local/bin/crouton$1" + local linker="$2" + local cflags='-xc -Os' + if [ "$3" = 'so' ]; then + out="/usr/local/lib/crouton$1.so" + cflags="$cflags -shared -fPIC" + shift 1 + fi shift 2 + echo "Installing dependencies for $out..." 1>&2 local pkgs="gcc libc6-dev $*" install --minimal --asdeps $pkgs &2 local tmp="`mktemp crouton.XXXXXX --tmpdir=/tmp`" addtrap "rm -f '$tmp'" - gcc -xc -Os - $linker -o "$tmp" + gcc $cflags - $linker -o "$tmp" /usr/bin/install -sDT "$tmp" "$out" } +# Convert an automake Makefile.am into a shell script, and provide useful +# functions to compile libraries and executables. +# Needs to be run in the same directory as the Makefile.am file. +# This outputs the converted Makefile.am to stdout, which is meant to be +# piped to sh -s (see audio and xiat for examples) +convert_automake() { + echo ' + top_srcdir=".." + top_builddir=".." + ' + sed -e ' + # Concatenate lines ending in \ + : start; /\\$/{N; b start} + s/ *\\\n[ \t]*/ /g + # Convert automake to shell + s/^[^ ]*:/#\0/ + s/^\t/#\0/ + s/\t/ /g + s/ *= */=/ + s/\([^ ]*\) *+= */\1=${\1}\ / + s/ /\\ /g + y/()/{}/ + s/if\\ \(.*\)/if [ -n "${\1}" ]; then/ + s/endif/fi/ + ' 'Makefile.am' + echo ' + # buildsources: Build all source files for target + # $1: target + # $2: additional gcc flags + # Prints a list of .o files + buildsources() { + local target="$1" + local extragccflags="$2" + + eval local sources=\"\$${target}_SOURCES\" + eval local cppflags=\"\$${target}_CPPFLAGS\" + local cflags="$cppflags ${CFLAGS} ${AM_CFLAGS}" + + for dep in $sources; do + if [ "${dep%.c}" != "$dep" ]; then + ofile="${dep%.c}.o" + gcc -c "$dep" -o "$ofile" '"$archgccflags"' \ + $cflags $extragccflags 1>&2 || return $? + echo -n "$ofile " + fi + done + } + + # fixlibadd: + # Fix list of libraries ($1): replace lib.la by -l + fixlibadd() { + for libdep in $*; do + if [ "${libdep%.la}" != "$libdep" ]; then + libdep="${libdep%.la}" + libdep="-l${libdep#lib}" + fi + echo -n "$libdep " + done + } + + # buildlib: Build a library + # $1: library name + # $2: additional linker flags + buildlib() { + local lib="$1" + local extraflags="$2" + local ofiles + # local eats the return status: separate the 2 statements + ofiles="`buildsources "${lib}_la" "-fPIC -DPIC"`" + + eval local libadd=\"\$${lib}_la_LIBADD\" + eval local ldflags=\"\$${lib}_la_LDFLAGS\" + + libadd="`fixlibadd $libadd`" + + # Detect library version (e.g. 0.0.0) + local fullver="`echo -n "$ldflags" | \ + sed -n '\''y/:/./; \ + s/.*-version-info \([0-9.]*\)$/\\1/p'\''`" + local shortver="" + # Get "short" library version (e.g. 0) + if [ -n "$fullver" ]; then + shortver=".${fullver%%.*}" + fullver=".$fullver" + fi + local fullso="$lib.so$fullver" + local shortso="$lib.so$shortver" + gcc -shared -fPIC -DPIC $ofiles $libadd -o "$fullso" \ + '"$archgccflags"' $extraflags -Wl,-soname,"$shortso" + if [ -n "$fullver" ]; then + ln -sf "$fullso" "$shortso" + # Needed at link-time only + ln -sf "$shortso" "$lib.so" + fi + } + + # buildexe: Build an executable file + # $1: executable file name + # $2: additional linker flags + buildexe() { + local exe="$1" + local extraflags="$2" + local ofiles="`buildsources "$exe" ""`" + + eval local ldadd=\"\$${exe}_LDADD\" + eval local ldflags=\"\$${exe}_LDFLAGS\" + + ldadd="`fixlibadd $ldadd`" + + gcc $ofiles $ldadd -o "$exe" '"$archgccflags"' $extraflags + } + ' +} + + # The rest is dictated first by the distro-specific prepare.sh, and then by the # selected targets. diff --git a/src/fbserver-proto.h b/src/fbserver-proto.h new file mode 100644 index 000000000..2347d4dd3 --- /dev/null +++ b/src/fbserver-proto.h @@ -0,0 +1,91 @@ +/* Copyright (c) 2014 The crouton Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * WebSocket fbserver shared structures. + * + */ + +#ifndef FB_SERVER_PROTO_H_ +#define FB_SERVER_PROTO_H_ + +#include + +/* WebSocket constants */ +#define VERSION "VF2" +#define PORT_BASE 30010 + +/* Request for a frame */ +struct __attribute__((__packed__)) screen { + char type; /* 'S' */ + uint8_t shm:1; /* Transfer data through shm */ + uint8_t refresh:1; /* Force a refresh, even if no damage is observed */ + uint16_t width; + uint16_t height; + uint64_t paddr; /* shm: client buffer address */ + uint64_t sig; /* shm: signature at the beginning of buffer */ +}; + +/* Reply to request for a frame */ +struct __attribute__((__packed__)) screen_reply { + char type; /* 'S' */ + uint8_t shm:1; /* Data was transfered through shm */ + uint8_t shmfailed:1; /* shm trick has failed */ + uint8_t updated:1; /* data has been updated (Xdamage) */ + uint8_t cursor_updated:1; /* cursor has been updated */ + uint16_t width; + uint16_t height; + uint32_t cursor_serial; /* Cursor to display */ +}; + +/* Request for cursor image (if cursor_serial is unknown) */ +struct __attribute__((__packed__)) cursor { + char type; /* 'P' */ +}; + +/* Reply to requets for a cursor image (variable length) */ +struct __attribute__((__packed__)) cursor_reply { + char type; /* 'P' */ + uint16_t width, height; + uint16_t xhot, yhot; /* "Hot" coordinates */ + uint32_t cursor_serial; /* X11 unique serial number */ + uint32_t pixels[0]; /* Payload, 32-bit per pixel */ +}; + +/* Change resolution (query + reply) */ +struct __attribute__((__packed__)) resolution { + char type; /* 'R' */ + uint16_t width; + uint16_t height; +}; + +/* Press a key */ +struct __attribute__((__packed__)) key { + char type; /* 'K' */ + uint8_t down:1; /* 1: down, 0: up */ + uint8_t keycode; /* X11 KeyCode (8-255) */ +}; + +/* Press a key (compatibility with VF1) */ +/* TODO: Remove support for VF1. */ +struct __attribute__((__packed__)) key_vf1 { + char type; /* 'K' */ + uint8_t down:1; /* 1: down, 0: up */ + uint32_t keysym; /* X11 KeySym */ +}; + +/* Move the mouse */ +struct __attribute__((__packed__)) mousemove { + char type; /* 'M' */ + uint16_t x; + uint16_t y; +}; + +/* Click the mouse */ +struct __attribute__((__packed__)) mouseclick { + char type; /* 'C' */ + uint8_t down:1; + uint8_t button; /* X11 button number (e.g. 1 is left) */ +}; + +#endif /* FB_SERVER_PROTO_H_ */ diff --git a/src/fbserver.c b/src/fbserver.c new file mode 100644 index 000000000..c555c036f --- /dev/null +++ b/src/fbserver.c @@ -0,0 +1,622 @@ +/* Copyright (c) 2014 The crouton Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * WebSocket server that acts as a X11 framebuffer server. It communicates + * with the extension in Chromium OS. It sends framebuffer and cursor data, + * and receives keyboard/mouse events. + * + */ + +#include "websocket.h" +#include "fbserver-proto.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* X11-related variables */ +static Display *dpy; +static int damageEvent; +static int fixesEvent; + +/* shm entry cache */ +struct cache_entry { + uint64_t paddr; /* Address from PNaCl side */ + int fd; + void *map; /* mmap-ed memory */ + size_t length; /* mmap length */ +}; + +static struct cache_entry cache[2]; +static int next_entry; + +/* Remember which keys/buttons are currently pressed */ +typedef enum { MOUSE=1, KEYBOARD=2 } keybuttontype; +struct keybutton { + keybuttontype type; + uint32_t code; /* KeyCode or mouse button */ +}; + +/* Store currently pressed keys/buttons in an array. + * No valid entry on or after curmax. */ +static struct keybutton pressed[256]; +static int pressed_len = 0; + +/* Adds a key/button to array of pressed keys */ +void kb_add(keybuttontype type, uint32_t code) { + trueorabort(pressed_len < sizeof(pressed)/sizeof(struct keybutton), + "Too many keys pressed"); + + int i; + for (i = 0; i < pressed_len; i++) { + if (pressed[i].type == type && pressed[i].code == code) + return; + } + + pressed[pressed_len].type = type; + pressed[pressed_len].code = code; + pressed_len++; +} + +/* Removes a key/button from array of pressed keys */ +void kb_remove(keybuttontype type, uint32_t code) { + int i; + for (i = 0; i < pressed_len; i++) { + if (pressed[i].type == type && pressed[i].code == code) { + if (i < pressed_len-1) + pressed[i] = pressed[pressed_len-1]; + + pressed_len--; + return; + } + } +} + +/* Releases all pressed key/buttons, and empties array */ +void kb_release_all() { + int i; + log(2, "Releasing all keys..."); + for (i = 0; i < pressed_len; i++) { + if (pressed[i].type == MOUSE) { + log(2, "Mouse %d", pressed[i].code); + XTestFakeButtonEvent(dpy, pressed[i].code, 0, CurrentTime); + } else if (pressed[i].type == KEYBOARD) { + log(2, "Keyboard %d", pressed[i].code); + XTestFakeKeyEvent(dpy, pressed[i].code, 0, CurrentTime); + } + } + pressed_len = 0; +} + +/* X11-related functions */ + +static int xerror_handler(Display *dpy, XErrorEvent *e) { + if (verbose < 1) + return 0; + char msg[64] = {0}; + char op[32] = {0}; + sprintf(msg, "%d", e->request_code); + XGetErrorDatabaseText(dpy, "XRequest", msg, "", op, sizeof(op)); + XGetErrorText(dpy, e->error_code, msg, sizeof(msg)); + error("%s (%s)", msg, op); + return 0; +} + +/* Sets the CROUTON_CONNECTED property for the root window */ +static void set_connected(Display *dpy, uint8_t connected) { + Window root = DefaultRootWindow(dpy); + Atom prop = XInternAtom(dpy, "CROUTON_CONNECTED", False); + if (prop == None) { + error("Unable to get atom"); + return; + } + XChangeProperty(dpy, root, prop, XA_INTEGER, 8, PropModeReplace, + &connected, 1); + XFlush(dpy); +} + +/* Registers XDamage events for a given Window. */ +static void register_damage(Display *dpy, Window win) { + XWindowAttributes attrib; + if (XGetWindowAttributes(dpy, win, &attrib) && + !attrib.override_redirect) { + XDamageCreate(dpy, win, XDamageReportRawRectangles); + } +} + +/* Connects to the X11 display, initializes extensions, register for events */ +static int init_display(char* name) { + dpy = XOpenDisplay(name); + + if (!dpy) { + error("Cannot open display."); + return -1; + } + + /* We need XTest, XDamage and XFixes */ + int event, error, major, minor; + if (!XTestQueryExtension(dpy, &event, &error, &major, &minor)) { + error("XTest not available!"); + return -1; + } + + if (!XDamageQueryExtension(dpy, &damageEvent, &error)) { + error("XDamage not available!"); + return -1; + } + + if (!XFixesQueryExtension(dpy, &fixesEvent, &error)) { + error("XFixes not available!"); + return -1; + } + + /* Get notified when new windows are created. */ + Window root = DefaultRootWindow(dpy); + XSelectInput(dpy, root, SubstructureNotifyMask); + + /* Register damage events for existing windows */ + Window rootp, parent; + Window *children; + unsigned int i, nchildren; + XQueryTree(dpy, root, &rootp, &parent, &children, &nchildren); + + /* FIXME: We never reset the handler, is that a good thing? */ + XSetErrorHandler(xerror_handler); + + register_damage(dpy, root); + for (i = 0; i < nchildren; i++) { + register_damage(dpy, children[i]); + } + + XFree(children); + + /* Register for cursor events */ + XFixesSelectCursorInput(dpy, root, XFixesDisplayCursorNotifyMask); + + return 0; +} + +/* Changes resolution using external handler. + * Reply must be a resolution in "canonical" form: x[_] */ +/* FIXME: Maybe errors here should not be fatal... */ +void change_resolution(const struct resolution* rin) { + /* Setup parameters and run command */ + char arg1[32], arg2[32]; + int c; + c = snprintf(arg1, sizeof(arg1), "%d", rin->width); + trueorabort(c > 0, "snprintf"); + c = snprintf(arg2, sizeof(arg2), "%d", rin->height); + trueorabort(c > 0, "snprintf"); + + char* cmd = "setres"; + char* args[] = {cmd, arg1, arg2, NULL}; + char buffer[256]; + log(2, "Running %s %s %s", cmd, arg1, arg2); + c = popen2(cmd, args, NULL, 0, buffer, sizeof(buffer)); + trueorabort(c > 0, "popen2"); + + /* Parse output */ + buffer[c < sizeof(buffer) ? c : (sizeof(buffer)-1)] = 0; + log(2, "Result: %s", buffer); + char* cut = strchr(buffer, '_'); + if (cut) *cut = 0; + cut = strchr(buffer, 'x'); + trueorabort(cut, "Invalid answer: %s", buffer); + *cut = 0; + + char* endptr; + long nwidth = strtol(buffer, &endptr, 10); + trueorabort(buffer != endptr && *endptr == '\0', + "Invalid width: '%s'", buffer); + long nheight = strtol(cut+1, &endptr, 10); + trueorabort(cut+1 != endptr && (*endptr == '\0' || *endptr == '\n'), + "Invalid height: '%s'", cut+1); + log(1, "New resolution %ld x %ld", nwidth, nheight); + + char reply_raw[FRAMEMAXHEADERSIZE + sizeof(struct resolution)]; + struct resolution* r = (struct resolution*)(reply_raw + FRAMEMAXHEADERSIZE); + r->type = 'R'; + r->width = nwidth; + r->height = nheight; + socket_client_write_frame(reply_raw, sizeof(*r), WS_OPCODE_BINARY, 1); +} + +/* Closes the mmap/fd in the entry. */ +void close_mmap(struct cache_entry* entry) { + if (!entry->map) + return; + + log(2, "Closing mmap %p %zu %d", entry->map, entry->length, entry->fd); + munmap(entry->map, entry->length); + close(entry->fd); + entry->map = NULL; +} + +/* Finds NaCl/Chromium shm memory using external handler. + * Reply must be in the form PID:file */ +struct cache_entry* find_shm(uint64_t paddr, uint64_t sig, size_t length) { + struct cache_entry* entry = NULL; + + /* Find entry in cache */ + if (cache[0].paddr == paddr) { + entry = &cache[0]; + } else if (cache[1].paddr == paddr) { + entry = &cache[1]; + } else { + /* Not found: erase an existing entry. */ + entry = &cache[next_entry]; + next_entry = (next_entry + 1) % 2; + close_mmap(entry); + } + + int try; + for (try = 0; try < 2; try++) { + /* Check signature */ + if (entry->map) { + if (*((uint64_t*)entry->map) == sig) + return entry; + + log(1, "Invalid signature, fetching new shm!"); + close_mmap(entry); + } + + /* Setup parameters and run command */ + char arg1[32], arg2[32]; + int c; + + c = snprintf(arg1, sizeof(arg1), "%08lx", (long)paddr & 0xffffffff); + trueorabort(c > 0, "snprintf"); + int i, p = 0; + for (i = 0; i < 8; i++) { + c = snprintf(arg2 + p, sizeof(arg2) - p, "%02x", + ((uint8_t*)&sig)[i]); + trueorabort(c > 0, "snprintf"); + p += c; + } + + char* cmd = "croutonfindnacl"; + char* args[] = {cmd, arg1, arg2, NULL}; + char buffer[256]; + log(2, "Running %s %s %s", cmd, arg1, arg2); + c = popen2(cmd, args, NULL, 0, buffer, sizeof(buffer)); + if (c <= 0) { + error("Error running helper."); + return NULL; + } + buffer[c < sizeof(buffer) ? c : (sizeof(buffer)-1)] = 0; + log(2, "Result: %s", buffer); + + /* Parse PID:file output */ + char* cut = strchr(buffer, ':'); + if (!cut) { + error("No ':' in helper reply: %s.", cut); + return NULL; + } + *cut = 0; + + char* endptr; + long pid = strtol(buffer, &endptr, 10); + if(buffer == endptr || *endptr != '\0') { + error("Invalid pid: %s", buffer); + return NULL; + } + char* file = cut+1; + log(2, "PID:%ld, FILE:%s", pid, file); + + entry->paddr = paddr; + entry->fd = open(file, O_RDWR); + if (entry->fd < 0) { + error("Cannot open file %s\n", file); + return NULL; + } + + entry->length = length; + entry->map = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_SHARED, + entry->fd, 0); + if (!entry->map) { + error("Cannot mmap %s\n", file); + close(entry->fd); + return NULL; + } + + log(2, "mmap ok %p %zu %d", entry->map, entry->length, entry->fd); + } + + error("Cannot find shm."); + return NULL; +} + +/* WebSocket functions */ + +XImage* img = NULL; +XShmSegmentInfo shminfo; + +/* Writes framebuffer image to websocket/shm */ +int write_image(const struct screen* screen) { + char reply_raw[FRAMEMAXHEADERSIZE + sizeof(struct screen_reply)]; + struct screen_reply* reply = + (struct screen_reply*)(reply_raw + FRAMEMAXHEADERSIZE); + int refresh = 0; + + memset(reply_raw, 0, sizeof(reply_raw)); + + reply->type = 'S'; + reply->width = screen->width; + reply->height = screen->height; + + /* Allocate XShmImage */ + if (!img || img->width != screen->width || img->height != screen->height) { + if (img) { + XDestroyImage(img); + shmdt(shminfo.shmaddr); + shmctl(shminfo.shmid, IPC_RMID, 0); + } + + /* FIXME: Some error checking should happen here... */ + img = XShmCreateImage(dpy, DefaultVisual(dpy, 0), 24, + ZPixmap, NULL, &shminfo, + screen->width, screen->height); + trueorabort(img, "XShmCreateImage"); + shminfo.shmid = shmget(IPC_PRIVATE, img->bytes_per_line*img->height, + IPC_CREAT|0777); + trueorabort(shminfo.shmid != -1, "shmget"); + shminfo.shmaddr = img->data = shmat(shminfo.shmid, 0, 0); + trueorabort(shminfo.shmaddr != (void*)-1, "shmat"); + shminfo.readOnly = False; + int ret = XShmAttach(dpy, &shminfo); + trueorabort(ret, "XShmAttach"); + /* Force refresh */ + refresh = 1; + } + + if (screen->refresh) { + log(1, "Force refresh from client."); + /* refresh forced by the client */ + refresh = 1; + } + + XEvent ev; + /* Register damage on new windows */ + while (XCheckTypedEvent(dpy, MapNotify, &ev)) { + register_damage(dpy, ev.xcreatewindow.window); + refresh = 1; + } + + /* Check for damage */ + while (XCheckTypedEvent(dpy, damageEvent + XDamageNotify, &ev)) { + refresh = 1; + } + + /* Check for cursor events */ + reply->cursor_updated = 0; + while (XCheckTypedEvent(dpy, fixesEvent + XFixesCursorNotify, &ev)) { + XFixesCursorNotifyEvent* curev = (XFixesCursorNotifyEvent*)&ev; + if (verbose >= 2) { + char* name = XGetAtomName(dpy, curev->cursor_name); + log(2, "cursor! %ld %s", curev->cursor_serial, name); + XFree(name); + } + reply->cursor_updated = 1; + reply->cursor_serial = curev->cursor_serial; + } + + /* No update */ + if (!refresh) { + reply->shm = 0; + reply->updated = 0; + socket_client_write_frame(reply_raw, sizeof(*reply), + WS_OPCODE_BINARY, 1); + return 0; + } + + /* Get new image from framebuffer */ + XShmGetImage(dpy, DefaultRootWindow(dpy), img, 0, 0, AllPlanes); + + int size = img->bytes_per_line * img->height; + + trueorabort(size == screen->width*screen->height*4, + "Invalid screen byte count"); + + trueorabort(screen->shm, "Non-SHM rendering is not supported"); + + struct cache_entry* entry = find_shm(screen->paddr, screen->sig, size); + + reply->shm = 1; + reply->updated = 1; + reply->shmfailed = 0; + + if (entry && entry->map) { + if (size == entry->length) { + memcpy(entry->map, img->data, size); + msync(entry->map, size, MS_SYNC); + } else { + /* This should never happen (it means the client passed an + * outdated buffer to us). */ + error("Invalid shm entry length (client bug!)."); + reply->shmfailed = 1; + } + } else { + /* Keep the flow going, even if we cannot find the shm. Next time + * the NaCl client reallocates the buffer, we are likely to be able + * to find it. */ + error("Cannot find shm, moving on..."); + reply->shmfailed = 1; + } + + /* Confirm write is done */ + socket_client_write_frame(reply_raw, sizeof(*reply), + WS_OPCODE_BINARY, 1); + + return 0; +} + +/* Writes cursor image to websocket */ +int write_cursor() { + XFixesCursorImage *img = XFixesGetCursorImage(dpy); + if (!img) { + error("XFixesGetCursorImage returned NULL"); + return -1; + } + int size = img->width*img->height; + const int replylength = sizeof(struct cursor_reply) + size*sizeof(uint32_t); + char reply_raw[FRAMEMAXHEADERSIZE + replylength]; + struct cursor_reply* reply = + (struct cursor_reply*)(reply_raw + FRAMEMAXHEADERSIZE); + + memset(reply_raw, 0, sizeof(*reply_raw)); + + reply->type = 'P'; + reply->width = img->width; + reply->height = img->height; + reply->xhot = img->xhot; + reply->yhot = img->yhot; + reply->cursor_serial = img->cursor_serial; + /* This casts long[] to uint32_t[] */ + int i; + for (i = 0; i < size; i++) + reply->pixels[i] = img->pixels[i]; + + socket_client_write_frame(reply_raw, replylength, WS_OPCODE_BINARY, 1); + XFree(img); + + return 0; +} + +/* Checks if a packet size is correct */ +int check_size(int length, int target, char* error) { + if (length != target) { + error("Invalid %s packet (%d != %d)", error, length, target); + socket_client_close(0); + return 0; + } + return 1; +} + +/* Prints usage */ +void usage(char* argv0) { + fprintf(stderr, "%s [-v 0-3] display\n", argv0); + exit(1); +} + +int main(int argc, char** argv) { + int c; + while ((c = getopt(argc, argv, "v:")) != -1) { + switch (c) { + case 'v': + verbose = atoi(optarg); + break; + default: + usage(argv[0]); + } + } + + if (optind != argc-1) + usage(argv[0]); + + char* display = argv[optind]; + + trueorabort(display[0] == ':', "Invalid display: '%s'", display); + + char* endptr; + int displaynum = (int)strtol(display+1, &endptr, 10); + trueorabort(display+1 != endptr && (*endptr == '\0' || *endptr == '.'), + "Invalid display number: '%s'", display); + + init_display(display); + socket_server_init(PORT_BASE + displaynum); + + unsigned char buffer[BUFFERSIZE]; + int length; + + while (1) { + set_connected(dpy, False); + socket_server_accept(VERSION); + set_connected(dpy, True); + while (1) { + length = socket_client_read_frame((char*)buffer, sizeof(buffer)); + if (length < 0) { + socket_client_close(1); + break; + } + + if (length < 1) { + error("Invalid packet from client (size <1)."); + socket_client_close(0); + break; + } + + switch (buffer[0]) { + case 'S': /* Screen */ + if (!check_size(length, sizeof(struct screen), "screen")) + break; + write_image((struct screen*)buffer); + break; + case 'P': /* Cursor */ + if (!check_size(length, sizeof(struct cursor), "cursor")) + break; + write_cursor(); + break; + case 'R': /* Resolution */ + if (!check_size(length, sizeof(struct resolution), + "resolution")) + break; + change_resolution((struct resolution*)buffer); + break; + case 'K': { /* Key */ + if (!check_size(length, sizeof(struct key), "key")) + break; + struct key* k = (struct key*)buffer; + log(2, "Key: kc=%04x\n", k->keycode); + XTestFakeKeyEvent(dpy, k->keycode, k->down, CurrentTime); + if (k->down) { + kb_add(KEYBOARD, k->keycode); + } else { + kb_remove(KEYBOARD, k->keycode); + } + break; + } + case 'C': { /* Click */ + if (!check_size(length, sizeof(struct mouseclick), + "mouseclick")) + break; + struct mouseclick* mc = (struct mouseclick*)buffer; + XTestFakeButtonEvent(dpy, mc->button, mc->down, CurrentTime); + if (mc->down) { + kb_add(MOUSE, mc->button); + } else { + kb_remove(MOUSE, mc->button); + } + break; + } + case 'M': { /* Mouse move */ + if (!check_size(length, sizeof(struct mousemove), "mousemove")) + break; + struct mousemove* mm = (struct mousemove*)buffer; + XTestFakeMotionEvent(dpy, 0, mm->x, mm->y, CurrentTime); + break; + } + case 'Q': /* "Quit": release all keys */ + kb_release_all(); + break; + default: + error("Invalid packet from client (%d).", buffer[0]); + socket_client_close(0); + } + } + socket_client_close(0); + kb_release_all(); + close_mmap(&cache[0]); + close_mmap(&cache[1]); + } + + return 0; +} diff --git a/src/freon.c b/src/freon.c new file mode 100644 index 000000000..f60a5897a --- /dev/null +++ b/src/freon.c @@ -0,0 +1,192 @@ +/* Copyright (c) 2015 The crouton Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * LD_PRELOAD hack to make Xorg happy in a system without VT-switching. + * gcc -shared -fPIC -ldl -Wall -O2 freon.c -o croutonfreon.so + * + * Powered by black magic. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOCK_FILE_DIR "/tmp/crouton-lock" +#define DISPLAY_LOCK_FILE LOCK_FILE_DIR "/display" +#define FREON_DBUS_METHOD_CALL(function) \ + system("host-dbus dbus-send --system --dest=org.chromium.LibCrosService " \ + "--type=method_call --print-reply /org/chromium/LibCrosService " \ + "org.chromium.LibCrosServiceInterface." #function) + +#define TRACE(...) /* fprintf(stderr, __VA_ARGS__) */ +#define ERROR(...) fprintf(stderr, __VA_ARGS__) + +static int tty0fd = -1; +static int tty7fd = -1; +static int lockfd = -1; + +static int (*orig_ioctl)(int d, int request, void* data); +static int (*orig_open)(const char *pathname, int flags, mode_t mode); +static int (*orig_close)(int fd); + +static void preload_init() { + orig_ioctl = dlsym(RTLD_NEXT, "ioctl"); + orig_open = dlsym(RTLD_NEXT, "open"); + orig_close = dlsym(RTLD_NEXT, "close"); +} + +/* Grabs the system-wide lockfile that arbitrates which chroot is using the GPU. + * + * pid should be either the pid of the process that owns the GPU (eg. getpid()), + * or 0 to indicate that Chromium OS now owns the GPU. + * + * Returns 0 on success, or -1 on error. + */ +static int set_display_lock(unsigned int pid) { + if (lockfd == -1) { + if (pid == 0) { + ERROR("No display lock to release.\n"); + return 0; + } + (void) mkdir(LOCK_FILE_DIR, 0777); + lockfd = orig_open(DISPLAY_LOCK_FILE, O_CREAT | O_WRONLY, 0666); + if (lockfd == -1) { + ERROR("Unable to open display lock file.\n"); + return -1; + } + if (flock(lockfd, LOCK_EX) == -1) { + ERROR("Unable to lock display lock file.\n"); + return -1; + } + } + if (ftruncate(lockfd, 0) == -1) { + ERROR("Unable to truncate display lock file.\n"); + return -1; + } + char buf[11]; + int len; + if ((len = snprintf(buf, sizeof(buf), "%d\n", pid)) < 0) { + ERROR("pid sprintf failed.\n"); + return -1; + } + if (write(lockfd, buf, len) == -1) { + ERROR("Unable to write to display lock file.\n"); + return -1; + } + if (pid == 0) { + int ret = orig_close(lockfd); + lockfd = -1; + if (ret == -1) { + ERROR("Failure when closing display lock file.\n"); + } + return ret; + } + return 0; +} + +int ioctl(int fd, unsigned long int request, ...) { + if (!orig_ioctl) preload_init(); + + int ret = 0; + va_list argp; + va_start(argp, request); + void* data = va_arg(argp, void*); + + if (fd == tty0fd) { + TRACE("ioctl tty0 %d %lx %p\n", fd, request, data); + if (request == VT_OPENQRY) { + TRACE("OPEN\n"); + *(int*)data = 7; + } + ret = 0; + } else if (fd == tty7fd) { + TRACE("ioctl tty7 %d %lx %p\n", fd, request, data); + if (request == VT_GETSTATE) { + TRACE("STATE\n"); + struct vt_stat* stat = data; + stat->v_active = 0; + } + + if ((request == VT_RELDISP && (long)data == 1) || + (request == VT_ACTIVATE && (long)data == 0)) { + if (lockfd != -1) { + TRACE("Telling Chromium OS to regain control\n"); + ret = FREON_DBUS_METHOD_CALL(TakeDisplayOwnership); + if (set_display_lock(0) < 0) { + ERROR("Failed to release display lock\n"); + } + } + } else if ((request == VT_RELDISP && (long)data == 2) || + (request == VT_ACTIVATE && (long)data == 7)) { + if (set_display_lock(getpid()) == 0) { + TRACE("Telling Chromium OS to drop control\n"); + ret = FREON_DBUS_METHOD_CALL(ReleaseDisplayOwnership); + } else { + ERROR("Unable to claim display lock\n"); + ret = -1; + } + } else { + ret = 0; + } + } else { + if (request == EVIOCGRAB) { + TRACE("ioctl GRAB %d %lx %p\n", fd, request, data); + /* Driver requested a grab: assume we have it already and report + * success */ + ret = 0; + } else { + ret = orig_ioctl(fd, request, data); + } + } + va_end(argp); + return ret; +} + +int open(const char *pathname, int flags, ...) { + if (!orig_open) preload_init(); + + va_list argp; + va_start(argp, flags); + int mode = va_arg(argp, int); + + TRACE("open %s\n", pathname); + if (!strcmp(pathname, "/dev/tty0")) { + tty0fd = orig_open("/dev/null", flags, mode); + return tty0fd; + } else if (!strcmp(pathname, "/dev/tty7")) { + tty7fd = orig_open("/dev/null", flags, mode); + return tty7fd; + } else { + const char* event = "/dev/input/event"; + int fd = orig_open(pathname, flags, mode); + TRACE("open %s %d\n", pathname, fd); + if (!strncmp(pathname, event, strlen(event))) { + TRACE("GRAB\n"); + orig_ioctl(fd, EVIOCGRAB, (void *) 1); + } + return fd; + } +} + +int close(int fd) { + if (!orig_close) preload_init(); + + TRACE("close %d\n", fd); + + if (fd == tty0fd) { + tty0fd = -1; + } else if (fd == tty7fd) { + tty7fd = -1; + } + return orig_close(fd); +} diff --git a/src/websocket.c b/src/websocket.c index 0b7c2b7b8..7ff241b40 100644 --- a/src/websocket.c +++ b/src/websocket.c @@ -3,260 +3,33 @@ * found in the LICENSE file. * * WebSocket server that provides an interface to an extension running in - * Chromium OS. + * Chromium OS, used for clipboard synchronization and URL handling. * - * Mostly compliant with RFC 6455 - The WebSocket Protocol. - * - * Things that are supported, but not tested: - * - Fragmented packets from client - * - Ping packets */ -#define _GNU_SOURCE /* for ppoll */ -#include -#include -#include -#include -#include +#include "websocket.h" #include -#include -#include #include -#include -#include -#include -#include -#include - -const int BUFFERSIZE = 4096; +#include /* WebSocket constants */ -#define VERSION "1" -const int PORT = 30001; -const int FRAMEMAXHEADERSIZE = 2+8; -const int MAXFRAMESIZE = 16*1048576; // 16MiB -const char* GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; -/* Key from client must be 24 bytes long (16 bytes, base64 encoded) */ -const int SECKEY_LEN = 24; -/* SHA-1 is 20 bytes long */ -const int SHA1_LEN = 20; -/* base64-encoded SHA-1 must be 28 bytes long (ceil(20/3*4)+1). */ -const int SHA1_BASE64_LEN = 28; - -/* WebSocket opcodes */ -const int WS_OPCODE_CONT = 0x0; -const int WS_OPCODE_TEXT = 0x1; -const int WS_OPCODE_BINARY = 0x2; -const int WS_OPCODE_CLOSE = 0x8; -const int WS_OPCODE_PING = 0x9; -const int WS_OPCODE_PONG = 0xA; +#define VERSION "V2" +#define PORT 30001 /* Pipe constants */ const char* PIPE_DIR = "/tmp/crouton-ext"; const char* PIPEIN_FILENAME = "/tmp/crouton-ext/in"; const char* PIPEOUT_FILENAME = "/tmp/crouton-ext/out"; +const char* PIPE_VERSION_FILE = "/tmp/crouton-ext/version"; const int PIPEOUT_WRITE_TIMEOUT = 3000; -/* 0 - Quiet - * 1 - General messages (init, new connections) - * 2 - 1 + Information on each transfer - * 3 - 2 + Extra information */ -static int verbose = 0; - -#define log(level, str, ...) do { \ - if (verbose >= (level)) printf("%s: " str "\n", __func__, ##__VA_ARGS__); \ -} while (0) - -#define error(str, ...) printf("%s: " str "\n", __func__, ##__VA_ARGS__) - -/* Similar to perror, but prints function name as well */ -#define syserror(str, ...) printf("%s: " str " (%s)\n", \ - __func__, ##__VA_ARGS__, strerror(errno)) - /* File descriptors */ -static int server_fd = -1; static int pipein_fd = -1; -static int client_fd = -1; static int pipeout_fd = -1; -/* Prototypes */ -static int socket_client_write_frame(char* buffer, unsigned int size, - unsigned int opcode, int fin); -static int socket_client_read_frame_header(int* fin, uint32_t* maskkey, - int* length); -static int socket_client_read_frame_data(char* buffer, unsigned int size, - uint32_t maskkey); -static void socket_client_close(int close_reason); - static void pipeout_close(); - -/**/ -/* Helper functions */ -/**/ - -/* Read exactly size bytes from fd, no matter how many reads it takes. - * Returns size if successful, < 0 in case of error. */ -static int block_read(int fd, char* buffer, size_t size) { - int n; - int tot = 0; - - while (tot < size) { - n = read(fd, buffer+tot, size-tot); - log(3, "n=%d+%d/%zd", n, tot, size); - if (n < 0) - return n; - if (n == 0) - return -1; /* EOF */ - tot += n; - } - - return tot; -} - -/* Write exactly size bytes from fd, no matter how many writes it takes. - * Returns size if successful, < 0 in case of error. */ -static int block_write(int fd, char* buffer, size_t size) { - int n; - int tot = 0; - - while (tot < size) { - n = write(fd, buffer+tot, size-tot); - log(3, "n=%d+%d/%zd", n, tot, size); - if (n < 0) - return n; - if (n == 0) - return -1; - tot += n; - } - - return tot; -} - -/* Run external command, piping some data on its stdin, and reading back - * the output. Returns the number of bytes read from the process (at most - * outlen), or -1 on error. */ -static int popen2(char* cmd, char* input, int inlen, char* output, int outlen) { - pid_t pid = 0; - int stdin_fd[2]; - int stdout_fd[2]; - int ret = -1; - - if (pipe(stdin_fd) < 0 || pipe(stdout_fd) < 0) { - syserror("Failed to create pipe."); - return -1; - } - - pid = fork(); - - if (pid < 0) { - syserror("Fork error."); - return -1; - } else if (pid == 0) { - /* Child: connect stdin/out to the pipes, close the unneeded halves */ - close(stdin_fd[1]); - dup2(stdin_fd[0], STDIN_FILENO); - close(stdout_fd[0]); - dup2(stdout_fd[1], STDOUT_FILENO); - - execlp(cmd, cmd, NULL); - - error("Error running '%s'.", cmd); - exit(1); - } - - /* Parent */ - - /* Write input, and read output, while waiting for process termination. - * This could be done without polling, by reacting on SIGCHLD, but this is - * good enough for our purpose, and slightly simpler. */ - struct pollfd fds[2]; - fds[0].events = POLLIN; - fds[0].fd = stdout_fd[0]; - fds[1].events = POLLOUT; - fds[1].fd = stdin_fd[1]; - - pid_t wait_pid; - int readlen = 0; - int writelen = 0; - while (1) { - /* Get child status */ - wait_pid = waitpid(pid, NULL, WNOHANG); - /* Check if there is data to read, no matter the process status. */ - /* Timeout after 10ms, or immediately if the process exited already */ - int polln = poll(fds, 2, (wait_pid == pid) ? 0 : 10); - - if (polln < 0) { - syserror("poll error."); - goto error; - } - - log(3, "poll=%d (%d)", polln, (wait_pid == pid)); - - /* We can write something to stdin */ - if (fds[1].revents & POLLOUT) { - int n = write(stdin_fd[1], input+writelen, inlen-writelen); - if (n < 0) { - error("write error."); - goto error; - } - log(3, "write n=%d/%d", n, inlen); - - writelen += n; - if (writelen == inlen) { - /* Done writing: Only poll stdout from now on. */ - close(stdin_fd[1]); - stdin_fd[1] = -1; - fds[1].fd = -1; - } - polln--; - } - - /* We can read something from stdout */ - if (fds[0].revents & POLLIN) { - int n = read(stdout_fd[0], output+readlen, outlen-readlen); - if (n < 0) { - error("read error."); - goto error; - } - log(3, "read n=%d", n); - - readlen += n; - if (readlen >= outlen) { - error("Output too long."); - ret = readlen; - goto error; - } - polln--; - } - - if (polln != 0) { - error("Unknown poll event (%d).", fds[0].revents); - goto error; - } - - if (wait_pid == -1) { - error("waitpid error."); - goto error; - } else if (wait_pid == pid) { - log(3, "child exited!"); - break; - } - } - - if (stdin_fd[1] >= 0) - close(stdin_fd[1]); - close(stdout_fd[0]); - return readlen; - -error: - if (stdin_fd[1] >= 0) - close(stdin_fd[1]); - /* Closing the stdout pipe forces the child process to exit */ - close(stdout_fd[0]); - /* Try to wait 10ms for the process to exit, then bail out. */ - waitpid(pid, NULL, 10); - return ret; -} +static int socket_client_handle_unrequested(const char* buffer, + const int length); /* Open a pipe in non-blocking mode, then set it back to blocking mode. */ /* Returns fd on success, -1 if the pipe cannot be open, -2 if the O_NONBLOCK @@ -380,6 +153,7 @@ static void pipein_read() { int n; char buffer[FRAMEMAXHEADERSIZE+BUFFERSIZE]; int first = 1; + char firstchar = '\0'; if (client_fd < 0) { log(1, "No client FD."); @@ -400,6 +174,9 @@ static void pipein_read() { break; } + if (first) + firstchar = buffer[FRAMEMAXHEADERSIZE]; + /* Write a text frame for the first packet, then cont frames. */ n = socket_client_write_frame(buffer, n, first ? WS_OPCODE_TEXT : WS_OPCODE_CONT, 0); @@ -431,6 +208,7 @@ static void pipein_read() { int fin = 0; uint32_t maskkey; int retry = 0; + first = 1; /* Ignore return value, so we still read the frame even if pipeout * cannot be open. */ @@ -446,21 +224,49 @@ static void pipein_read() { continue; if (len < 0) - break; + goto exit; /* Read the whole frame, and write it to pipeout */ while (len > 0) { int rlen = (len > BUFFERSIZE) ? BUFFERSIZE: len; - if (socket_client_read_frame_data(buffer, rlen, maskkey) < 0) { - pipeout_close(); - return; + if (socket_client_read_frame_data(buffer, rlen, maskkey) < 0) + goto exit; + + /* Check first byte */ + if (first && buffer[0] != firstchar && buffer[0] != 'E') { + /* This is not a response: unrequested packet */ + if (!fin && len < BUFFERSIZE) { + /* !fin, and buffer not full, finish reading... */ + rlen = socket_client_read_frame(buffer+len, + sizeof(buffer)-len); + if (rlen < 0) + goto exit; + + len += rlen; + } + + if (len >= BUFFERSIZE) { + error("Unrequested command too long: (>%d bytes).", len); + socket_client_close(1); + goto exit; + } + + if (socket_client_handle_unrequested(buffer, len) < 0) + goto exit; + + /* Command was handled, try reading the answer again. */ + fin = 0; + break; } + /* Ignore return value as well */ pipeout_write(buffer, rlen); len -= rlen; + first = 0; } } +exit: pipeout_close(); } @@ -529,685 +335,117 @@ void pipe_init() { exit(1); } - pipein_reopen(); -} - -/**/ -/* Websocket functions. */ -/**/ - -/* Close the client socket, sending a close packet if sendclose is true. */ -static void socket_client_close(int sendclose) { - if (client_fd < 0) - return; - - if (sendclose) { - char buffer[FRAMEMAXHEADERSIZE]; - socket_client_write_frame(buffer, 0, WS_OPCODE_CLOSE, 1); - /* FIXME: We are supposed to read back the answer (if we are not - * replying to a close frame sent by the client), but we probably do not - * want to block, waiting for the answer, so we just close the socket. - */ - } - - close(client_fd); - client_fd = -1; -} - -/* Send a frame to the WebSocket client. - * - buffer needs to be FRAMEMAXHEADERSIZE+size long, and data must start at - * buffer[FRAMEMAXHEADERSIZE] only. - * - opcode should generally be WS_OPCODE_TEXT or WS_OPCODE_CONT (continuation) - * - fin indicates if the this is the last frame in the message - * Returns size on success. On error, closes the socket, and returns -1. - */ -static int socket_client_write_frame(char* buffer, unsigned int size, - unsigned int opcode, int fin) { - /* Start of frame, with header: at least 2 bytes before the actual data */ - char* pbuffer = buffer+FRAMEMAXHEADERSIZE-2; - int payloadlen = size; - int extlensize = 0; - - /* Test if we need an extended length field. */ - if (payloadlen > 125) { - if (payloadlen < 65536) { - payloadlen = 126; - extlensize = 2; - } else { - payloadlen = 127; - extlensize = 8; - } - pbuffer -= extlensize; - - /* Network-order (big-endian) */ - unsigned int tmpsize = size; - int i; - for (i = extlensize-1; i >= 0; i--) { - pbuffer[2+i] = tmpsize & 0xff; - tmpsize >>= 8; - } - } - - pbuffer[0] = opcode & 0x0f; - if (fin) pbuffer[0] |= 0x80; - pbuffer[1] = payloadlen; /* No mask (0x80) in server->client direction */ - - int wlen = 2+extlensize+size; - if (block_write(client_fd, pbuffer, wlen) != wlen) { - syserror("Write error."); - socket_client_close(0); - return -1; - } - - return size; -} - -/* Read a WebSocket frame header: - * - fin indicates in this is the final frame in a fragmented message - * - maskkey is the XOR key used for the message - * - retry is set to 1 if we receive a control packet: the caller must call - * again if it expects more data. - * - * Returns the frame length on success. On error, closes the socket, - * and returns -1. - * - * Data is then read with socket_client_read_frame_data() - */ -static int socket_client_read_frame_header(int* fin, uint32_t* maskkey, - int* retry) { - char header[2]; /* Minimum header length */ - char extlen[8]; /* Extended length */ - int n; - - *retry = 0; - - n = block_read(client_fd, header, 2); - if (n != 2) { - error("Read error."); - socket_client_close(0); - return -1; - } - - int opcode, mask; - uint64_t length; - *fin = (header[0] & 0x80) != 0; - if (header[0] & 0x70) { /* Reserved bits are on */ - error("Reserved bits are on."); - socket_client_close(1); - return -1; - } - opcode = header[0] & 0x0F; - mask = (header[1] & 0x80) != 0; - length = header[1] & 0x7F; - - log(2, "fin=%d; opcode=%d; mask=%d; length=%llu", - *fin, opcode, mask, (long long unsigned int)length); - - /* Read extended length if necessary */ - int extlensize = 0; - if (length == 126) - extlensize = 2; - else if (length == 127) - extlensize = 8; - - if (extlensize > 0) { - n = block_read(client_fd, extlen, extlensize); - if (n != extlensize) { - error("Read error."); - socket_client_close(0); - return -1; - } - - /* Network-order (big-endian) */ - int i; - length = 0; - for (i = 0; i < extlensize; i++) { - length = length << 8 | (uint8_t)extlen[i]; - } - - log(3, "extended length=%llu", (long long unsigned int)length); - } - - /* Read masking key if necessary */ - if (mask) { - n = block_read(client_fd, (char*)maskkey, 4); - if (n != 4) { - error("Read error."); - socket_client_close(0); - return -1; - } - } else { - /* RFC section 5.1 says we must close the connection if we receive a - * frame that is not masked. */ - error("No mask set."); - socket_client_close(1); - return -1; - } - - log(3, "maskkey=%04x", *maskkey); - - if (length > MAXFRAMESIZE) { - error("Frame too big! (%llu>%d)\n", - (long long unsigned int)length, MAXFRAMESIZE); - socket_client_close(1); - return -1; - } - - /* is opcode continuation, text, or binary? */ - /* FIXME: We should check that only the first packet is text or binary, and - * that the following are continuation ones. */ - if (opcode != WS_OPCODE_CONT && - opcode != WS_OPCODE_TEXT && opcode != WS_OPCODE_BINARY) { - log(2, "Got a control packet (opcode=%d).", opcode); - - /* Control packets cannot be fragmented. - * Unknown data (opcodes 3-7) will result in error anyway. */ - if (*fin == 0) { - error("Fragmented unknown packet (%x).", opcode); - socket_client_close(1); - return -1; - } - - /* Read the rest of the packet */ - char* buffer = malloc(length+3); /* +3 for unmasking safety */ - if (socket_client_read_frame_data(buffer, length, *maskkey) < 0) { - socket_client_close(0); - free(buffer); - return -1; - } - - if (opcode == WS_OPCODE_CLOSE) { /* Connection close. */ - error("Connection close from WebSocket client."); - socket_client_close(1); - free(buffer); - return -1; - } else if (opcode == WS_OPCODE_PING) { /* Ping */ - socket_client_write_frame(buffer, length, WS_OPCODE_PONG, 1); - } else if (opcode == WS_OPCODE_PONG) { /* Pong */ - /* Do nothing */ - } else { /* Unknown opcode */ - error("Unknown packet (%x).", opcode); - socket_client_close(1); - free(buffer); - return -1; - } - - free(buffer); - - /* Tell the caller to wait for the next packet */ - *retry = 1; - *fin = 0; - return 0; - } - - return length; -} - -/* Read frame data from the WebSocket client: - * - Make sure that buffer is at least 4*ceil(size/4) long, as unmasking works - * on blocks of 4 bytes. - * Returns size on success (the buffer has been completely filled). - * On error, closes the socket, and returns -1. - */ -static int socket_client_read_frame_data(char* buffer, unsigned int size, - uint32_t maskkey) { - int n = block_read(client_fd, buffer, size); - if (n != size) { - error("Read error."); - socket_client_close(0); - return -1; - } - - if (maskkey != 0) { - int i; - int len32 = (size+3)/4; - uint32_t* buffer32 = (uint32_t*)buffer; - for (i = 0; i < len32; i++) { - buffer32[i] ^= maskkey; - } - } - - return n; -} - -/* Unrequested data came in from WebSocket client. */ -static void socket_client_read() { - char buffer[BUFFERSIZE]; - int length = 0; - int fin = 0; - uint32_t maskkey; - int retry = 0; - int data = 0; /* 1 if we received some valid data */ - - /* Read possible fragmented message into buffer */ - while (fin != 1) { - int curlen = socket_client_read_frame_header(&fin, &maskkey, &retry); - - if (retry) { - if (!data) { - /* We only got a control frame, go back to main loop. We will - * get called again if there is more data waiting. */ - return; - } else { - /* We already read some frames of a fragmented message: wait - * for the rest. */ - continue; - } - } - - if (curlen < 0) { - socket_client_close(0); - return; - } - - if (length+curlen > BUFFERSIZE) { - error("Message too big (%d>%d).", length+curlen, BUFFERSIZE); - socket_client_close(1); - return; - } - - if (socket_client_read_frame_data(buffer+length, curlen, maskkey) < 0) { - error("Read error."); - socket_client_close(0); - return; - } - - length += curlen; - data = 1; - } - - /* In future versions, we can process such packets here. */ - - /* In the current version, this is actually never supposed to happen: - * close the connection */ - error("Received an unexpected packet from client."); - socket_client_close(0); -} - -/* Send a version packet to the extension, and read VOK reply. */ -static void socket_client_sendversion() { - char* version = "V"VERSION; - int versionlen = strlen(version); - char* outbuf = malloc(FRAMEMAXHEADERSIZE+versionlen); - memcpy(outbuf+FRAMEMAXHEADERSIZE, version, versionlen); - - log(2, "Sending version packet (%s).", version); - - if (socket_client_write_frame(outbuf, versionlen, WS_OPCODE_TEXT, 1) < 0) { - error("Write error."); - socket_client_close(0); - free(outbuf); - return; - } - free(outbuf); - - /* Read response back */ - char buffer[256]; - int buflen = 0; - int fin = 0; - uint32_t maskkey; - int retry = 0; - - /* Read possibly fragmented message from WebSocket. */ - while (fin != 1) { - int len = socket_client_read_frame_header(&fin, &maskkey, &retry); - - if (retry) - continue; - - if (len < 0) - break; - - if (len+buflen > 256) { - error("Response too long: (>%d bytes).", 256); - socket_client_close(1); - return; - } - - if (socket_client_read_frame_data(buffer+buflen, len, maskkey) < 0) { - socket_client_close(0); - return; - } - buflen += len; - } - - buffer[buflen == 256 ? 255 : buflen] = 0; - if (buflen != 3 || strcmp(buffer, "VOK")) { - int i; - for (i = 0; i < buflen; i++) { - if (!isprint(buffer[i])) - buffer[i] = '?'; - } - error("Invalid response: %s.", buffer); - socket_client_close(1); - return; - } - - log(2, "Received VOK."); -} - -/* Bitmask indicating if we received everything we need in the header */ -const int OK_GET = 0x01; /* GET {PATH} HTTP/1.1 */ -const int OK_GET_PATH = 0x02; /* {PATH} == / in GET request */ -const int OK_UPGRADE = 0x04; /* Upgrade: websocket */ -const int OK_CONNECTION = 0x08; /* Connection: Upgrade */ -const int OK_SEC_VERSION = 0x10; /* Sec-WebSocket-Version: {VERSION} */ -const int OK_VERSION = 0x20; /* {VERSION} == 13 */ -const int OK_SEC_KEY = 0x40; /* Sec-WebSocket-Key: 24 bytes */ -const int OK_HOST = 0x80; /* Host: localhost:PORT */ -const int OK_ALL = 0xFF; /* Final correct value is 0xFF */ - -/* Send an error on a new client socket, then close the socket. */ -static void socket_server_error(int newclient_fd, int ok) { - /* Values found only in WebSocket header */ - const int OK_WEBSOCKET = OK_UPGRADE|OK_CONNECTION|OK_SEC_VERSION| - OK_VERSION|OK_SEC_KEY; - /* Values found in WebSocket header of a possibly wrong version */ - const int OK_OTHER_VERSION = OK_GET|OK_UPGRADE|OK_CONNECTION|OK_SEC_VERSION; - - char buffer[BUFFERSIZE]; - - if ((ok & OK_GET) && - (!(ok & OK_GET_PATH) || !(ok & OK_WEBSOCKET))) { - /* Path is not /, or / but clearly not a WebSocket handshake: 404 */ - strncpy(buffer, - "HTTP/1.1 404 Not Found\r\n" - "\r\n" - "

404 Not Found

", BUFFERSIZE); - } else if ((ok & OK_OTHER_VERSION) == OK_OTHER_VERSION && - !(ok & OK_VERSION)) { - /* We received something that looks like a WebSocket handshake, - * but wrong version */ - strncpy(buffer, - "HTTP/1.1 400 Bad Request\r\n" - "Sec-WebSocket-Version: 13\r\n" - "\r\n", BUFFERSIZE); - } else { - /* Generic answer */ - strncpy(buffer, - "HTTP/1.1 400 Bad Request\r\n" - "\r\n" - "

400 Bad Request

", BUFFERSIZE); + /* Create a file with the version number of the protocol */ + FILE *vers = fopen(PIPE_VERSION_FILE, "w"); + if (!vers + || fputs(VERSION "\n", vers) == EOF + || fclose(vers) == EOF) { + error("Unable to write to %s.", PIPE_VERSION_FILE); + exit(1); } - log(3, "answer:\n%s===", buffer); - - /* Ignore errors */ - block_write(newclient_fd, buffer, strlen(buffer)); - - close(newclient_fd); + pipein_reopen(); } -/* Read and parse HTTP header. - * Returns 0 if the header is valid. websocket_key must be at least SECKEY_LEN - * bytes long, and contains the value of Sec-WebSocket-Key on success. - * Returns < 0 in case of error: in that case newclient_fd is closed. +/* Handle unrequested packet from extension. + * Returns 0 on success. On error, returns -1 and closes websocket connection. */ -static int socket_server_read_header(int newclient_fd, char* websocket_key) { - int first = 1; - char buffer[BUFFERSIZE]; - int ok = 0x00; - - char* pbuffer = buffer; - int n = read(newclient_fd, buffer, BUFFERSIZE); - if (n <= 0) { - syserror("Cannot read from client."); - close(newclient_fd); - return -1; - } - - while (1) { - /* Start of current line (until ':' for key-value pairs) */ - char* key = pbuffer; - /* Start of value in current line (part after ': '). */ - char* value = NULL; - - /* Read a line of header, splitting key-value pairs if possible. */ - while (1) { - if (n == 0) { - /* No more data in buffer: shift data so that key == buffer, - * and try reading again. */ - memmove(buffer, key, pbuffer-key); - if (value) - value -= (key-buffer); - pbuffer -= (key-buffer); - key = buffer; - - n = read(newclient_fd, pbuffer, BUFFERSIZE-(pbuffer-buffer)); - if (n <= 0) { - syserror("Cannot read from client."); - close(newclient_fd); +static int socket_client_handle_unrequested(const char* buffer, + const int length) { + /* Process the client request. */ + switch (buffer[0]) { + case 'C': { /* Send a command to croutoncycle */ + char reply[BUFFERSIZE]; + int replylength = 1; + reply[FRAMEMAXHEADERSIZE] = 'C'; + + char* cmd = "croutoncycle"; + char param[length]; + memcpy(param, buffer+1, length-1); + param[length-1] = '\0'; + char* args[] = { cmd, param, NULL }; + + log(2, "Received croutoncycle command (%s)", param); + + /* We are only interested in the output for list commands */ + if (param[0] == 'l') { + int n = popen2(cmd, args, NULL, 0, + &reply[FRAMEMAXHEADERSIZE+1], + BUFFERSIZE-FRAMEMAXHEADERSIZE-1); + if (n < 0) { + error("Call to croutoncycle failed."); + socket_client_close(0); return -1; } - } - - /* Detect new line: - * HTTP RFC says it must be CRLF, but we accept LF. */ - if (*pbuffer == '\n') { - if (*(pbuffer-1) == '\r') - *(pbuffer-1) = '\0'; - else - *pbuffer = '\0'; - n--; pbuffer++; + replylength += n; + } else if (param[0] == 'O') { + /* Extra OK response from a C back-and-forth. Disregard. */ break; - } - - /* Detect "Key: Value" pairs, on all lines but the first one. */ - if (!first && !value && *pbuffer == ':') { - value = pbuffer+2; - *pbuffer = '\0'; - } - - n--; pbuffer++; - } - - log(3, "HTTP header: key=%s; value=%s.", key, value); - - /* Empty line indicates end of header. */ - if (strlen(key) == 0 && !value) - break; - - if (first) { /* Normally GET / HTTP/1.1 */ - first = 0; - - char* tok = strtok(key, " "); - if (!tok || strcmp(tok, "GET")) { - error("Invalid HTTP method (%s).", tok); - continue; - } - - tok = strtok(NULL, " "); - if (!tok || strcmp(tok, "/")) { - error("Invalid path (%s).", tok); } else { - ok |= OK_GET_PATH; - } - - tok = strtok(NULL, " "); - if (!tok || strcmp(tok, "HTTP/1.1")) { - error("Invalid HTTP version (%s).", tok); - continue; + /* Launch command in background (this is necessary as + croutoncycle may send a websocket command, leaving us + deadlocked...) */ + pid_t pid = fork(); + if (pid < 0) { + syserror("Fork error."); + exit(1); + } else if (pid == 0) { + /* Double-fork to avoid zombies */ + pid_t pid2 = fork(); + if (pid2 < 0) { + syserror("Fork error."); + exit(1); + } else if (pid2 == 0) { + execvp(cmd, args); + error("Error running '%s'.", cmd); + exit(127); + } + exit(0); + } + /* Wait for first fork to complete. */ + waitpid(pid, NULL, 0); } - - ok |= OK_GET; - } else { - if (!value) { - error("Invalid HTTP header (%s).", key); - socket_server_error(newclient_fd, 0x00); + if (socket_client_write_frame(reply, replylength, + WS_OPCODE_TEXT, 1) < 0) { + error("Write error."); + socket_client_close(0); return -1; } - - if (!strcmp(key, "Upgrade") && !strcmp(value, "websocket")) { - ok |= OK_UPGRADE; - } else if (!strcmp(key, "Connection") && - !strcmp(value, "Upgrade")) { - ok |= OK_CONNECTION; - } else if (!strcmp(key, "Sec-WebSocket-Version")) { - ok |= OK_SEC_VERSION; - if (strcmp(value, "13")) { - error("Invalid Sec-WebSocket-Version: '%s'.", value); - continue; - } - ok |= OK_VERSION; - } else if (!strcmp(key, "Sec-WebSocket-Key")) { - if (strlen(value) != SECKEY_LEN) { - error("Invalid Sec-WebSocket-Key: '%s'.", value); - continue; - } - memcpy(websocket_key, value, SECKEY_LEN); - ok |= OK_SEC_KEY; - } else if (!strcmp(key, "Host")) { - char strbuf[32]; - snprintf(strbuf, 32, "localhost:%d", PORT); - - if (strcmp(value, strbuf)) { - error("Invalid Host field: '%s'.", value); - continue; - } - ok |= OK_HOST; - } + break; + } + default: { + int len = length > 64 ? 64 : length; + char dump[len+1]; + memcpy(dump, buffer, len); + dump[len] = '\0'; + error("Received an unexpected packet from client (%s).", dump); + socket_client_close(0); + return -1; } - } - - if (ok != OK_ALL) { - error("Some WebSocket headers missing (%x).", ~ok & OK_ALL); - socket_server_error(newclient_fd, ok); - return -1; } return 0; } -/* Accept a new client connection on the server socket. */ -static void socket_server_accept() { - int newclient_fd; - struct sockaddr_in client_addr; - unsigned int client_addr_len = sizeof(client_addr); +/* Unrequested data came in from WebSocket client. */ +static void socket_client_read() { char buffer[BUFFERSIZE]; + int length; - newclient_fd = accept(server_fd, - (struct sockaddr*)&client_addr, &client_addr_len); - - if (newclient_fd < 0) { - syserror("Error accepting new connection."); - return; - } - - /* key from client + GUID */ - int websocket_keylen = SECKEY_LEN+strlen(GUID); - char websocket_key[websocket_keylen]; - - /* Read and parse HTTP header */ - if (socket_server_read_header(newclient_fd, websocket_key) < 0) { - return; - } - - log(1, "Header read successfully."); - - /* Compute sha1+base64 response (RFC section 4.2.2, paragraph 5.4) */ - - char sha1[SHA1_LEN]; - - /* Some margin so we can read the full output of base64 */ - int b64_len = SHA1_BASE64_LEN+4; - char b64[b64_len]; - int i; - - memcpy(websocket_key+SECKEY_LEN, GUID, strlen(GUID)); - - /* SHA-1 is 20 bytes long (40 characters in hex form) */ - if (popen2("sha1sum", websocket_key, websocket_keylen, - buffer, BUFFERSIZE) < 2*SHA1_LEN) { - error("sha1sum response too short."); - exit(1); - } - - for (i = 0; i < SHA1_LEN; i++) { - unsigned int value; - if (sscanf(&buffer[i*2], "%02x", &value) != 1) { - buffer[2*SHA1_LEN] = 0; - error("Cannot read SHA-1 sum (%s).", buffer); - exit(1); - } - sha1[i] = (char)value; - } - - /* base64 encoding of SHA1_LEN bytes must be SHA1_BASE64_LEN bytes long. - * Either the output is exactly SHA1_BASE64_LEN long, or the last character - * is a line feed (RFC 3548 forbids other characters in output) */ - int n = popen2("base64", sha1, SHA1_LEN, b64, b64_len); - if (n < SHA1_BASE64_LEN || - (n != SHA1_BASE64_LEN && b64[SHA1_BASE64_LEN] != '\r' && - b64[SHA1_BASE64_LEN] != '\n')) { - error("Invalid base64 response."); - exit(1); - } - b64[SHA1_BASE64_LEN] = '\0'; - - int len = snprintf(buffer, BUFFERSIZE, - "HTTP/1.1 101 Switching Protocols\r\n" - "Upgrade: websocket\r\n" - "Connection: Upgrade\r\n" - "Sec-WebSocket-Accept: %s\r\n" - "\r\n", b64); - - if (len == BUFFERSIZE) { - error("Response length > %d.", BUFFERSIZE); - exit(1); - } - - log(3, "HTTP response:\n%s===", buffer); - - if (block_write(newclient_fd, buffer, len) != len) { - syserror("Cannot write response."); - close(newclient_fd); + length = socket_client_read_frame(buffer, sizeof(buffer)); + if (length < 0) { + socket_client_close(1); return; } - log(2, "Response sent."); - - /* Close existing connection, if any. */ - if (client_fd >= 0) + if (length >= BUFFERSIZE) { + error("Unrequested command too long: (>%d bytes).", length); socket_client_close(1); - - client_fd = newclient_fd; - - socket_client_sendversion(); - - return; -} - -/* Initialise WebSocket server */ -static void socket_server_init() { - struct sockaddr_in server_addr; - int optval; - - server_fd = socket(AF_INET, SOCK_STREAM, 0); - if (server_fd < 0) { - syserror("Cannot create server socket."); - exit(1); - } - - /* SO_REUSEADDR to make sure the server can restart after a crash. */ - optval = 1; - setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); - - /* Listen on loopback interface, port PORT. */ - memset(&server_addr, 0, sizeof(server_addr)); - server_addr.sin_family = AF_INET; - server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - server_addr.sin_port = htons(PORT); - - if (bind(server_fd, - (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { - syserror("Cannot bind server socket."); - exit(1); + return; } - if (listen(server_fd, 5) < 0) { - syserror("Cannot listen on server socket."); - exit(1); - } + /* Ignore return value (connection gets closed on error) */ + socket_client_handle_unrequested(buffer, length); } static int terminate = 0; @@ -1281,7 +519,7 @@ int main(int argc, char **argv) { fds[2].events = POLLIN; /* Initialise pipe and WebSocket server */ - socket_server_init(); + socket_server_init(PORT); pipe_init(); while (!terminate) { @@ -1306,7 +544,7 @@ int main(int argc, char **argv) { if (fds[0].revents & POLLIN) { log(1, "WebSocket accept."); - socket_server_accept(); + socket_server_accept(VERSION); n--; } if (fds[1].revents & POLLIN) { diff --git a/src/websocket.h b/src/websocket.h new file mode 100644 index 000000000..5def60653 --- /dev/null +++ b/src/websocket.h @@ -0,0 +1,934 @@ +/* Copyright (c) 2014 The crouton Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * Provides common WebSocket functions that can be used by both websocket.c + * and fbserver.c. + * + * Mostly compliant with RFC 6455 - The WebSocket Protocol. + * + * Things that are supported, but not tested: + * - Fragmented packets from client + * - Ping packets + */ + +#define _GNU_SOURCE /* for ppoll */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const int BUFFERSIZE = 4096; + +/* WebSocket constants */ +const int FRAMEMAXHEADERSIZE = 16; // Actually 2+8, but align on 8-byte boundary +const int MAXFRAMESIZE = 16*1048576; // 16MiB +const char* GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; +/* Key from client must be 24 bytes long (16 bytes, base64 encoded) */ +const int SECKEY_LEN = 24; +/* SHA-1 is 20 bytes long */ +const int SHA1_LEN = 20; +/* base64-encoded SHA-1 must be 28 bytes long (ceil(20/3*4)+1). */ +const int SHA1_BASE64_LEN = 28; + +/* WebSocket opcodes */ +const int WS_OPCODE_CONT = 0x0; +const int WS_OPCODE_TEXT = 0x1; +const int WS_OPCODE_BINARY = 0x2; +const int WS_OPCODE_CLOSE = 0x8; +const int WS_OPCODE_PING = 0x9; +const int WS_OPCODE_PONG = 0xA; + +/* WebSocket bitmasks */ +const char WS_HEADER0_FIN = 0x80; /* fin */ +const char WS_HEADER0_RSV = 0x70; /* reserved */ +const char WS_HEADER0_OPCODE_MASK = 0x0F; /* opcode */ +const char WS_HEADER1_MASK = 0x80; /* mask */ +const char WS_HEADER1_LEN_MASK = 0x7F; /* payload length */ + +/* 0 - Quiet + * 1 - General messages (init, new connections) + * 2 - 1 + Information on each transfer + * 3 - 2 + Extra information */ +static int verbose = 0; + +#define log(level, str, ...) do { \ + if (verbose >= (level)) printf("%s: " str "\n", __func__, ##__VA_ARGS__); \ +} while (0) + +#define error(str, ...) printf("%s: " str "\n", __func__, ##__VA_ARGS__) + +/* Aborts if expr is false */ +#define trueorabort(expr, str, ...) do { \ + if (!(expr)) { \ + printf("%s: ASSERTION " #expr " FAILED (" str ")\n", \ + __func__, ##__VA_ARGS__); \ + abort(); \ + } \ +} while (0) + +/* Similar to perror, but prints function name as well */ +#define syserror(str, ...) printf("%s: " str " (%s)\n", \ + __func__, ##__VA_ARGS__, strerror(errno)) + +/* Port number, assigned in socket_server_init() */ +static int port = -1; + +/* File descriptors */ +static int server_fd = -1; +static int client_fd = -1; + +/* Prototypes */ +static int socket_client_write_frame(char* buffer, unsigned int size, + unsigned int opcode, int fin); +static int socket_client_read_frame_header(int* fin, uint32_t* maskkey, + int* length); +static int socket_client_read_frame_data(char* buffer, unsigned int size, + uint32_t maskkey); +static void socket_client_close(int close_reason); + +/**/ +/* Helper functions */ +/**/ + +/* Read exactly size bytes from fd, no matter how many reads it takes. + * Returns size if successful, < 0 in case of error. */ +static int block_read(int fd, char* buffer, size_t size) { + int n; + int tot = 0; + + while (tot < size) { + n = read(fd, buffer + tot, size - tot); + log(3, "n=%d+%d/%zd", n, tot, size); + if (n < 0) + return n; + if (n == 0) + return -1; /* EOF */ + tot += n; + } + + return tot; +} + +/* Write exactly size bytes from fd, no matter how many writes it takes. + * Returns size if successful, < 0 in case of error. */ +static int block_write(int fd, char* buffer, size_t size) { + int n; + int tot = 0; + + while (tot < size) { + n = write(fd, buffer + tot, size - tot); + log(3, "n=%d+%d/%zd", n, tot, size); + if (n < 0) + return n; + if (n == 0) + return -1; + tot += n; + } + + return tot; +} + +/* Run external command, piping some data on its stdin, and reading back + * the output. Returns the number of bytes read from the process (at most + * outlen), or a negative number on error (-exit status). */ +static int popen2(char* cmd, char *const argv[], + char* input, int inlen, char* output, int outlen) { + pid_t pid = 0; + int stdin_fd[2]; + int stdout_fd[2]; + + if (pipe(stdin_fd) < 0 || pipe(stdout_fd) < 0) { + syserror("Failed to create pipe."); + return -1; + } + + log(3, "pipes: in %d/%d; out %d/%d", + stdin_fd[0], stdin_fd[1], stdout_fd[0], stdout_fd[1]); + + pid = fork(); + + if (pid < 0) { + syserror("Fork error."); + return -1; + } else if (pid == 0) { + /* Child: connect stdin/out to the pipes, close the unneeded halves */ + close(stdin_fd[1]); + dup2(stdin_fd[0], STDIN_FILENO); + close(stdout_fd[0]); + dup2(stdout_fd[1], STDOUT_FILENO); + + if (argv) { + execvp(cmd, argv); + } else { + execlp(cmd, cmd, NULL); + } + + syserror("Error running '%s'.", cmd); + exit(127); + } + + /* Parent */ + + /* Close uneeded halves (those are used by the child) */ + close(stdin_fd[0]); + close(stdout_fd[1]); + + /* Write input, and read output. We rely on POLLHUP getting set on stdout + * when the process exits (this assumes the process does not do anything + * strange like closing stdout and staying alive). */ + struct pollfd fds[2]; + fds[0].events = POLLIN; + fds[0].fd = stdout_fd[0]; + fds[1].events = POLLOUT; + fds[1].fd = stdin_fd[1]; + + int readlen = 0; /* Also acts as return value */ + int writelen = 0; + while (1) { + int polln = poll(fds, 2, -1); + + if (polln < 0) { + syserror("poll error."); + readlen = -1; + break; + } + + log(3, "poll=%d", polln); + + /* We can write something to stdin */ + if (fds[1].revents & POLLOUT) { + if (inlen > writelen) { + int n = write(stdin_fd[1], input + writelen, inlen - writelen); + if (n < 0) { + error("write error."); + readlen = -1; + break; + } + log(3, "write n=%d/%d", n, inlen); + writelen += n; + } + + if (writelen == inlen) { + /* Done writing: Only poll stdout from now on. */ + close(stdin_fd[1]); + stdin_fd[1] = -1; + fds[1].fd = -1; + } + fds[1].revents &= ~POLLOUT; + } + + if (fds[1].revents != 0) { + error("Unknown poll event on stdout (%d).", fds[1].revents); + readlen = -1; + break; + } + + /* We can read something from stdout */ + if (fds[0].revents & POLLIN) { + int n = read(stdout_fd[0], output + readlen, outlen - readlen); + if (n < 0) { + error("read error."); + readlen = -1; + break; + } + log(3, "read n=%d", n); + readlen += n; + + if (verbose >= 3) { + fwrite(output, 1, readlen, stdout); + } + + if (readlen >= outlen) { + error("Output too long."); + break; + } + fds[0].revents &= ~POLLIN; + } + + /* stdout has hung up (process terminated) */ + if (fds[0].revents == POLLHUP) { + log(3, "pollhup"); + break; + } else if (fds[0].revents != 0) { + error("Unknown poll event on stdin (%d).", fds[0].revents); + readlen = -1; + break; + } + } + + if (stdin_fd[1] >= 0) + close(stdin_fd[1]); + /* Closing the stdout pipe forces the child process to exit */ + close(stdout_fd[0]); + + /* Get child status (no timeout: we assume the child behaves well) */ + int status = 0; + pid_t wait_pid = waitpid(pid, &status, 0); + + if (wait_pid != pid) { + syserror("waitpid error."); + return -1; + } + + if (WIFEXITED(status)) { + log(3, "child exited!"); + if (WEXITSTATUS(status) != 0) { + error("child exited with status %d", WEXITSTATUS(status)); + return -WEXITSTATUS(status); + } + } else { + error("child process did not exit: %d", status); + return -1; + } + + if (writelen != inlen) { + error("Incomplete write."); + return -1; + } + + return readlen; +} + +/**/ +/* Websocket functions. */ +/**/ + +/* Close the client socket, sending a close packet if sendclose is true. */ +static void socket_client_close(int sendclose) { + if (client_fd < 0) + return; + + if (sendclose) { + char buffer[FRAMEMAXHEADERSIZE]; + socket_client_write_frame(buffer, 0, WS_OPCODE_CLOSE, 1); + /* FIXME: We are supposed to read back the answer (if we are not + * replying to a close frame sent by the client), but we probably do not + * want to block, waiting for the answer, so we just close the socket. + */ + } + + close(client_fd); + client_fd = -1; +} + +/* Send a frame to the WebSocket client. + * - buffer needs to be FRAMEMAXHEADERSIZE+size long, and data must start at + * buffer[FRAMEMAXHEADERSIZE] only. + * - opcode should generally be WS_OPCODE_TEXT or WS_OPCODE_CONT (continuation) + * - fin indicates if the this is the last frame in the message + * Returns size on success. On error, closes the socket, and returns -1. + */ +static int socket_client_write_frame(char* buffer, unsigned int size, + unsigned int opcode, int fin) { + /* Start of frame, with header: at least 2 bytes before the actual data */ + char* pbuffer = buffer + FRAMEMAXHEADERSIZE - 2; + int payloadlen = size; + int extlensize = 0; + + /* Test if we need an extended length field. */ + if (payloadlen > 125) { + if (payloadlen < 65536) { + payloadlen = 126; + extlensize = 2; + } else { + payloadlen = 127; + extlensize = 8; + } + pbuffer -= extlensize; + + /* Network-order (big-endian) */ + unsigned int tmpsize = size; + int i; + for (i = extlensize-1; i >= 0; i--) { + pbuffer[2+i] = tmpsize & 0xff; + tmpsize >>= 8; + } + } + + pbuffer[0] = opcode & WS_HEADER0_OPCODE_MASK; + if (fin) pbuffer[0] |= WS_HEADER0_FIN; + pbuffer[1] = payloadlen; /* No mask (0x80) in server->client direction */ + + int wlen = 2 + extlensize + size; + if (block_write(client_fd, pbuffer, wlen) != wlen) { + syserror("Write error."); + socket_client_close(0); + return -1; + } + + return size; +} + +/* Read a WebSocket frame header: + * - fin indicates in this is the final frame in a fragmented message + * - maskkey is the XOR key used for the message + * - retry is set to 1 if we receive a control packet: the caller must call + * again if it expects more data. + * + * Returns the frame length on success. On error, closes the socket, + * and returns -1. + * + * Data is then read with socket_client_read_frame_data() + */ +static int socket_client_read_frame_header(int* fin, uint32_t* maskkey, + int* retry) { + char header[2]; /* Minimum header length */ + char extlen[8]; /* Extended length */ + int n; + + *retry = 0; + + n = block_read(client_fd, header, 2); + if (n != 2) { + error("Read error."); + socket_client_close(0); + return -1; + } + + int opcode, mask; + uint64_t length; + *fin = (header[0] & WS_HEADER0_FIN) != 0; + if (header[0] & WS_HEADER0_RSV) { + error("Reserved bits are on."); + socket_client_close(1); + return -1; + } + opcode = header[0] & WS_HEADER0_OPCODE_MASK; + mask = (header[1] & WS_HEADER1_MASK) != 0; + length = header[1] & WS_HEADER1_LEN_MASK; + + log(2, "fin=%d; opcode=%d; mask=%d; length=%llu", + *fin, opcode, mask, (long long unsigned int)length); + + /* Read extended length if necessary */ + int extlensize = 0; + if (length == 126) + extlensize = 2; + else if (length == 127) + extlensize = 8; + + if (extlensize > 0) { + n = block_read(client_fd, extlen, extlensize); + if (n != extlensize) { + error("Read error."); + socket_client_close(0); + return -1; + } + + /* Network-order (big-endian) */ + int i; + length = 0; + for (i = 0; i < extlensize; i++) { + length = length << 8 | (uint8_t)extlen[i]; + } + + log(3, "extended length=%llu", (long long unsigned int)length); + } + + /* Read masking key if necessary */ + if (mask) { + n = block_read(client_fd, (char*)maskkey, 4); + if (n != 4) { + error("Read error."); + socket_client_close(0); + return -1; + } + } else { + /* RFC section 5.1 says we must close the connection if we receive a + * frame that is not masked. */ + error("No mask set."); + socket_client_close(1); + return -1; + } + + log(3, "maskkey=%04x", *maskkey); + + if (length > MAXFRAMESIZE) { + error("Frame too big! (%llu>%d)\n", + (long long unsigned int)length, MAXFRAMESIZE); + socket_client_close(1); + return -1; + } + + /* is opcode continuation, text, or binary? */ + /* FIXME: We should check that only the first packet is text or binary, and + * that the following are continuation ones. */ + if (opcode != WS_OPCODE_CONT && + opcode != WS_OPCODE_TEXT && opcode != WS_OPCODE_BINARY) { + log(2, "Got a control packet (opcode=%d).", opcode); + + /* Control packets cannot be fragmented. + * Unknown data (opcodes 3-7) will result in error anyway. */ + if (*fin == 0) { + error("Fragmented unknown packet (%x).", opcode); + socket_client_close(1); + return -1; + } + + /* Read the rest of the packet */ + char* buffer = malloc(length+3); /* +3 for unmasking safety */ + if (socket_client_read_frame_data(buffer, length, *maskkey) < 0) { + socket_client_close(0); + free(buffer); + return -1; + } + + if (opcode == WS_OPCODE_CLOSE) { /* Connection close. */ + error("Connection close from WebSocket client."); + socket_client_close(1); + free(buffer); + return -1; + } else if (opcode == WS_OPCODE_PING) { /* Ping */ + socket_client_write_frame(buffer, length, WS_OPCODE_PONG, 1); + } else if (opcode == WS_OPCODE_PONG) { /* Pong */ + /* Do nothing */ + } else { /* Unknown opcode */ + error("Unknown packet (%x).", opcode); + socket_client_close(1); + free(buffer); + return -1; + } + + free(buffer); + + /* Tell the caller to wait for the next packet */ + *retry = 1; + *fin = 0; + return 0; + } + + return length; +} + +/* Read frame data from the WebSocket client: + * - Make sure that buffer is at least 4*ceil(size/4) long, as unmasking works + * on blocks of 4 bytes. + * Returns size on success (the buffer has been completely filled). + * On error, closes the socket, and returns -1. + */ +static int socket_client_read_frame_data(char* buffer, unsigned int size, + uint32_t maskkey) { + int n = block_read(client_fd, buffer, size); + if (n != size) { + error("Read error."); + socket_client_close(0); + return -1; + } + + if (maskkey != 0) { + int i; + int len32 = (size+3)/4; + uint32_t* buffer32 = (uint32_t*)buffer; + for (i = 0; i < len32; i++) { + buffer32[i] ^= maskkey; + } + } + + return n; +} + +/* Read a complete frame from the WebSocket client: + * - Make sure that buffer size is a multiple of 4 (for unmasking). + * Returns packet size on success. + * On error (e.g. packet too large for buffer), closes the socket, and + * returns -1. + */ +static int socket_client_read_frame(char* buffer, int size) { + int buflen = 0; + int fin = 0; + uint32_t maskkey; + int retry = 0; + + /* Read possibly fragmented message from WebSocket. */ + while (fin != 1) { + int len = socket_client_read_frame_header(&fin, &maskkey, &retry); + + if (retry) + continue; + + if (len < 0) + return -1; + + if (len+buflen > size) { + error("Response too long: (>%d bytes).", size); + socket_client_close(1); + return -1; + } + + if (socket_client_read_frame_data(buffer + buflen, len, maskkey) < 0) { + socket_client_close(0); + return -1; + } + buflen += len; + } + + return buflen; +} + +/* Send a version packet to the extension, and read VOK reply. */ +static int socket_client_sendversion(char* version) { + int versionlen = strlen(version); + char* outbuf = malloc(FRAMEMAXHEADERSIZE + versionlen); + memcpy(outbuf + FRAMEMAXHEADERSIZE, version, versionlen); + + log(2, "Sending version packet (%s).", version); + + if (socket_client_write_frame(outbuf, versionlen, WS_OPCODE_TEXT, 1) < 0) { + error("Write error."); + socket_client_close(0); + free(outbuf); + return -1; + } + free(outbuf); + + /* Read response back */ + char buffer[256]; + int buflen = socket_client_read_frame(buffer, sizeof(buffer)); + + buffer[buflen == 256 ? 255 : buflen] = 0; + if (buflen != 3 || strcmp(buffer, "VOK")) { + int i; + for (i = 0; i < buflen; i++) { + if (!isprint(buffer[i])) + buffer[i] = '?'; + } + error("Invalid response: %s.", buffer); + socket_client_close(1); + return -1; + } + + log(2, "Received VOK."); + return 0; +} + +/* Bitmask indicating if we received everything we need in the header */ +const int OK_GET = 0x01; /* GET {PATH} HTTP/1.1 */ +const int OK_GET_PATH = 0x02; /* {PATH} == / in GET request */ +const int OK_UPGRADE = 0x04; /* Upgrade: websocket */ +const int OK_CONNECTION = 0x08; /* Connection: Upgrade */ +const int OK_SEC_VERSION = 0x10; /* Sec-WebSocket-Version: {VERSION} */ +const int OK_VERSION = 0x20; /* {VERSION} == 13 */ +const int OK_SEC_KEY = 0x40; /* Sec-WebSocket-Key: 24 bytes */ +const int OK_HOST = 0x80; /* Host: localhost:PORT */ +const int OK_ALL = 0xFF; /* Final correct value is 0xFF */ + +/* Send an error on a new client socket, then close the socket. */ +static void socket_server_error(int newclient_fd, int ok) { + /* Values found only in WebSocket header */ + const int OK_WEBSOCKET = OK_UPGRADE|OK_CONNECTION|OK_SEC_VERSION| + OK_VERSION|OK_SEC_KEY; + /* Values found in WebSocket header of a possibly wrong version */ + const int OK_OTHER_VERSION = OK_GET|OK_UPGRADE|OK_CONNECTION|OK_SEC_VERSION; + + char buffer[BUFFERSIZE]; + + if ((ok & OK_GET) && + (!(ok & OK_GET_PATH) || !(ok & OK_WEBSOCKET))) { + /* Path is not /, or / but clearly not a WebSocket handshake: 404 */ + strncpy(buffer, + "HTTP/1.1 404 Not Found\r\n" + "\r\n" + "

404 Not Found

", BUFFERSIZE); + } else if ((ok & OK_OTHER_VERSION) == OK_OTHER_VERSION && + !(ok & OK_VERSION)) { + /* We received something that looks like a WebSocket handshake, + * but wrong version */ + strncpy(buffer, + "HTTP/1.1 400 Bad Request\r\n" + "Sec-WebSocket-Version: 13\r\n" + "\r\n", BUFFERSIZE); + } else { + /* Generic answer */ + strncpy(buffer, + "HTTP/1.1 400 Bad Request\r\n" + "\r\n" + "

400 Bad Request

", BUFFERSIZE); + } + + log(3, "answer:\n%s===", buffer); + + /* Ignore errors */ + block_write(newclient_fd, buffer, strlen(buffer)); + + close(newclient_fd); +} + +/* Read and parse HTTP header. + * Returns 0 if the header is valid. websocket_key must be at least SECKEY_LEN + * bytes long, and contains the value of Sec-WebSocket-Key on success. + * Returns < 0 in case of error: in that case newclient_fd is closed. + */ +static int socket_server_read_header(int newclient_fd, char* websocket_key) { + int first = 1; + char buffer[BUFFERSIZE]; + int ok = 0x00; + + char* pbuffer = buffer; + int n = read(newclient_fd, buffer, BUFFERSIZE); + if (n <= 0) { + syserror("Cannot read from client."); + close(newclient_fd); + return -1; + } + + while (1) { + /* Start of current line (until ':' for key-value pairs) */ + char* key = pbuffer; + /* Start of value in current line (part after ': '). */ + char* value = NULL; + + /* Read a line of header, splitting key-value pairs if possible. */ + while (1) { + if (n == 0) { + /* No more data in buffer: shift data so that key == buffer, + * and try reading again. */ + memmove(buffer, key, pbuffer-key); + if (value) + value -= (key-buffer); + pbuffer -= (key-buffer); + key = buffer; + + n = read(newclient_fd, pbuffer, BUFFERSIZE-(pbuffer-buffer)); + if (n <= 0) { + syserror("Cannot read from client."); + close(newclient_fd); + return -1; + } + } + + /* Detect new line: + * HTTP RFC says it must be CRLF, but we accept LF. */ + if (*pbuffer == '\n') { + if (*(pbuffer-1) == '\r') + *(pbuffer-1) = '\0'; + else + *pbuffer = '\0'; + n--; pbuffer++; + break; + } + + /* Detect "Key: Value" pairs, on all lines but the first one. */ + if (!first && !value && *pbuffer == ':') { + value = pbuffer+2; + *pbuffer = '\0'; + } + + n--; pbuffer++; + } + + log(3, "HTTP header: key=%s; value=%s.", key, value); + + /* Empty line indicates end of header. */ + if (strlen(key) == 0 && !value) + break; + + if (first) { /* Normally GET / HTTP/1.1 */ + first = 0; + + char* tok = strtok(key, " "); + if (!tok || strcmp(tok, "GET")) { + error("Invalid HTTP method (%s).", tok); + continue; + } + + tok = strtok(NULL, " "); + if (!tok || strcmp(tok, "/")) { + error("Invalid path (%s).", tok); + } else { + ok |= OK_GET_PATH; + } + + tok = strtok(NULL, " "); + if (!tok || strcmp(tok, "HTTP/1.1")) { + error("Invalid HTTP version (%s).", tok); + continue; + } + + ok |= OK_GET; + } else { + if (!value) { + error("Invalid HTTP header (%s).", key); + socket_server_error(newclient_fd, 0x00); + return -1; + } + + if (!strcmp(key, "Upgrade") && !strcmp(value, "websocket")) { + ok |= OK_UPGRADE; + } else if (!strcmp(key, "Connection") && + !strcmp(value, "Upgrade")) { + ok |= OK_CONNECTION; + } else if (!strcmp(key, "Sec-WebSocket-Version")) { + ok |= OK_SEC_VERSION; + if (strcmp(value, "13")) { + error("Invalid Sec-WebSocket-Version: '%s'.", value); + continue; + } + ok |= OK_VERSION; + } else if (!strcmp(key, "Sec-WebSocket-Key")) { + if (strlen(value) != SECKEY_LEN) { + error("Invalid Sec-WebSocket-Key: '%s'.", value); + continue; + } + memcpy(websocket_key, value, SECKEY_LEN); + ok |= OK_SEC_KEY; + } else if (!strcmp(key, "Host")) { + char strbuf[32]; + snprintf(strbuf, 32, "localhost:%d", port); + + if (strcmp(value, strbuf)) { + error("Invalid Host field: '%s'.", value); + continue; + } + ok |= OK_HOST; + } + } + } + + if (ok != OK_ALL) { + error("Some WebSocket headers missing (%x).", ~ok & OK_ALL); + socket_server_error(newclient_fd, ok); + return -1; + } + + return 0; +} + +/* Accept a new client connection on the server socket. */ +static int socket_server_accept(char* version) { + int newclient_fd; + struct sockaddr_in client_addr; + unsigned int client_addr_len = sizeof(client_addr); + char buffer[BUFFERSIZE]; + + newclient_fd = accept(server_fd, + (struct sockaddr*)&client_addr, &client_addr_len); + + if (newclient_fd < 0) { + syserror("Error accepting new connection."); + return -1; + } + + /* key from client + GUID */ + int websocket_keylen = SECKEY_LEN + strlen(GUID); + char websocket_key[websocket_keylen]; + + /* Read and parse HTTP header */ + if (socket_server_read_header(newclient_fd, websocket_key) < 0) { + return -1; + } + + log(1, "Header read successfully."); + + /* Compute sha1+base64 response (RFC section 4.2.2, paragraph 5.4) */ + + char sha1[SHA1_LEN]; + + /* Some margin so we can read the full output of base64 */ + int b64_len = SHA1_BASE64_LEN + 4; + char b64[b64_len]; + int i; + + memcpy(websocket_key + SECKEY_LEN, GUID, strlen(GUID)); + + /* SHA-1 is 20 bytes long (40 characters in hex form) */ + if (popen2("sha1sum", NULL, websocket_key, websocket_keylen, + buffer, BUFFERSIZE) < 2*SHA1_LEN) { + error("sha1sum response too short."); + exit(1); + } + + /* Make sure sscanf does not read too much data */ + buffer[2*SHA1_LEN + 1] = '\0'; + for (i = 0; i < SHA1_LEN; i++) { + unsigned int value; + if (sscanf(&buffer[i*2], "%02x", &value) != 1) { + buffer[2*SHA1_LEN] = 0; + error("Cannot read SHA-1 sum (%s).", buffer); + exit(1); + } + sha1[i] = (char)value; + } + + /* base64 encoding of SHA1_LEN bytes must be SHA1_BASE64_LEN bytes long. + * Either the output is exactly SHA1_BASE64_LEN long, or the last character + * is a line feed (RFC 3548 forbids other characters in output) */ + int n = popen2("base64", NULL, sha1, SHA1_LEN, b64, b64_len); + if (n < SHA1_BASE64_LEN || + (n != SHA1_BASE64_LEN && b64[SHA1_BASE64_LEN] != '\r' && + b64[SHA1_BASE64_LEN] != '\n')) { + error("Invalid base64 response."); + exit(1); + } + b64[SHA1_BASE64_LEN] = '\0'; + + int len = snprintf(buffer, BUFFERSIZE, + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: %s\r\n" + "\r\n", b64); + + if (len == BUFFERSIZE) { + error("Response length > %d.", BUFFERSIZE); + exit(1); + } + + log(3, "HTTP response:\n%s===", buffer); + + if (block_write(newclient_fd, buffer, len) != len) { + syserror("Cannot write response."); + close(newclient_fd); + return -1; + } + + log(2, "Response sent."); + + /* Close existing connection, if any. */ + if (client_fd >= 0) + socket_client_close(1); + + client_fd = newclient_fd; + + return socket_client_sendversion(version); +} + +/* Initialise WebSocket server */ +static void socket_server_init(int port_) { + struct sockaddr_in server_addr; + int optval; + + port = port_; + + server_fd = socket(AF_INET, SOCK_STREAM, 0); + if (server_fd < 0) { + syserror("Cannot create server socket."); + exit(1); + } + + /* SO_REUSEADDR to make sure the server can restart after a crash. */ + optval = 1; + setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); + + /* Listen on loopback interface, port PORT. */ + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + server_addr.sin_port = htons(port); + + if (bind(server_fd, + (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { + syserror("Cannot bind server socket."); + exit(1); + } + + if (listen(server_fd, 5) < 0) { + syserror("Cannot listen on server socket."); + exit(1); + } +} diff --git a/targets/audio b/targets/audio index a43543212..bfc33af62 100644 --- a/targets/audio +++ b/targets/audio @@ -117,90 +117,6 @@ if tail -n 1 "$log" | grep -q "^Error"; then fi fi -# Patch CRAS so that x86 client can connect to x86_64 server (multilib) -# Assume current directory is "$CRASBUILDTMP"/cras/src -patch_cras_x86_x86_64() { - echo "Patching CRAS (x86 client with x86_64 server)..." 1>&2 - - common="common/cras_shm.h common/cras_messages.h common/cras_iodev_info.h \ - common/cras_types.h common/cras_audio_format.c" - cras_audio_format_h="" - - # Newer versions (>=5062) contain an additional header file - if [ -e "common/cras_audio_format.h" ]; then - cras_audio_format_h="common/cras_audio_format.h" - fi - - # Replace size_t/long by fixed-size integers corresponding to their - # respective sizes on x86_64, aligned on 8-byte boundaries - sed -i -e 's/uint64_t[ \t]/aligned_uint64_t /g - s/size_t[ \t]/aligned_uint64_t /g - s/long[ \t]/aligned_int64_t /g' $common $cras_audio_format_h - - # Hack to make sure sizeof(struct cras_server/client_message) is a - # multiple of 8 - sed -i -e \ - 's/\(enum CRAS_CLIENT_MESSAGE_ID\) id;/\1 __attribute__((aligned(8))) id;/ - s/\(enum CRAS_SERVER_MESSAGE_ID\) id;/\1 __attribute__((aligned(8))) id;/' \ - common/cras_messages.h - - # Disable syslog to remove warnings about printf formats - sed -i -e '/#include /a \ -#define syslog(...) do \{\} while(0)' \ - common/cras_fmt_conv.c libcras/cras_client.c - - # Replace timespec/timeval - sed -i -e 's/struct timespec[ \t]/struct cras_timespec /g - s/clock_gettime(/cras_clock_gettime(/' \ - $common $cras_audio_format_h common/cras_util.h \ - libcras/cras_client.h libcras/cras_client.c \ - alsa_plugin/pcm_cras.c tests/cras_test_client.c - sed -i -e \ - 's/struct timeval/struct { aligned_int64_t tv_sec; aligned_int64_t tv_usec; }/' \ - common/cras_iodev_info.h - - # Include compat file (it will get included multiple times, but this is - # harmless compared to the complexity of a sed script that replaces the - # first match only) - sed -i -e '/#include.*/i \ -#include "cras_x86_64_compat.h"' common/cras_iodev_info.h $cras_audio_format_h - - # Create a new header file with x86_64 compatibility types: aligned integers - # and timespec wrapper. - cat > common/cras_x86_64_compat.h < -#include - -typedef uint64_t __attribute__((aligned(8))) aligned_uint64_t; -typedef int64_t __attribute__((aligned(8))) aligned_int64_t; - -struct cras_timespec { - aligned_int64_t tv_sec; - aligned_int64_t tv_nsec; -}; - -static inline int cras_clock_gettime(clockid_t clk_id, struct cras_timespec *ctp) { - struct timespec tp; - int ret = clock_gettime(clk_id, &tp); - ctp->tv_sec = tp.tv_sec; - ctp->tv_nsec = tp.tv_nsec; - return ret; -} -#endif -END - - # Restore uncorrectly replaced timespecs, add 2 necessary casts - sed -i -e 's/struct cras_timespec sleep_ts;/struct timespec sleep_ts;/ - s/struct cras_timespec wait_time;/struct timespec wait_time;/ - s/\(.*\)cras_clock_gettime\(.*&wait_time);.*\)/\1clock_gettime\2/ - s/nodes\[i\].priority/(size_t)nodes[i].priority/ - s/clients\[i\].id/(size_t)clients[i].id/' \ - tests/cras_test_client.c -} - # Build CRAS ALSA plugin for the given architecture ($1) # A blank parameter means we are building for the native architecture. build_cras() { @@ -249,23 +165,6 @@ build_cras() { pkg-config --variable=libdir alsa`/alsa-lib" fi - # SBC is not available in older Debian/Ubuntu: fetch it manually - if release -le quantal; then - install_mirror_package 'libsbc1' \ - 'pool/main/s/sbc' '1\.1-.*' $cras_arch - install_mirror_package --asdeps 'libsbc-dev' \ - 'pool/main/s/sbc' '1\.1-.*' $cras_arch - elif release -le wheezy -eq kali; then - # wheezy provides a backport for libsbc1 - install_mirror_package 'libsbc1' \ - 'pool/main/s/sbc' '.*~bpo7.*' $cras_arch - install_mirror_package --asdeps 'libsbc-dev' \ - 'pool/main/s/sbc' '.*~bpo7.*' $cras_arch - else - install --minimal libsbc1$pkgsuffix - install --minimal --asdeps libsbc-dev$pkgsuffix - fi - # Start subshell for compilation ( cd "$CRASBUILDTMP" @@ -283,86 +182,6 @@ build_cras() { install --minimal --asdeps patch - # Fix delay function in CRAS ALSA plugin, see http://crbug.com/331637 - # Applied upstream in version 5215 - # FIXME: Remove this when 5215 has trickled down to stable - if [ "$CROS_VER_1" -lt 5215 ]; then - echo "Patching CRAS (delay function bug)..." 1>&2 - patch -p3 <stream == SND_PCM_STREAM_PLAYBACK) { -- cras_client_calc_playback_latency(&pcm_cras->playback_sample_time, -- &latency); -+ /* Do not compute latency if playback_sample_time is not set */ -+ if (pcm_cras->playback_sample_time.tv_sec == 0 && -+ pcm_cras->playback_sample_time.tv_nsec == 0) { -+ latency.tv_sec = latency.tv_nsec = 0; -+ } else { -+ cras_client_calc_playback_latency(&pcm_cras->playback_sample_time, -+ &latency); -+ } -+ - *delayp = limit + io->appl_ptr - pcm_cras->playback_sample_index + - latency.tv_sec * io->rate + -- latency.tv_nsec / (1000000000L / io->rate); -+ latency.tv_nsec / (1000000000L / (long)io->rate); - } else { -- cras_client_calc_capture_latency(&pcm_cras->capture_sample_time, -- &latency); -+ /* Do not compute latency if capture_sample_time is not set */ -+ if (pcm_cras->capture_sample_time.tv_sec == 0 && -+ pcm_cras->capture_sample_time.tv_nsec == 0) { -+ latency.tv_sec = latency.tv_nsec = 0; -+ } else { -+ cras_client_calc_capture_latency(&pcm_cras->capture_sample_time, -+ &latency); -+ } -+ - *delayp = limit + pcm_cras->capture_sample_index - io->appl_ptr + - latency.tv_sec * io->rate + -- latency.tv_nsec / (1000000000L / io->rate); -+ latency.tv_nsec / (1000000000L / (long)io->rate); - } - - /* Both appl and hw pointers wrap at the pcm boundary. */ -END - fi - - # Fix cras_test_client hanging on exit, see http://crbug.com/347997 - # Introduced in upstream build 5339, fixed in 5579 - # FIXME: Remove this when R34/5500 is not an active release anymore - if [ "$CROS_VER_1" -ge 5339 -a "$CROS_VER_1" -lt 5579 ]; then - echo "Patching CRAS (cras_test_client bug)..." 1>&2 - patch -p3 <server_state) - shmdt(client->server_state); - if (client->server_fd >= 0) -END - fi - - # CRAS needs to be patched so that x86 client can access x86_64 server - # Applied upstream in build 5652, see http://crbug.com/309961 - # FIXME: Remove this when 5652 has trickled down to stable - if [ "$CROS_VER_1" -lt 5652 ] && [ "`uname -m`" = "x86_64" ] && - [ "$ARCH" = "i386" -o "$cras_arch" = "i386" ]; then - patch_cras_x86_x86_64 - fi - # Fix volume control, see crbug.com/415661 # Applied upstream in build 6283 # FIXME: Remove this when 6283 has trickled down to stable @@ -514,6 +333,25 @@ quit: END fi + # Remove SBC dependency + sed -e 's/#include common/cras_sbc_codec.c < +#include +#include "cras_audio_codec.h" + +struct cras_audio_codec *cras_sbc_codec_create(uint8_t freq, + uint8_t mode, uint8_t subbands, uint8_t alloc, + uint8_t blocks, uint8_t bitpool) { + abort(); +} +void cras_sbc_codec_destroy(struct cras_audio_codec *codec) { + abort(); +} +END + # Drop SBC constants + sed -e 's/SBC_[A-Z0-9_]*/0/g' -i tests/cras_test_client.c + # Directory to install CRAS library/binaries CRASLIBDIR="/usr/local$archextrapath/lib" CRASBINDIR="/usr/local$archextrapath/bin" @@ -521,112 +359,9 @@ END echo "Compiling CRAS (${cras_arch:-native})..." 1>&2 # Convert Makefile.am to a shell script, and run it. { - echo ' - top_srcdir=".." - top_builddir=".." - SBC_LIBS="'"`PKG_CONFIG_PATH="$pkgconfigpath" \ - pkg-config --libs sbc`"'" - SBC_CFLAGS="'"`PKG_CONFIG_PATH="$pkgconfigpath" \ - pkg-config --cflags sbc`"'" - ' - sed -e ' - # Concatenate lines ending in \ - : start; /\\$/{N; b start} - s/ *\\\n[ \t]*/ /g - # Convert automake to shell - s/^[^ ]*:/#\0/ - s/^\t/#\0/ - s/ *= */=/ - s/\([^ ]*\) *+= */\1=${\1}\ / - s/ /\\ /g - y/()/{}/ - ' 'Makefile.am' - echo ' - # buildsources: Build all source files for target - # $1: target - # $2: additional gcc flags - # Prints a list of .o files - buildsources() { - local target="$1" - local extragccflags="$2" - - eval local sources=\"\$${target}_SOURCES\" - eval local cppflags=\"\$${target}_CPPFLAGS\" - - for dep in $sources; do - if [ "${dep%.c}" != "$dep" ]; then - ofile="${dep%.c}.o" - gcc -c "$dep" -o "$ofile" '"$archgccflags"' \ - $cppflags $extragccflags 1>&2 || return $? - echo -n "$ofile " - fi - done - } - - # fixlibadd: - # Fix list of libraries ($1): replace lib.la by -l - fixlibadd() { - for libdep in $*; do - if [ "${libdep%.la}" != "$libdep" ]; then - libdep="${libdep%.la}" - libdep="-l${libdep#lib}" - fi - echo -n "$libdep " - done - } - - # buildlib: Build a library - # $1: library name - # $2: additional linker flags - buildlib() { - local lib="$1" - local extraflags="$2" - local ofiles - # local eats the return status: separate the 2 statements - ofiles="`buildsources "${lib}_la" "-fPIC -DPIC"`" - - eval local libadd=\"\$${lib}_la_LIBADD\" - eval local ldflags=\"\$${lib}_la_LDFLAGS\" - - libadd="`fixlibadd $libadd`" - - # Detect library version (e.g. 0.0.0) - local fullver="`echo -n "$ldflags" | \ - sed -n '\''y/:/./; \ - s/.*-version-info \([0-9.]*\)$/\\1/p'\''`" - local shortver="" - # Get "short" library version (e.g. 0) - if [ -n "$fullver" ]; then - shortver=".${fullver%%.*}" - fullver=".$fullver" - fi - local fullso="$lib.so$fullver" - local shortso="$lib.so$shortver" - gcc -shared -fPIC -DPIC $ofiles $libadd -o "$fullso" \ - '"$archgccflags"' $extraflags -Wl,-soname,"$shortso" - if [ -n "$fullver" ]; then - ln -sf "$fullso" "$shortso" - # Needed at link-time only - ln -sf "$shortso" "$lib.so" - fi - } - - # buildexe: Build an executable file - # $1: executable file name - # $2: additional linker flags - buildexe() { - local exe="$1" - local extraflags="$2" - local ofiles="`buildsources "$exe" ""`" - - eval local ldadd=\"\$${exe}_LDADD\" - eval local ldflags=\"\$${exe}_LDFLAGS\" - - ldadd="`fixlibadd $ldadd`" - - gcc $ofiles $ldadd -o "$exe" '"$archgccflags"' $extraflags - } + convert_automake + echo ' buildlib libcras # Pass -rpath=$CRASLIBDIR to linker, so we do not need to add diff --git a/targets/chrome-common b/targets/chrome-common index f11265c39..2766c7c53 100644 --- a/targets/chrome-common +++ b/targets/chrome-common @@ -9,12 +9,12 @@ # This command expects CHANNEL to be set with the requested Google Chrome # channel: stable, beta, or unstable. -if [ -z "$TARGETNOINSTALL" -a "${ARCH#arm}" != "$ARCH" ]; then +if [ "${TARGETNOINSTALL:-c}" = 'c' -a "${ARCH#arm}" != "$ARCH" ]; then echo 'Google Chrome does not yet have an ARM build. Installing Chromium instead.' 1>&2 TARGET=chromium . "${TARGETSDIR:="$PWD"}/$TARGET" fi -if [ -z "$TARGETNOINSTALL" -a "$TARGET" != 'chrome' ]; then +if [ "${TARGETNOINSTALL:-c}" = 'c' -a "$TARGET" != 'chrome' ]; then # Avoid installing multiple chrome targets if grep -q "^chrome\$" "${TARGETDEDUPFILE:-/dev/null}"; then exit diff --git a/targets/chromium b/targets/chromium index 1f1649313..1f7d6e5d2 100644 --- a/targets/chromium +++ b/targets/chromium @@ -2,7 +2,7 @@ # Copyright (c) 2014 The crouton Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -if [ -z "$TARGETNOINSTALL" ] && +if [ "${TARGETNOINSTALL:-c}" = 'c' ] && [ "$DISTRO" = 'debian' -o "$DISTRO" = 'kali' ] && [ "${ARCH#arm}" != "$ARCH" ]; then error 99 "chromium target is not supported on Debian/ARM." diff --git a/targets/common b/targets/common index 46cd406e5..70ef712b0 100644 --- a/targets/common +++ b/targets/common @@ -32,6 +32,14 @@ if [ "$TARGETS" = 'help' ]; then if [ -n "$REQUIRES" ]; then echo " Requires: $REQUIRES" fi +elif [ "$TARGETNOINSTALL" = "p" ]; then + # This will check the provides from /etc/crouton/targets and targets + # specified with -t. If there are duplicates we will respect the + # order of the targets specified. This means on an update targets + # specified with -t will have higher priority then already installed targets. + for t in $PROVIDES; do + echo "$t=$TARGET" >> "${PROVIDESFILE:-/dev/null}" + done elif [ -z "$TARGETNOINSTALL" -o -n "$RESTOREHOSTBIN" ]; then # Avoid double-adding targets if grep -q "^$TARGET\$" "${TARGETDEDUPFILE:-/dev/null}"; then @@ -45,6 +53,9 @@ elif [ -z "$TARGETNOINSTALL" -o -n "$RESTOREHOSTBIN" ]; then done # Source the prerequisites for t in $REQUIRES; do + # If it is already provided change it to the target what provides it. + provider="`awk -F= '/'"$t"'=/{print $2; exit}' "${PROVIDESFILE:-/dev/null}"`" + t="${provider:-"$t"}" (TARGET="$t" PROVIDES='' HOSTBIN='' CHROOTBIN='' CHROOTETC='' . "$TARGETSDIR/$t") done @@ -82,7 +93,8 @@ elif [ -z "$TARGETNOINSTALL" -o -n "$RESTOREHOSTBIN" ]; then # Lines with "### append filename" will queue a file to be processed after # the current file is done. # Lines that start with "compile" will have their source code inserted as a - # HERE document. + # HERE document. In that case, we also look for local include file, and + # insert them as needed. t="$TARGET" if [ "${t#/}" = "$t" ]; then t="$TARGETSDIR/$t" @@ -103,7 +115,23 @@ elif [ -z "$TARGETNOINSTALL" -o -n "$RESTOREHOSTBIN" ]; then } src && $NF != substr("\\\\", 1, 1) { print $0 " < 0) { + if (line ~ /^#include \".*\.h\"$/) { + include = line + sub(/^#include \"/, "", include) + sub(/\"$/, "", include) + include = "'"${SRCDIR:-$TARGETSDIR/../src}"'/" include; + print "// BEGIN " include + while ((getline line < include) > 0) { + print line + } + close(include) + print "// END " include + } else { + print line + } + } + close(src) print "EOF" src = ""; next diff --git a/targets/core b/targets/core index 72a09494d..9d9f82091 100644 --- a/targets/core +++ b/targets/core @@ -4,7 +4,7 @@ # found in the LICENSE file. REQUIRES='' DESCRIPTION='Performs core system configuration. Most users would want this.' -CHROOTBIN='croutonversion host-dbus host-x11' +CHROOTBIN='brightness croutonpowerd croutonversion host-dbus host-x11' . "${TARGETSDIR:="$PWD"}/common" ### Append to prepare.sh: @@ -108,12 +108,14 @@ fi fixkeyboardmode # Install critical packages -install --minimal sudo wget ca-certificates +install --minimal sudo wget ca-certificates apt-transport-https # Generate and set default locale if [ ! -f '/etc/default/locale' ] && hash locale-gen 2>/dev/null; then locale-gen --lang en_US.UTF-8 - update-locale LANG=en_US.UTF-8 + if [ "${DISTROAKA:-"$DISTRO"}" = 'debian' ]; then + dpkg-reconfigure locales + fi fi # Link debian_chroot to the chroot name diff --git a/targets/e17 b/targets/e17 index 92a3846c3..ec5afd572 100644 --- a/targets/e17 +++ b/targets/e17 @@ -2,7 +2,7 @@ # Copyright (c) 2014 The crouton Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -if [ -z "$TARGETNOINSTALL" ] && release -lt jessie -eq kali; then +if [ "${TARGETNOINSTALL:-c}" = 'c' ] && release -lt jessie -eq kali; then error 99 "e17 target is not supported on wheezy/kali." fi REQUIRES='gtk-extra' diff --git a/targets/extension b/targets/extension index d2e08d5e5..411fdcece 100644 --- a/targets/extension +++ b/targets/extension @@ -4,7 +4,7 @@ # found in the LICENSE file. REQUIRES='x11' DESCRIPTION='Clipboard synchronization and URL handling with Chromium OS.' -CHROOTBIN='croutonclip croutonurlhandler' +CHROOTBIN='croutonclip croutonnotify croutonurlhandler' EXTENSION='gcpneefbbnfalgjniomfjknbcgkbijom' # Check if the extension is installed and add a mark to the preparation script @@ -23,8 +23,14 @@ install x11-utils xclip compile websocket '' -# XMETHOD is defined in x11 (or xephyr), which this package depends on -if [ "$XMETHOD" = 'x11' ]; then +# vtmonitor is needed for supporting xorg. +# There are three ways xorg might be installed relative to extension: via a +# dependency before extension, via explicit selection before extension, and via +# explicit selection after extension. +# The first is handled by checking XMETHOD, as the first X11 backend brought in +# (explicit or otherwise) sets XMETHOD permanently. The latter two are handled +# by greping the explicit targets file for xorg. +if [ "$XMETHOD" = 'xorg' ] || grep -q xorg /etc/crouton/targets; then compile vtmonitor '' fi diff --git a/targets/gnome b/targets/gnome index d68c66a45..70bb709f7 100644 --- a/targets/gnome +++ b/targets/gnome @@ -5,7 +5,7 @@ REQUIRES='gtk-extra' DESCRIPTION='Installs the GNOME desktop environment. (Approx. 400MB)' HOSTBIN='startgnome' -CHROOTBIN='startgnome gnome-session-wrapper' +CHROOTBIN='crouton-noroot startgnome gnome-session-wrapper' . "${TARGETSDIR:="$PWD"}/common" ### Append to prepare.sh: diff --git a/targets/gnome-desktop b/targets/gnome-desktop new file mode 100644 index 000000000..8e8256bd5 --- /dev/null +++ b/targets/gnome-desktop @@ -0,0 +1,13 @@ +#!/bin/sh -e +# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +if [ "${TARGETNOINSTALL:-c}" = 'c' ] && release -lt quantal; then + error 99 "gnome-desktop target is not available in $RELEASE." +fi +REQUIRES='gnome' +DESCRIPTION='Installs GNOME along with common applications. (Approx. 1100MB)' +. "${TARGETSDIR:="$PWD"}/common" + +### Append to prepare.sh: +install ubuntu=ubuntu-gnome-desktop,task-gnome-desktop -- network-manager xorg diff --git a/targets/kde b/targets/kde index ff1137b45..57a1a6571 100644 --- a/targets/kde +++ b/targets/kde @@ -5,6 +5,7 @@ REQUIRES='x11' DESCRIPTION='Installs a minimal KDE desktop environment. (Approx. 600MB)' HOSTBIN='startkde' +CHROOTBIN='crouton-noroot startkde' . "${TARGETSDIR:="$PWD"}/common" ### Append to prepare.sh: diff --git a/targets/kde-desktop b/targets/kde-desktop new file mode 100644 index 000000000..0e03f0f2d --- /dev/null +++ b/targets/kde-desktop @@ -0,0 +1,10 @@ +#!/bin/sh -e +# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +REQUIRES='kde' +DESCRIPTION='Installs KDE along with common applications. (Approx. 1000MB)' +. "${TARGETSDIR:="$PWD"}/common" + +### Append to prepare.sh: +install ubuntu=kubuntu-desktop,task-kde-desktop -- network-manager diff --git a/targets/lxde b/targets/lxde index 177a51ff6..512e98e8b 100644 --- a/targets/lxde +++ b/targets/lxde @@ -5,6 +5,7 @@ REQUIRES='gtk-extra' DESCRIPTION='Installs the LXDE desktop environment. (Approx. 200MB)' HOSTBIN='startlxde' +CHROOTBIN='crouton-noroot startlxde' . "${TARGETSDIR:="$PWD"}/common" ### Append to prepare.sh: diff --git a/targets/lxde-desktop b/targets/lxde-desktop new file mode 100644 index 000000000..6a61c706d --- /dev/null +++ b/targets/lxde-desktop @@ -0,0 +1,10 @@ +#!/bin/sh -e +# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +REQUIRES='lxde' +DESCRIPTION='Installs LXDE along with common applications. (Approx. 800MB)' +. "${TARGETSDIR:="$PWD"}/common" + +### Append to prepare.sh: +install ubuntu=lubuntu-desktop,task-lxde-desktop -- network-manager diff --git a/targets/post-common b/targets/post-common index 09f261b10..a9a8dfc15 100644 --- a/targets/post-common +++ b/targets/post-common @@ -35,7 +35,7 @@ if [ "${DISTROAKA:-"$DISTRO"}" = 'debian' ]; then fi # Add the primary user -groups='audio,video,sudo,plugdev' +groups='audio,input,video,sudo,plugdev' if ! grep -q ':1000:' /etc/passwd; then while ! echo "$username" | grep -q '^[a-z][-a-z0-9_]*$'; do if [ -n "$username" ]; then diff --git a/targets/touch b/targets/touch index bd6fd1026..2f682b906 100644 --- a/targets/touch +++ b/targets/touch @@ -2,7 +2,7 @@ # Copyright (c) 2014 The crouton Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -if [ -z "$TARGETNOINSTALL" ] && +if [ "${TARGETNOINSTALL:-c}" = 'c' ] && [ "$DISTRO" = 'debian' -o "$DISTRO" = 'kali' ]; then error 99 "touch target is not supported on Debian." fi diff --git a/targets/unity b/targets/unity index b2e5ec8a6..98ce93585 100644 --- a/targets/unity +++ b/targets/unity @@ -2,17 +2,17 @@ # Copyright (c) 2014 The crouton Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -if [ -z "$TARGETNOINSTALL" ]; then +if [ "${TARGETNOINSTALL:-c}" = 'c' ]; then if [ "$DISTRO" != 'ubuntu' ]; then error 99 "unity target is only supported on Ubuntu." - elif [ -a ! "${ARCH#arm}" = "$ARCH" ] && ! release -eq precise; then + elif [ "${ARCH#arm}" != "$ARCH" ] && ! release -eq precise; then error 99 "unity target does not work in xephyr in $RELEASE due to missing egl support." fi fi REQUIRES='gtk-extra' DESCRIPTION='Installs the Unity desktop environment. (Approx. 700MB)' HOSTBIN='startunity' -CHROOTBIN='startunity gnome-session-wrapper crouton-unity-autostart' +CHROOTBIN='crouton-noroot startunity gnome-session-wrapper crouton-unity-autostart' CHROOTETC='unity-autostart.desktop unity-profiled' . "${TARGETSDIR:="$PWD"}/common" diff --git a/targets/unity-desktop b/targets/unity-desktop new file mode 100644 index 000000000..ef066fbb2 --- /dev/null +++ b/targets/unity-desktop @@ -0,0 +1,10 @@ +#!/bin/sh -e +# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +REQUIRES='unity' +DESCRIPTION='Installs Unity along with common applications. (Approx. 1100MB)' +. "${TARGETSDIR:="$PWD"}/common" + +### Append to prepare.sh: +install ubuntu-desktop -- network-manager xorg diff --git a/targets/x11 b/targets/x11 index 1d624742f..229342669 100644 --- a/targets/x11 +++ b/targets/x11 @@ -2,8 +2,17 @@ # Copyright (c) 2014 The crouton Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -if [ "${ARCH#arm}" != "$ARCH" ]; then - REQUIRES='xephyr' +if grep -q 'SAMSUNG EXYNOS' /proc/cpuinfo || \ + ([ "${ARCH#arm}" != "$ARCH" ] && \ + awk -F= '/_RELEASE_VERSION=/ { exit !(int($2) < 6689) }' \ + '/etc/lsb-release'); then + # Xorg won't work on Samsung ARM devices or K1 on release less than 6689 + # But if we're on Freon, we can't use xephyr either, so xiwi's all we have + if [ -f /sbin/frecon ]; then + REQUIRES='xiwi' + else + REQUIRES='xephyr' + fi else REQUIRES='xorg' fi diff --git a/targets/x11-common b/targets/x11-common index 203a6280a..4219b561a 100644 --- a/targets/x11-common +++ b/targets/x11-common @@ -8,7 +8,25 @@ ### Append to prepare.sh: # Store and apply the X11 method -ln -sfT "/etc/crouton/xserverrc-$XMETHOD" '/etc/X11/xinit/xserverrc' +ln -sfT '/etc/crouton/xserverrc' '/etc/X11/xinit/xserverrc' +echo "$XMETHOD" > '/etc/crouton/xmethod' + +# Only apply hack if vgem card exists +if [ -c '/dev/dri/card1' ]; then + # Modify the Xorg executable to *not* poll udev for cards + offset="`grep -F -m 1 -boa 'card[0-9]*' /usr/bin/Xorg 2>/dev/null || true`" + if [ -n "$offset" ]; then + echo -n 'croutonhax' | dd seek="${offset%:*}" bs=1 of=/usr/bin/Xorg \ + conv=notrunc,nocreat 2>/dev/null + fi +elif grep -q 'croutonhax' '/usr/bin/Xorg'; then + # Undo hack since there's no vgem module + offset="`grep -F -m 1 -boa 'croutonhax' /usr/bin/Xorg 2>/dev/null || true`" + if [ -n "$offset" ]; then + echo -n 'card[0-9]*' | dd seek="${offset%:*}" bs=1 of=/usr/bin/Xorg \ + conv=notrunc,nocreat 2>/dev/null + fi +fi # Install utility for croutoncycle compile wmtools '-lX11' libx11-dev @@ -19,8 +37,11 @@ install --minimal dbus xdg-utils ln -sf croutonpowerd /usr/local/bin/gnome-screensaver-command ln -sf croutonpowerd /usr/local/bin/xscreensaver-command -# Install xbindkeys and xautomation for key shortcuts and kbd for chvt -install --minimal xbindkeys xautomation kbd +# Install nicer cursors +install --minimal dmz-cursor-theme + +# Install bsdmainutils, xbindkeys and xautomation for shortcuts; kbd for chvt +install --minimal bsdmainutils xbindkeys xautomation kbd # Allow users to run sudo chvt without password, so we don't need to run # croutoncycle as root echo '%sudo ALL = NOPASSWD:/bin/chvt' > /etc/sudoers.d/chvt diff --git a/targets/xbmc b/targets/xbmc index bcf279efd..956114720 100644 --- a/targets/xbmc +++ b/targets/xbmc @@ -5,11 +5,31 @@ REQUIRES='x11' DESCRIPTION='Installs the XBMC media player. (Approx. 140MB)' HOSTBIN='startxbmc' +CHROOTETC='xbmc-keyboard.xml xbmc-cycle.py' . "${TARGETSDIR:="$PWD"}/common" ### Append to prepare.sh: install xbmc +# Configure keymaps xbmc for the hotkeys ctr-shift-alt F1/F2 to +# cycle through chroots/chromeos. We use ~/.xbmc/userdata/keymaps/keyboard.xml +# for this purpose, but the main user may not have been created yet, so we +# add a script in /etc/profile.d to link ~/.xbmc/userdata/keymaps/keyboard.xml +# to /etc/crouton/xbmc-keyboard.xml + +profiledsh='/etc/profile.d/crouton-xbmc-keymaps.sh' +# Make sure symbolic link is setup on login +echo '#!/bin/sh + +keyboardxmldir="$HOME/.xbmc/userdata/keymaps" +# Do not install if user is root, or $HOME does not exist +if [ "`id -u`" -ne 0 -a -d "$HOME" -a ! -e "$keyboardxmldir/keyboard.xml" ]; then + mkdir -p "$keyboardxmldir" + ln -sfT /etc/crouton/xbmc-keyboard.xml "$keyboardxmldir/keyboard.xml" +fi' > "$profiledsh" + +chmod 755 "$profiledsh" + TIPS="$TIPS You can start XBMC via the startxbmc host command: sudo startxbmc " diff --git a/targets/xephyr b/targets/xephyr index 16f775f02..aaaa0c5d4 100644 --- a/targets/xephyr +++ b/targets/xephyr @@ -2,11 +2,14 @@ # Copyright (c) 2014 The crouton Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -REQUIRES='core audio' +if [ "${TARGETNOINSTALL:-c}" = 'c' -a -f /sbin/frecon ]; then + error 1 'xephyr target does not work with Freon. Use xiwi instead.' +fi +REQUIRES='audio' PROVIDES='x11' DESCRIPTION='Nested X11 backend. Improves compatibility but lacks GPU accel.' -CHROOTBIN='brightness croutoncycle croutonpowerd croutonwheel croutonxinitrc-wrapper xinit' -CHROOTETC='xbindkeysrc.scm xserverrc-xephyr xserverrc-local.example' +CHROOTBIN='croutoncycle croutontriggerd croutonwheel croutonxinitrc-wrapper xinit' +CHROOTETC='xbindkeysrc.scm xserverrc xserverrc-xephyr xserverrc-local.example' . "${TARGETSDIR:="$PWD"}/common" ### Append to prepare.sh: @@ -29,7 +32,7 @@ fi # xserver-xephyr won't auto replace the manually-downloaded version. install --minimal \ - xserver-xephyr xinit dmz-cursor-theme libgl1-mesa-dri \ + xserver-xephyr xinit libgl1-mesa-dri \ x11-utils x11-xserver-utils xinput xterm # Compile croutoncursor diff --git a/targets/xfce b/targets/xfce index f2e91ebc1..1d829bae2 100644 --- a/targets/xfce +++ b/targets/xfce @@ -5,6 +5,7 @@ REQUIRES='gtk-extra' DESCRIPTION='Installs the Xfce desktop environment. (Approx. 250MB)' HOSTBIN='startxfce4' +CHROOTBIN='crouton-noroot startxfce4' . "${TARGETSDIR:="$PWD"}/common" ### Append to prepare.sh: diff --git a/targets/xfce-desktop b/targets/xfce-desktop new file mode 100644 index 000000000..edb539a22 --- /dev/null +++ b/targets/xfce-desktop @@ -0,0 +1,10 @@ +#!/bin/sh -e +# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +REQUIRES='xfce' +DESCRIPTION='Installs Xfce along with common applications. (Approx. 1200MB)' +. "${TARGETSDIR:="$PWD"}/common" + +### Append to prepare.sh: +install ubuntu=xubuntu-desktop,task-xfce-desktop -- network-manager xorg diff --git a/targets/xiwi b/targets/xiwi new file mode 100644 index 000000000..fae487c1d --- /dev/null +++ b/targets/xiwi @@ -0,0 +1,411 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +REQUIRES='audio extension' +PROVIDES='x11' +DESCRIPTION='X.org X11 backend running unaccelerated in a Chromium OS window.' +CHROOTBIN='croutoncycle croutonfindnacl croutontriggerd croutonxinitrc-wrapper setres xinit xiwi' +CHROOTETC='xbindkeysrc.scm xiwi.conf xorg-dummy.conf xserverrc xserverrc-xiwi xserverrc-local.example' +. "${TARGETSDIR:="$PWD"}/common" + +### Append to prepare.sh: +XMETHOD="${XMETHOD:-xiwi}" + +ltspackages='' +# On non-ARM precise, install lts-trusty xorg server for compatibility with xorg +# if kernel version != 3.4 (lts-trusty mesa requires version >=3.6) +if [ "${ARCH#arm}" = "$ARCH" ] && release -eq precise \ + && ! uname -r | grep -q "^3.4."; then + # We still install xorg later to pull in its dependencies + ltspackages='-lts-trusty' + install --minimal "xserver-xorg$ltspackages" "libgl1-mesa-glx$ltspackages" +fi + +# Unhold xserver-xorg-video-dummy to make sure deps are pulled in +if [ "${DISTROAKA:-"$DISTRO"}" = 'debian' ]; then + apt-mark unhold xserver-xorg-video-dummy$ltspackages || true 2>/dev/null +fi + +install xorg xserver-xorg-video-dummy$ltspackages +install --minimal i3 + +# Compile croutonfbserver +compile fbserver '-lX11 -lXfixes -lXdamage -lXext -lXtst' \ + libx11-dev libxfixes-dev libxdamage-dev libxext-dev libxtst-dev + +# Make croutonfbserver setuid root. See issue #1411; this is way insecure +chmod u+s /usr/local/bin/croutonfbserver + +ln -sf /etc/crouton/xorg-dummy.conf /etc/X11/ + +# Download the latest xf86-video-dummy package +urlbase="http://xorg.freedesktop.org/releases/individual/driver/" + +DUMMYBUILDTMP="`mktemp -d crouton-cras.XXXXXX --tmpdir=/tmp`" + +addtrap "rm -rf --one-file-system '$DUMMYBUILDTMP'" + +echo "Download Xorg dummy driver..." 1>&2 + +wget -O "$DUMMYBUILDTMP/dummy.tar.gz" "$urlbase/xf86-video-dummy-0.3.7.tar.gz" + +install --minimal --asdeps patch gcc libc-dev pkg-config \ + xserver-xorg-dev$ltspackages x11proto-xf86dga-dev + +( + cd "$DUMMYBUILDTMP" + # -m prevents "time stamp is in the future" spam + tar --strip-components=1 -xmf dummy.tar.gz + + echo "Patching Xorg dummy driver (xrandr 1.2 support)..." 1>&2 + patch -p1 < + #endif + ++#include "xf86Crtc.h" ++ + /* + * Driver data structures. + */ +@@ -178,6 +180,115 @@ dummySetup(pointer module, pointer opts, int *errmaj, int *errmin) + #endif /* XFree86LOADER */ + + static Bool ++size_valid(ScrnInfoPtr pScrn, int width, int height) ++{ ++ /* Guard against invalid parameters */ ++ if (width == 0 || height == 0 || ++ width > DUMMY_MAX_WIDTH || height > DUMMY_MAX_HEIGHT) ++ return FALSE; ++ ++ /* videoRam is in kb, divide first to avoid 32-bit int overflow */ ++ if ((width*height+1023)/1024*pScrn->bitsPerPixel/8 > pScrn->videoRam) ++ return FALSE; ++ ++ return TRUE; ++} ++ ++static Bool ++dummy_xf86crtc_resize(ScrnInfoPtr pScrn, int width, int height) ++{ ++ int old_width, old_height; ++ ++ old_width = pScrn->virtualX; ++ old_height = pScrn->virtualY; ++ ++ if (size_valid(pScrn, width, height)) { ++ PixmapPtr rootPixmap; ++ ScreenPtr pScreen = pScrn->pScreen; ++ ++ pScrn->virtualX = width; ++ pScrn->virtualY = height; ++ ++ rootPixmap = pScreen->GetScreenPixmap(pScreen); ++ if (!pScreen->ModifyPixmapHeader(rootPixmap, width, height, ++ -1, -1, -1, NULL)) { ++ pScrn->virtualX = old_width; ++ pScrn->virtualY = old_height; ++ return FALSE; ++ } ++ ++ pScrn->displayWidth = rootPixmap->devKind / ++ (rootPixmap->drawable.bitsPerPixel / 8); ++ ++ return TRUE; ++ } else { ++ return FALSE; ++ } ++} ++ ++static const xf86CrtcConfigFuncsRec dummy_xf86crtc_config_funcs = { ++ dummy_xf86crtc_resize ++}; ++ ++static xf86OutputStatus ++dummy_output_detect(xf86OutputPtr output) ++{ ++ return XF86OutputStatusConnected; ++} ++ ++static int ++dummy_output_mode_valid(xf86OutputPtr output, DisplayModePtr pMode) ++{ ++ if (size_valid(output->scrn, pMode->HDisplay, pMode->VDisplay)) { ++ return MODE_OK; ++ } else { ++ return MODE_MEM; ++ } ++} ++ ++static DisplayModePtr ++dummy_output_get_modes(xf86OutputPtr output) ++{ ++ return NULL; ++} ++ ++static void ++dummy_output_dpms(xf86OutputPtr output, int dpms) ++{ ++ return; ++} ++ ++static const xf86OutputFuncsRec dummy_output_funcs = { ++ .detect = dummy_output_detect, ++ .mode_valid = dummy_output_mode_valid, ++ .get_modes = dummy_output_get_modes, ++ .dpms = dummy_output_dpms, ++}; ++ ++static Bool ++dummy_crtc_set_mode_major(xf86CrtcPtr crtc, DisplayModePtr mode, ++ Rotation rotation, int x, int y) ++{ ++ crtc->mode = *mode; ++ crtc->x = x; ++ crtc->y = y; ++ crtc->rotation = rotation; ++ ++ return TRUE; ++} ++ ++static void ++dummy_crtc_dpms(xf86CrtcPtr output, int dpms) ++{ ++ return; ++} ++ ++static const xf86CrtcFuncsRec dummy_crtc_funcs = { ++ .set_mode_major = dummy_crtc_set_mode_major, ++ .dpms = dummy_crtc_dpms, ++}; ++ ++static Bool + DUMMYGetRec(ScrnInfoPtr pScrn) + { + /* +@@ -283,6 +394,8 @@ DUMMYPreInit(ScrnInfoPtr pScrn, int flags) + DUMMYPtr dPtr; + int maxClock = 230000; + GDevPtr device = xf86GetEntityInfo(pScrn->entityList[0])->device; ++ xf86OutputPtr output; ++ xf86CrtcPtr crtc; + + if (flags & PROBE_DETECT) + return TRUE; +@@ -346,13 +459,6 @@ DUMMYPreInit(ScrnInfoPtr pScrn, int flags) + if (!xf86SetDefaultVisual(pScrn, -1)) + return FALSE; + +- if (pScrn->depth > 1) { +- Gamma zeros = {0.0, 0.0, 0.0}; +- +- if (!xf86SetGamma(pScrn, zeros)) +- return FALSE; +- } +- + xf86CollectOptions(pScrn, device->options); + /* Process the options */ + if (!(dPtr->Options = malloc(sizeof(DUMMYOptions)))) +@@ -382,64 +488,45 @@ DUMMYPreInit(ScrnInfoPtr pScrn, int flags) + maxClock); + } + +- pScrn->progClock = TRUE; +- /* +- * Setup the ClockRanges, which describe what clock ranges are available, +- * and what sort of modes they can be used for. +- */ +- clockRanges = (ClockRangePtr)xnfcalloc(sizeof(ClockRange), 1); +- clockRanges->next = NULL; +- clockRanges->ClockMulFactor = 1; +- clockRanges->minClock = 11000; /* guessed ยงยงยง */ +- clockRanges->maxClock = 300000; +- clockRanges->clockIndex = -1; /* programmable */ +- clockRanges->interlaceAllowed = TRUE; +- clockRanges->doubleScanAllowed = TRUE; +- +- /* Subtract memory for HW cursor */ +- +- +- { +- int apertureSize = (pScrn->videoRam * 1024); +- i = xf86ValidateModes(pScrn, pScrn->monitor->Modes, +- pScrn->display->modes, clockRanges, +- NULL, 256, DUMMY_MAX_WIDTH, +- (8 * pScrn->bitsPerPixel), +- 128, DUMMY_MAX_HEIGHT, pScrn->display->virtualX, +- pScrn->display->virtualY, apertureSize, +- LOOKUP_BEST_REFRESH); +- +- if (i == -1) +- RETURN; +- } ++ xf86CrtcConfigInit(pScrn, &dummy_xf86crtc_config_funcs); ++ ++ xf86CrtcSetSizeRange(pScrn, 256, 256, DUMMY_MAX_WIDTH, DUMMY_MAX_HEIGHT); ++ ++ crtc = xf86CrtcCreate(pScrn, &dummy_crtc_funcs); ++ ++ output = xf86OutputCreate (pScrn, &dummy_output_funcs, "default"); ++ ++ output->possible_crtcs = 0x7f; + +- /* Prune the modes marked as invalid */ +- xf86PruneDriverModes(pScrn); ++ xf86InitialConfiguration(pScrn, TRUE); ++ ++ if (pScrn->depth > 1) { ++ Gamma zeros = {0.0, 0.0, 0.0}; ++ ++ if (!xf86SetGamma(pScrn, zeros)) ++ return FALSE; ++ } + +- if (i == 0 || pScrn->modes == NULL) { ++ if (pScrn->modes == NULL) { + xf86DrvMsg(pScrn->scrnIndex, X_ERROR, "No valid modes found\n"); + RETURN; + } + +- /* +- * Set the CRTC parameters for all of the modes based on the type +- * of mode, and the chipset's interlace requirements. +- * +- * Calling this is required if the mode->Crtc* values are used by the +- * driver and if the driver doesn't provide code to set them. They +- * are not pre-initialised at all. +- */ +- xf86SetCrtcForModes(pScrn, 0); +- + /* Set the current mode to the first in the list */ + pScrn->currentMode = pScrn->modes; + +- /* Print the list of modes being used */ +- xf86PrintModes(pScrn); ++ /* Set default mode in CRTC */ ++ crtc->funcs->set_mode_major(crtc, pScrn->currentMode, RR_Rotate_0, 0, 0); + + /* If monitor resolution is set on the command line, use it */ + xf86SetDpi(pScrn, 0, 0); + ++ /* Set monitor size based on DPI */ ++ output->mm_width = pScrn->xDpi > 0 ? ++ (pScrn->virtualX * 254 / (10*pScrn->xDpi)) : 0; ++ output->mm_height = pScrn->yDpi > 0 ? ++ (pScrn->virtualY * 254 / (10*pScrn->yDpi)) : 0; ++ + if (xf86LoadSubModule(pScrn, "fb") == NULL) { + RETURN; + } +@@ -559,6 +646,8 @@ DUMMYScreenInit(SCREEN_INIT_ARGS_DECL) + + if (!miSetPixmapDepths ()) return FALSE; + ++ pScrn->displayWidth = pScrn->virtualX; ++ + /* + * Call the framebuffer layer's ScreenInit function, and fill in other + * pScreen fields. +@@ -597,23 +686,6 @@ DUMMYScreenInit(SCREEN_INIT_ARGS_DECL) + if (dPtr->swCursor) + xf86DrvMsg(pScrn->scrnIndex, X_CONFIG, "Using Software Cursor.\n"); + +- { +- +- +- BoxRec AvailFBArea; +- int lines = pScrn->videoRam * 1024 / +- (pScrn->displayWidth * (pScrn->bitsPerPixel >> 3)); +- AvailFBArea.x1 = 0; +- AvailFBArea.y1 = 0; +- AvailFBArea.x2 = pScrn->displayWidth; +- AvailFBArea.y2 = lines; +- xf86InitFBManager(pScreen, &AvailFBArea); +- +- xf86DrvMsg(pScrn->scrnIndex, X_INFO, +- "Using %i scanlines of offscreen memory \n" +- , lines - pScrn->virtualY); +- } +- + xf86SetBackingStore(pScreen); + xf86SetSilkenMouse(pScreen); + +@@ -640,6 +712,9 @@ DUMMYScreenInit(SCREEN_INIT_ARGS_DECL) + | CMAP_RELOAD_ON_MODE_SWITCH)) + return FALSE; + ++ if (!xf86CrtcScreenInit(pScreen)) ++ return FALSE; ++ + /* DUMMYInitVideo(pScreen); */ + + pScreen->SaveScreen = DUMMYSaveScreen; +EOF + + # Fake version 0.3.8 + package="xf86-video-dummy" + major='0' + minor='3' + patch='8' + version="$major.$minor.$patch" + + sed -e ' + s/#undef \(HAVE_.*\)$/#define \1 1/ + s/#undef \(USE_.*\)$/#define \1 1/ + s/#undef \(STDC_HEADERS\)$/#define \1 1/ + s/#undef \(.*VERSION\)$/#define \1 "'$version'"/ + s/#undef \(.*VERSION_MAJOR\)$/#define \1 '$major'/ + s/#undef \(.*VERSION_MINOR\)$/#define \1 '$minor'/ + s/#undef \(.*VERSION_PATCHLEVEL\)$/#define \1 '$patch'/ + s/#undef \(.*PACKAGE_STRING\)$/#define \1 "'"$package $version"'"/ + s/#undef \(.*PACKAGE_*\)$/#define \1 "'$package'"/ + ' config.h.in > config.h + + echo "Compiling Xorg dummy driver..." 1>&2 + + cd src + # Convert Makefile.am to a shell script, and run it. + { + echo ' + DGA=1 + CFLAGS="-std=gnu99 -O2 -g -DHAVE_CONFIG_H -I.. -I." + XORG_CFLAGS="'"`pkg-config --cflags xorg-server`"'" + ' + + convert_automake + + echo ' + buildlib dummy_drv + ' + } | sh -s -e $SETOPTIONS + + echo "Installing Xorg dummy driver..." 1>&2 + + DRIVERDIR="/usr/lib/xorg/modules/drivers" + mkdir -p "$DRIVERDIR/" + /usr/bin/install -s dummy_drv.so "$DRIVERDIR/" +) # End compilation subshell + +if [ "${DISTROAKA:-"$DISTRO"}" = 'debian' ]; then + # Hold xserver-xorg-video-dummy to make sure the driver does not get erased + apt-mark hold xserver-xorg-video-dummy$ltspackages +fi + +TIPS="$TIPS"' +You can open your running chroot desktops by clicking on the extension icon. +Once in a crouton window, press fullscreen or the "switch window" key to switch +back to Chromium OS. + +You can launch individual apps in crouton windows by using the "xiwi" command +in the chroot shell. Wrap that command with enter-chroot to launch directly +from the host shell. Use the enter-chroot parameter -b to run in the background. +Example: sudo enter-chroot -b xiwi xterm +' + +### append x11-common diff --git a/targets/xorg b/targets/xorg index 2688f7051..7df83c6e9 100644 --- a/targets/xorg +++ b/targets/xorg @@ -2,25 +2,38 @@ # Copyright (c) 2014 The crouton Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -if [ -z "$TARGETNOINSTALL" -a "${ARCH#arm}" != "$ARCH" ]; then - error 1 'xorg target does not work on ARM.' +if [ "${TARGETNOINSTALL:-c}" = 'c' ]; then + if grep -q 'SAMSUNG EXYNOS' /proc/cpuinfo; then + error 1 'xorg target does not work on Samsung ARM devices.' + fi fi -REQUIRES='core audio' +REQUIRES='audio' PROVIDES='x11' DESCRIPTION='X.Org X11 backend. Enables GPU acceleration on supported platforms.' -CHROOTBIN='brightness croutoncycle croutonpowerd croutonxinitrc-wrapper setres xinit' -CHROOTETC='xbindkeysrc.scm xserverrc-x11 xserverrc-local.example' +CHROOTBIN='croutoncycle croutontriggerd croutonxinitrc-wrapper setres xinit' +CHROOTETC='xbindkeysrc.scm xorg-intel-sna.conf xserverrc xserverrc-xorg xserverrc-local.example' . "${TARGETSDIR:="$PWD"}/common" ### Append to prepare.sh: -XMETHOD="${XMETHOD:-x11}" +XMETHOD="${XMETHOD:-xorg}" -# On precise, install lts-trusty xorg server to support newer hardware if kernel -# version != 3.4 (lts-trusty mesa requires version >=3.6) -if release -eq precise && ! uname -r | grep -q "^3.4."; then +# Migration from ambiguous XMETHOD +rm -f '/etc/crouton/xserverrc-x11' + +# On Freon, we need crazy xorg hacks +if [ ! -f "/sys/class/tty/tty0/active" ]; then + compile freon '-ldl' so +fi + +ltspackages='' +# On non-ARM precise, install lts-trusty xorg server to support newer hardware +# if kernel version != 3.4 (lts-trusty mesa requires version >=3.6) +if [ "${ARCH#arm}" = "$ARCH" ] && release -eq precise \ + && ! uname -r | grep -q "^3.4."; then # We still install xorg later to pull in its dependencies - install --minimal xserver-xorg-lts-trusty libgl1-mesa-glx-lts-trusty \ - xserver-xorg-input-synaptics-lts-trusty + ltspackages='-lts-trusty' + install --minimal "xserver-xorg$ltspackages" "libgl1-mesa-glx$ltspackages" \ + "xserver-xorg-input-synaptics$ltspackages" fi # On saucy onwards, if kernel version is 3.4, manually pin down old mesa @@ -91,7 +104,21 @@ END apt-get -y --force-yes --no-install-recommends dist-upgrade -f fi -install xorg dmz-cursor-theme +install xorg xserver-xorg-video-fbdev$ltspackages +if [ "${ARCH#arm}" = "$ARCH" ]; then + install xserver-xorg-video-intel$ltspackages +fi + +# This is a system with framebuffer compression, so we need SNA+tearfree +xorgconf='/usr/share/X11/xorg.conf.d/20-crouton-intel-sna.conf' +if grep -q 1 '/sys/module/i915/parameters/i915_enable_fbc' 2>/dev/null; then + mkdir -p "${xorgconf%/*}" + ln -sfT /etc/crouton/xorg-intel-sna.conf "$xorgconf" +else + # In case this got moved to a different system, delete the config + rm -f "$xorgconf" +fi + # Fix launching X11 from inside crosh (user doesn't own a TTY) sed -i 's/allowed_users=.*/allowed_users=anybody/' '/etc/X11/Xwrapper.config' diff --git a/test/autotest_control.template b/test/autotest_control.template index 4cc106eef..72b7695ed 100644 --- a/test/autotest_control.template +++ b/test/autotest_control.template @@ -25,13 +25,6 @@ This test fetches a specific branch of crouton, and runs crouton tests. # For debugging purpose utils.system('cat /etc/lsb-release') -# Add repository so that the test tarball can be fetched -# FIXME: Remove this hack when test is merged -devserver_url = 'http://172.19.140.1:8082' -testcsum = """###TESTCSUM###""" -utils.system("curl '" + devserver_url + "/stage?archive_url=gs://drinkcat-crouton/crouton-test/packages-" + testcsum + "&files=packages.checksum,test-platform_Crouton.tar.bz2'") -job.add_repository([devserver_url + "/static/crouton-test/packages-" + testcsum]) - args_dict = { 'repo': """###REPO###""", 'branch': """###BRANCH###""", diff --git a/test/daemon.sh b/test/daemon.sh index eb2d67db2..6dbafc533 100755 --- a/test/daemon.sh +++ b/test/daemon.sh @@ -38,8 +38,6 @@ MIRRORENV="" # Maximum test run time (minutes): 24 hours MAXTESTRUNTIME="$((24*60))" GSAUTOTEST="gs://chromeos-autotest-results" -# FIXME: Remove this when test is merged -GSCROUTONTEST="gs://drinkcat-crouton/crouton-test/packages" USAGE="$APPLICATION [options] -q QUEUEURL @@ -250,15 +248,6 @@ DUTSSHOPTIONS="-o ConnectTimeout=30 -o IdentityFile=$SSHKEY \ -o ControlPath=$TMPROOT/ssh/%h \ -o ControlMaster=auto -o ControlPersist=10m" -# FIXME: Remove this when test is merged -echo "Building latest test-plaform_Crouton tarball..." 1>&2 -tar cvfj "$TMPROOT/test-platform_Crouton.tar.bz2" \ - -C "$AUTOTESTROOT/client/site_tests/platform_Crouton" . --exclude='*~' -( cd "$TMPROOT"; md5sum test-platform_Crouton.tar.bz2 > packages.checksum ) -TESTCSUM="`cat "$TMPROOT/packages.checksum" | cut -d' ' -f 1`" -gsutil cp "$TMPROOT/test-platform_Crouton.tar.bz2" \ - "$TMPROOT/packages.checksum" "$GSCROUTONTEST-$TESTCSUM/" - syncstatus while sleep "$POLLINTERVAL"; do @@ -381,7 +370,6 @@ while sleep "$POLLINTERVAL"; do -e "s|###BRANCH###|$branch|" \ -e "s|###RUNARGS###|$params|" \ -e "s|###ENV###|$MIRRORENV|" \ - -e "s|###TESTCSUM###|$TESTCSUM|" \ $SCRIPTDIR/test/autotest_control.template \ > "$curtesthostroot/control" diff --git a/test/run.sh b/test/run.sh index 06118c5ab..4301c1a2b 100755 --- a/test/run.sh +++ b/test/run.sh @@ -8,9 +8,12 @@ # Tests numbering is categorical in 10's by the following guide: # 0*: meta-tests, e.g. tester # 1*: core tests, e.g. basic, background, upgrade -# 2*: small-target/tech tests, e.g. cli-extra, audio +# 3*: small-target/tech tests, e.g. cli-extra, audio # 5*: DE tests, e.g. xfce, xbmc # 9*: misc application tests, e.g. chrome +# Alphabetic tests are long, and not run by default: +# w*: Start all DE/wm, and take snapshots +# x*: Install test all targets that do not have tests # Numbering within a category is arbitrary and can have overlaps. # Tests are always run in alphanumeric order unless specified by parameters. @@ -111,7 +114,30 @@ logto() { local AWK='mawk -W interactive' # srand() uses system time as seed but returns previous seed. Call it twice. ((((ret=0; TRAP='' - ( release="$2" . "$1" ) &- || ret=$? + ( + PREFIX="`mktemp -d --tmpdir="$PREFIXROOT" "$tname.XXX"`" + # Remount noexec/etc to make the environment as harsh as possible + mount --bind "$PREFIX" "$PREFIX" + mount -i -o remount,nosuid,nodev,noexec "$PREFIX" + + # Get subshell pid + pid="`sh -c 'echo $PPID'`" + + # Clean up on exit + settrap " + set -x + echo Running trap... + if [ -d '$PREFIX/chroots' ]; then + sh -e '$SCRIPTDIR/host-bin/unmount-chroot' \ + -a -f -y -c '$PREFIX/chroots' + fi + # Kill any leftover subprocess + pkill -9 -P '$pid' + umount -l '$PREFIX' + rm -rf --one-file-system '$PREFIX' + " + release="$2" . "$1" + ) &- || ret=$? sleep 1 if [ "$ret" = 0 ]; then log "TEST PASSED: $retpreamble $ret" @@ -210,12 +236,12 @@ bootstrap() { echo "$file" # Use flock so that bootstrap can be called in parallel - if ! flock -n 3; then + if ! flock -n 4; then log "Waiting for bootstrap for $1 to complete..." - flock 3 + flock 4 elif [ ! -s "$file" ]; then crouton -r "$1" -f "$file" -d 1>&2 - fi 3>>"$file" + fi 4>>"$file" if [ ! -s "$file" ]; then log "FAIL due to incomplete bootstrap for $1" @@ -235,14 +261,14 @@ snapshot() { local name="${3:-"$1"}" # Use flock so that snapshot can be called in parallel - if ! flock -n 3; then + if ! flock -n 4; then log "Waiting for snapshot for $1-$targets to complete..." - flock 3 + flock 4 elif [ ! -s "$file" ]; then crouton -f "`bootstrap "$1"`" -t "$targets" -n "$name" 1>&2 host edit-chroot -y -b -f "$file" "$name" return 0 - fi 3>>"$file" + fi 4>>"$file" # Restore the snapshot into place crouton -f "$file" -n "$name" @@ -378,6 +404,26 @@ SyntaxError: invalid syntax" 1>&2 return 0 } +# Ensures only one test can play with graphics at one time +# Run it without parameters, in a subshell. The lock will be released when +# the subshell exits +vtlock() { + local vtlockfile='/var/lock/croutonvt' + exec 4>>"$vtlockfile" + if ! flock -n 4; then + log 'Waiting for VT lock...' + flock 4 + fi +} + +# Runs the provided command under vtlock +vtlockrun() { + ( + vtlock + "$@" || return $? + ) +} + # Default responses to questions export CROUTON_USERNAME='test' export CROUTON_PASSPHRASE='hunter2' @@ -526,19 +572,6 @@ while true; do tname="${t##*/}.$rel.$try" # Run the test ( - PREFIX="`mktemp -d --tmpdir="$PREFIXROOT" "$tname.XXX"`" - # Remount PREFIX noexec/etc to make the environment as harsh as possible - mount --bind "$PREFIX" "$PREFIX" - mount -i -o remount,nosuid,nodev,noexec "$PREFIX" - # Clean up on exit - settrap " - if [ -d '$PREFIX/chroots' ]; then - sh -e '$SCRIPTDIR/host-bin/unmount-chroot' \ - -a -y -c '$PREFIX/chroots' - fi - umount -l '$PREFIX' - rm -rf --one-file-system '$PREFIX' - " if ! logto "$TESTDIR/$tname" "$t" "$rel" "$try"; then if [ "$((try+1))" -lt "$MAXTRIES" ]; then # Test failed, try again... diff --git a/test/test-mkpart.sh b/test/test-mkpart.sh new file mode 100755 index 000000000..a95b19b21 --- /dev/null +++ b/test/test-mkpart.sh @@ -0,0 +1,169 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +# +# This is a "server-side" test for mkpart.sh. It requires a DUT with a testing +# image installed. +# +# If run without parameter, it tries to lock all ready pool:crouton machines +# from autotest, and run tests on them. In that case, cli/ directory from +# autotest checkout must be in path: +# export PATH=.../crouton-testing/daemon/autotest.git/cli:$PATH + +set -e + +# Modify this as needed +REPO="drinkcat/chroagh" +BRANCH="separate_partition" + +APPLICATION="${0##*/}" +SCRIPTDIR="`readlink -f "\`dirname "$0"\`/.."`" +TESTDIR="$SCRIPTDIR/test/run" +TESTNAME="`sh -e "$SCRIPTDIR/build/genversion.sh" test`" +TESTDIR="$TESTDIR/$TESTNAME" +URL="https://github.com/$REPO/archive/$BRANCH.tar.gz" +SSHOPTS="-o IdentityFile=~/.ssh/testing_rsa -o StrictHostKeyChecking=no \ +-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5" + +# Common functions +. "$SCRIPTDIR/installer/functions" + +if [ -z "$1" ]; then + hosts="`atest host list -b pool:crouton -s Ready -N --unlocked`" + addtrap "atest host mod --unlock $hosts" + atest host mod --lock $hosts + + mkdir -p "$TESTDIR" + echo "Logging to $TESTDIR" + + for host in $hosts; do + ( + echo "Starting test on $host..." 1>&3 + if "$0" "$host.cros"; then + echo "test on $host succeeded..." 1>&3 + else + echo "test on $host failed..." 1>&3 + fi + ) 3>&1 > "$TESTDIR/$host.log" 2>&1 & + done + + wait + + exit 0 +fi + +HOST="$1" + +ssh_run() { + ssh $SSHOPTS root@"$HOST" "sh -exc '$1'" +} + +fetch_crouton() { + ssh_run " + mkdir -p /tmp/mkpart + cd /tmp/mkpart + rm -f '$BRANCH.tar.gz' + wget '$URL' + tar xf '$BRANCH.tar.gz' --strip-components 1" +} + +# wait_host on|off [timeout] +# Waits for host to appear/disappear +wait_host() { + local on="${1:-on}" + local timeout="${2:-300}" + echo "Waiting for host to turn $on..." + while [ "$timeout" -gt 0 ]; do + if [ "$on" = "on" ]; then + if ssh_run true >/dev/null 2>&1; then + break + fi + else + if ! ssh_run true >/dev/null 2>&1; then + break + fi + sleep 5 + fi + timeout="$((timeout-5))" + done + if [ "$timeout" -le 0 ]; then + echo "Timeout..." + exit 1 + fi +} + +check_no_crouton_partition() { + ssh_run ' + root="`rootdev -d -s`" + cgpt show -i 13 "$root" + [ "`cgpt show -i 13 -b "$root"`" = 0 ] # begin + [ "`cgpt show -i 13 -s "$root"`" = 0 ] # size + ' +} + +wait_host on 30 +fetch_crouton + +# Switch on the screen +ssh_run "dbus-send --system --dest=org.chromium.PowerManager \ + --type=method_call /org/chromium/PowerManager \ + org.chromium.PowerManager.HandleUserActivity" + +exists= +echo "Checking crouton partition does not exist..." +if check_no_crouton_partition; then + echo "Creating crouton partition..." + ssh_run ' + cd /tmp/mkpart + CROUTON_MKPART_YES=yes sh installer/main.sh -S -c 5000 50*1024*1024{exit 1}' +# Ensure it's compressed +test "`hexdump -v -n2 -e'"%02x"' "$BACKUPDIR/$release.tar.gz"`" = "8b1f" + +# Restore the split archive. Fails first due to existence +fails host edit-chroot -y -f "$BACKUPDIR/$release.tar.gz" -r "$release" +host edit-chroot -y -f "$BACKUPDIR/$release.tar.gz" -rr "$release" +host enter-chroot -n "$release" true # Delete it host delete-chroot -y "$release" fails host enter-chroot -n "$release" true # Restore the chroot using the crouton installer (without update) -crouton -f "$BACKUPDIR/$release.tar" -n "$release" +crouton -f "$BACKUPDIR/$release.tar.gz" -n "$release" host enter-chroot -n "$release" true host edit-chroot -l $release | tee /dev/stderr | passes grep "^release: $release" -rm "$BACKUPDIR/$release.tar" +rm "$BACKUPDIR/$release.tar.gz"* # Backup a chroot with automatic naming host edit-chroot -y -f "$BACKUPDIR" -b "$release" @@ -130,7 +138,7 @@ fails host edit-chroot -y -f "$BACKUPDIR" -b \ fails ls "$BACKUPDIR"/* # Backup both chroots -host edit-chroot -y -f "$BACKUPDIR" -b "$release" "$release.2" +host edit-chroot "$release" -y -f "$BACKUPDIR" -b "$release.2" # Move them to a new prefix: destination needs to end with a slash fails host edit-chroot -y -m "$PREFIX/chroots.2" "$release" "$release.2" @@ -142,7 +150,7 @@ host enter-chroot -c "$PREFIX/chroots.2" -n "$release" true host enter-chroot -c "$PREFIX/chroots.2" -n "$release.2" true # Delete both chroots -host delete-chroot -y -c "$PREFIX/chroots.2" "$release" "$release.2" +host delete-chroot "$release" "$release.2" -y -c "$PREFIX/chroots.2" fails host enter-chroot -c "$PREFIX/chroots.2" -n "$release" true fails host enter-chroot -c "$PREFIX/chroots.2" -n "$release.2" true diff --git a/test/tests/18-upgrade b/test/tests/18-upgrade index 28db38a6e..01f5c2f4c 100644 --- a/test/tests/18-upgrade +++ b/test/tests/18-upgrade @@ -33,7 +33,7 @@ fi # packages from the mirror, or install alternative packages. TARGETS="core,audio,touch,x11" -PACKAGES="libsbc1 touchegg" +PACKAGES="touchegg" # upgrade [-d] release # -d: upgrade to a development release diff --git a/test/tests/30-audio b/test/tests/30-audio index 704bc7e21..f99a863b5 100644 --- a/test/tests/30-audio +++ b/test/tests/30-audio @@ -4,14 +4,12 @@ # found in the LICENSE file. # All supported releases should be able to create an audio chroot and play sound - if [ -z "$release" ]; then echo "all" exit 0 fi -snapshot "$release" core -crouton -u -n "$release" -t audio +snapshot "$release" audio # We pass -fdat to aplay/arecord, which means 48kHz, 16-bit, stereo. # dd writes/reads 8 blocks of 48000 bytes: 2 seconds worth of sound. @@ -59,38 +57,4 @@ exitswithin 0 30 host enter-chroot -n "$release" sh -exc ' pulseaudio --kill ' -# On precise, test that install_mirror_package is able to up/downgrade -# libsbc1 as required. We first install version 1.2, then check that -# update downgrades it to 1.1 again. -if [ "$release" = "precise" ]; then - # Test if libsbc1 version starts with the argument - testsbcver() { - host enter-chroot -n "$release" sh -exc ' - ok='' - for ver in `dpkg-query -l libsbc1:* | - awk '"'"'/^[hi]i/ { print $3 }'"'"'`; do - echo $ver | grep -q "^'"$1"'" - ok='y' - done - test -n "$ok" - ' - } - - echo ' - cras_arch='' - if [ "`uname -m`" = "x86_64" ]; then - cras_arch="i386" - fi - install_mirror_package 'libsbc1' \ - 'pool/main/s/sbc' '1\.2-.*' $cras_arch - install_mirror_package 'libsbc-dev' \ - 'pool/main/s/sbc' '1\.2-.*' $cras_arch - ' | crouton -T -U -n "$release" - - testsbcver '1.2-' - - crouton -u -n "$release" -t audio - testsbcver '1.1-' -fi - host delete-chroot -y "$release" diff --git a/test/tests/35-xorg b/test/tests/35-xorg new file mode 100644 index 000000000..3d52ace5a --- /dev/null +++ b/test/tests/35-xorg @@ -0,0 +1,50 @@ +#!/bin/sh -e +# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# All supported releases should be able to launch an xorg server +if [ -z "$release" ]; then + if grep -q 'SAMSUNG EXYNOS' /proc/cpuinfo; then + log "xorg is not available on Samsung ARM. Skipping test." + else + echo "all" + fi + exit 0 +fi + +snapshot "$release" audio +crouton -U -n "$release" -t xorg + +echo ' + install mesa-utils + mkdir -p /home/tmp + chmod 777 /home/tmp + cat > /home/tmp/xinitrc <&1 | tee /home/tmp/glxinfo.out +xdriinfo 2>&1 | tee /home/tmp/xdriinfo.out +exec /usr/bin/xterm -e true +END +' | crouton -T -U -n "$release" + +vtlockrun exitswithin 0 60 host \ + enter-chroot -n "$release" exec xinit "/home/tmp/xinitrc" +host enter-chroot -n "$release" sh -c ' + echo "====GLX info" + echo -n "release:" + croutonversion -r + echo -n "uname:" + uname -a + echo -n "vendor:" + cat /sys/class/graphics/fb0/device/vendor + echo -n "device:" + cat /sys/class/graphics/fb0/device/device + echo -n "xdriinfo:" + cat /home/tmp/xdriinfo.out + echo "==glxinfo" + cat /home/tmp/glxinfo.out + echo "====/GLX info" +' | log +host delete-chroot -y "$release" diff --git a/test/tests/36-xephyr b/test/tests/36-xephyr new file mode 100644 index 000000000..a611a6d43 --- /dev/null +++ b/test/tests/36-xephyr @@ -0,0 +1,20 @@ +#!/bin/sh -e +# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# All supported releases should be able to launch an xephyr server +if [ -z "$release" ]; then + if [ -f /sbin/frecon ]; then + log "xephyr doesn't work with Freon. Skipping test." + else + echo "all" + fi + exit 0 +fi + +snapshot "$release" audio +crouton -U -n "$release" -t xephyr +vtlockrun exitswithin 0 60 host \ + enter-chroot -n "$release" exec xinit /usr/bin/xterm -e true +host delete-chroot -y "$release" diff --git a/test/tests/37-xiwi b/test/tests/37-xiwi new file mode 100644 index 000000000..212b7efd3 --- /dev/null +++ b/test/tests/37-xiwi @@ -0,0 +1,22 @@ +#!/bin/sh -e +# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# All supported releases should be able to launch an xiwi server +if [ -z "$release" ]; then + echo "all" + exit 0 +fi + +# FIXME: This test is incomplete, as it is not able to connect to the +# extension, so xinit parameter is never run. However, this tests target +# installation, and verifies that there is no critical X failures (e.g. +# X server cannot start). + +snapshot "$release" audio +crouton -U -n "$release" -t xiwi +# FIXME: When x11test is merged, this should be run under vtlock +exitswithin 0 60 host \ + enter-chroot -n "$release" exec xinit /usr/bin/xterm -e true +host delete-chroot -y "$release" diff --git a/test/tests/w0-common b/test/tests/w0-common new file mode 100644 index 000000000..befe2d403 --- /dev/null +++ b/test/tests/w0-common @@ -0,0 +1,70 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# This test is sourced by other w* test, do not run it on its own +if [ -z "$release" -o -z "$target" ]; then + exit 0 +fi + +if [ -z "$startcmd" ]; then + error 2 "startcmd not defined" +fi + +# Start with a x11 snapshot +snapshot "$release" x11 + +# Install snapshot tools + xte +echo 'install --minimal x11-apps imagemagick xautomation' | \ + crouton -T -U -n "$release" + +ret=0 +crouton -u -n "$release" -t "$target" || ret=$? +if [ "$ret" -ne 0 ]; then + if [ "$ret" -eq 99 ]; then + log "Target $target failed on $release, as expected (unsupported combination)." + exit 0 + else + log "Target $target failed on $release." + exit "$ret" + fi +fi + +( + vtlock + host "$startcmd" -b -n "$release" + host enter-chroot -n "$release" sh -exc ' + timeout=60 + while [ "$timeout" -gt 0 ]; do + timeout="$((timeout - 5))" + sleep 5 + DISPLAY="`croutoncycle display`" + if [ "$DISPLAY" != "aura" -a "$DISPLAY" != ":0" ]; then + break + fi + done + # Test croutoncycle as a bonus + if [ "$DISPLAY" = ":0" -o "$DISPLAY" = "aura" ]; then + echo "Invalid display ($DISPLAY)." 1>&2 + exit 1 + fi + # Let WM/DE settle, then play xte sequence + sleep 30 + export DISPLAY + if [ -n "'"$xte"'" ]; then + echo "'"$xte"'" | tr ";" "\n" | xte + fi + # Let WM/DE settle again + sleep 30 + # Snapshot! We use xwd, as import does not snapshot obscured windows + # correctly (dialogs end up as black squares) + xwd -root | convert -quality 75 xwd:- ~/"screenshot-'"$target"'.jpg"' + + mv "$PREFIX/chroots/$release/home/test/screenshot-$target.jpg" \ + "$file-snapshot.jpg" + + host unmount-chroot -f -y "$release" +) + +host delete-chroot -y "$release" diff --git a/test/tests/w1-e17 b/test/tests/w1-e17 new file mode 100644 index 000000000..20bcc519c --- /dev/null +++ b/test/tests/w1-e17 @@ -0,0 +1,17 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$release" ]; then + echo "all" + exit 0 +fi + +target="e17" +startcmd="starte17" +# FIXME: Unfortunately, e17 cannot be configured with the keyboard only... +xte="" + +# Run common file +. "$SCRIPTDIR/test/tests/w0-common" diff --git a/test/tests/w2-gnome b/test/tests/w2-gnome new file mode 100644 index 000000000..6f67045f2 --- /dev/null +++ b/test/tests/w2-gnome @@ -0,0 +1,16 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$release" ]; then + echo "all" + exit 0 +fi + +target="gnome" +startcmd="startgnome" +xte="" + +# Run common file +. "$SCRIPTDIR/test/tests/w0-common" diff --git a/test/tests/w2d-gnome-desktop b/test/tests/w2d-gnome-desktop new file mode 100644 index 000000000..e7ed520d4 --- /dev/null +++ b/test/tests/w2d-gnome-desktop @@ -0,0 +1,16 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$release" ]; then + echo "all" + exit 0 +fi + +target="gnome-desktop" +startcmd="startgnome" +xte="" + +# Run common file +. "$SCRIPTDIR/test/tests/w0-common" diff --git a/test/tests/w3-kde b/test/tests/w3-kde new file mode 100644 index 000000000..a1ebe6617 --- /dev/null +++ b/test/tests/w3-kde @@ -0,0 +1,16 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$release" ]; then + echo "all" + exit 0 +fi + +target="kde" +startcmd="startkde" +xte="" + +# Run common file +. "$SCRIPTDIR/test/tests/w0-common" diff --git a/test/tests/w3d-kde-desktop b/test/tests/w3d-kde-desktop new file mode 100644 index 000000000..fea93c3d3 --- /dev/null +++ b/test/tests/w3d-kde-desktop @@ -0,0 +1,16 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$release" ]; then + echo "all" + exit 0 +fi + +target="kde-desktop" +startcmd="startkde" +xte="" + +# Run common file +. "$SCRIPTDIR/test/tests/w0-common" diff --git a/test/tests/w4-lxde b/test/tests/w4-lxde new file mode 100644 index 000000000..230ab5170 --- /dev/null +++ b/test/tests/w4-lxde @@ -0,0 +1,16 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$release" ]; then + echo "all" + exit 0 +fi + +target="lxde" +startcmd="startlxde" +xte="" + +# Run common file +. "$SCRIPTDIR/test/tests/w0-common" diff --git a/test/tests/w4d-lxde-desktop b/test/tests/w4d-lxde-desktop new file mode 100644 index 000000000..29099ec56 --- /dev/null +++ b/test/tests/w4d-lxde-desktop @@ -0,0 +1,16 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$release" ]; then + echo "all" + exit 0 +fi + +target="lxde-desktop" +startcmd="startlxde" +xte="" + +# Run common file +. "$SCRIPTDIR/test/tests/w0-common" diff --git a/test/tests/w5-unity b/test/tests/w5-unity new file mode 100644 index 000000000..43dbd8ccb --- /dev/null +++ b/test/tests/w5-unity @@ -0,0 +1,16 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$release" ]; then + echo "all" + exit 0 +fi + +target="unity" +startcmd="startunity" +xte="" + +# Run common file +. "$SCRIPTDIR/test/tests/w0-common" diff --git a/test/tests/w5d-unity-desktop b/test/tests/w5d-unity-desktop new file mode 100644 index 000000000..840c4748f --- /dev/null +++ b/test/tests/w5d-unity-desktop @@ -0,0 +1,16 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$release" ]; then + echo "all" + exit 0 +fi + +target="unity-desktop" +startcmd="startunity" +xte="" + +# Run common file +. "$SCRIPTDIR/test/tests/w0-common" diff --git a/test/tests/w6-xbmc b/test/tests/w6-xbmc new file mode 100644 index 000000000..93e45a162 --- /dev/null +++ b/test/tests/w6-xbmc @@ -0,0 +1,16 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$release" ]; then + echo "all" + exit 0 +fi + +target="xbmc" +startcmd="startxbmc" +xte="" + +# Run common file +. "$SCRIPTDIR/test/tests/w0-common" diff --git a/test/tests/w7-xfce b/test/tests/w7-xfce new file mode 100644 index 000000000..452b2aaf2 --- /dev/null +++ b/test/tests/w7-xfce @@ -0,0 +1,18 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$release" ]; then + echo "all" + exit 0 +fi + +target="xfce" +startcmd="startxfce4" +# This magic sequence presses "Use default configuration" +xte="keydown Alt_L;key Tab;key Tab;keyup Alt_L;key Left;key Left;\ +keydown Return;sleep 1;keyup Return" + +# Run common file +. "$SCRIPTDIR/test/tests/w0-common" diff --git a/test/tests/w7d-xfce-desktop b/test/tests/w7d-xfce-desktop new file mode 100644 index 000000000..8c2240a27 --- /dev/null +++ b/test/tests/w7d-xfce-desktop @@ -0,0 +1,16 @@ +#!/bin/sh -e +# Copyright (c) 2014 The crouton Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$release" ]; then + echo "all" + exit 0 +fi + +target="xfce-desktop" +startcmd="startxfce4" +xte="" + +# Run common file +. "$SCRIPTDIR/test/tests/w0-common" diff --git a/test/tests/x0-alltargets b/test/tests/x0-alltargets index bfe475ebe..1f1525210 100644 --- a/test/tests/x0-alltargets +++ b/test/tests/x0-alltargets @@ -18,8 +18,15 @@ for target in "$SCRIPTDIR/targets/"*; do continue fi - # Some other targets do not require testing in this context - for blacklist in audio core x11 xephyr xorg; do + # Ignore *desktop targets too, since they'd all be blacklisted + if [ "${target%desktop}" != "$target" ]; then + continue + fi + + # Some other targets do not require testing in this context, + # or have their own w* tests + for blacklist in audio core x11 xephyr xiwi xorg \ + e17 gnome kde lxde unity xbmc xfce; do if [ "$target" = "$blacklist" ]; then break fi