Tuesday, March 27, 2012

[Service上線前你該知道的事]How to Debian-ize Your Web Application.

在布建一個Web Application時,用.war來佈屬服務,算是最沒有效率的一種方式,因為,這樣無法把web container所需要的設定檔一起佈建出去,同時,也無法板本控製你的應用程式;本文接下來會敘述,如何用maven & jetty & debian來佈屬你的Web Application。

首先,先講講我的需求,我的需求如下
  • Web Application的版本必需能夠簡單被管理,要能簡單的就知道目前安裝的版本,並且輕鬆的昇級到新版
  • Web Application被佈建時,要連Web Container及設定擋包在一起
  • Debian Box開關機時,要能自動開關Web Container

這些需求,我是靠 unix-maven-plugin ,幫我把我的 web application 包成一個 .deb 來做到的,包成 .deb 的好處是,在 debian 上可以簡單的安裝,若是公司有架 debian package repository, 那麼,打個 apt-get install 就可以安裝或昇級到最新版,同時間,可以透過 debian 安裝的機制,來把些 script 掛入 rc.*

由於 unix-maven-plugin 已被原作者遺棄,所以需要從 github下載並安裝後人修改過的版本。

然後依附錄的方式,增修你的設定檔,需要增修的檔案有
  • pom.xml: 加入 unix-maven-plugin 及服務的名稱及PORT
  • src/main/unix/resources/default/${service.user} : 給 jetty 用的 default 設定值,在這邊你可以修改 JAVA_OPTIONS 及 NO_START
  • src/main/unix/resources/etc/jetty.conf
  • src/main/unix/resources/etc/start.conf
  • src/main/unix/resources/scripts/${service.user} : 給 debian 用的 init script
  • src/main/unix/resources/scripts/post-install post-remove pre-install pre-remove : debian 在安裝及昇級 .deb 時會跑的 script

將這些檔案設定好後,在 debian 的機器上打 mvn package deploy ,就可以自動生成 .deb 的檔案,並且上傳到你的 maven repository 上去,如果貴公司有架 debian package repository 的話,還可以加一段程式碼把 .deb 傳到 debian package repository 去。

接著,打 dpkg -i xxxx.deb 就可以把你的 web application 當成一個 debian service 安裝到你的 debain machine 上去。

查詢目前安裝的板本則是打 dpkg -l xxxx 即可

||/ Name           Version        Description
+++-==============-==============-============================================
ii  backend       1.2.0-20120327.143115          backend


附錄



把底下的這段,加入你的 pom.xml


  4.0.0
  com.locadz
  backend
  war
  backend
  1.2.0-SNAPSHOT
  backend
  
    1.0-alpha-6.1
    7.4.5.v20110725

    Locadz Backend
    /var/lib/bluetang/locadz-backend
    locadz-backend
    9080
  
  
    
      debian-package
      
        
          unix
          linux
        
      
      
        
          
            maven-resources-plugin
            2.5
            
              
                copy-resources
                
                process-resources
                
                  copy-resources
                
                
                  ${basedir}/target
                  
                    
                      src/main/unix
                      unix
                      
                        **/*
                      
                      true
                    
                  
                
              
            
          

          
          
            org.mortbay.jetty.toolchain
            unix-maven-plugin
            ${version.maven.unix.plugin}
            true
            
              
                package
                
                  package-deb-attached
                
              
            
            
              
              true
              yho@bluetangstudio.com
              yho@bluetangstudio.com
              
                
                  ${service.user}
                  adm
                
                
                  ${service.user}
                  adm
                
              
              
                
bluetang
extra
org.eclipse.jetty:jetty-distribution:zip ${service.home} /jetty-distribution-${version.jetty}(.*) $1 */contexts/** */contexts-available/** */webapps/** */javadoc/** */LICENSES/** */logs/** **/win32/** ${project.build.directory}/${project.build.finalName}.${project.packaging} ${service.home}/webapps/ROOT.war ${basedir}/target/unix/resources/default/${service.user} /etc/default ${basedir}/target/unix/resources/etc /etc/bluetang/${service.user} ${basedir}/target/unix/scripts/${service.user} /etc/init.d 0754 ${service.user} admin
org.eclipse.jetty jetty-distribution ${version.jetty} zip * *

然後把下面這塊存成 src/main/unix/resources/default/${service.user}

# Defaults for jetty see /etc/${service.user}/jetty for more

# change to 0 to allow Jetty to start
NO_START=0

# change to 'no' or uncomment to use the default setting in /etc/default/rcS
VERBOSE=yes

# Run Jetty as this user ID (default: jetty)
# Set this to an empty string to prevent Jetty from starting automatically
JETTY_USER=${service.user}

# Listen to connections from this network host
# Use 0.0.0.0 as host to accept all connections.
# Uncomment to restrict access to localhost
#JETTY_HOST=$(uname -n)
JETTY_HOST=0.0.0.0

# The network port used by Jetty
JETTY_PORT=${service.port}

# Timeout in seconds for the shutdown of all webapps
#JETTY_SHUTDOWN=30

# Additional arguments to pass to Jetty
#JETTY_ARGS=

# Extra options to pass to the JVM
JAVA_OPTIONS="-Xmx256m -Djava.awt.headless=true -Duser.timezone=UTC"

# Home of Java installation.
#JAVA_HOME=

# The first existing directory is used for JAVA_HOME (if JAVA_HOME is not
# defined in /etc/default/jetty). Should contain a list of space separated directories.
#JDK_DIRS="/usr/lib/jvm/default-java /usr/lib/jvm/java-6-sun"

# Java compiler to use for translating JavaServer Pages (JSPs). You can use all
# compilers that are accepted by Ant's build.compiler property.
#JSP_COMPILER=jikes

# Jetty uses a directory to store temporary files like unpacked webapps
JETTY_TMP=/var/cache/bluetang/jetty

# Jetty uses a config file to setup its boot classpath
JETTY_START_CONFIG=/etc/bluetang/${service.user}/start.config

# Default for number of days to keep old log files in /var/log/jetty/
#LOGFILE_DAYS=14


然後把下面這塊存成 src/main/unix/resources/ect/jetty.conf

# list of jetty configuration and property files
/var/lib/bluetang/${service.user}/etc/jetty-logging.xml


然後把下面這塊存成 src/main/unix/resources/etc/start.conf

# This file controls what file are to be put on classpath or command line.
#
# Format is as follows:
# Each line contains entry for one JAR file.
# Format of line:
#
#  SUBJECT [ [!] CONDITION [AND|OR] ]*
#
# where SUBJECT:
#   ends with ".class" is the Main class to run.
#   ends with ".xml" is a configuration file for the command line
#   ends with "/" is a directory from which to add all jar and zip files.
#   ends with "/*" is a directory from which to add all unconsidered jar and zip files.
#   ends with "/**" is a directory from which to recursively add all unconsidered jar and zip files.
#   Containing = are used to assign system properties.
#   all other subjects are treated as files to be added to the classpath.
#
# Subjects may include system properties with $(propertyname) syntax.
#
# Files starting with "/" are considered absolute, all others are relative to
# the home directory.
#
# CONDITION is one of:
#   always
#   never
#   available classname        # true if class on classpath
#   property name              # true of set
#   java OPERATOR version      # java version compared to literal
#   nargs OPERATOR number      # number of command line args compared to literal
#   OPERATOR := one of "<",">","<=",">=","==","!="
#
# CONDITIONS can be combined with AND OR or !, with AND being the assume
# operator for a list of CONDITIONS.
# Classpath operations are evaluated on the fly, so once a class or jar is
# added to the classpath, subsequent available conditions will see that class.
#

$(jetty.class.path)                              always
$(jetty.lib)/**                                  exists $(jetty.lib)

jetty.home=/var/lib/bluetang/${service.user}                      always

# The main class to run
org.mortbay.xml.XmlConfiguration.class

# The default configuration files
$(jetty.home)/etc/jetty.xml                      nargs == 0

/usr/share/java/servlet-api-2.5.jar
/usr/share/java/slf4j-api.jar

# Optional stuff for libjetty-extra-java
/usr/share/java/gnumail.jar
/usr/share/java/activation.jar
/usr/share/java/ant.jar

# Set the jetty classpath
/usr/share/jetty/lib/**

# Add a resources directory if it is there
$(jetty.home)/resources/




然後把下面這塊存成 src/main/unix/scripts/${service.user}

#!/bin/bash  
### BEGIN INIT INFO
# Provides:          ${service.user}
# Required-Start:
# Required-Stop:
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Start daemon at boot time
# Description:       Enable service provided by daemon.
### END INIT INFO
#
# Startup script for jetty under *nix systems (it works under NT/cygwin too).
#
# Configuration files
#
# /etc/default/${service.user}
#   If it exists, this is read at the start of script. It may perform any 
#   sequence of shell commands, like setting relevant environment variables.
#
# /etc/bluetang/${service.user}/jetty.conf
#   If found, and no configurations were given on the command line,
#   the file will be used as this script's configuration. 
#   Each line in the file may contain:
#     - A comment denoted by the pound (#) sign as first non-blank character.
#     - The path to a regular file, which will be passed to jetty as a 
#       config.xml file.
#     - The path to a directory. Each *.xml file in the directory will be
#       passed to jetty as a config.xml file.
#
#   The files will be checked for existence before being passed to jetty.
#
# $JETTY_HOME/etc/jetty.xml
#   If found, used as this script's configuration file, but only if
#   /etc/bluetang/${service.user}/jetty.conf was not present. See above.
#   
# Configuration variables
#
# JAVA
#   Command to invoke Java. If not set, java (from the PATH) will be used.
#
# JAVA_OPTIONS
#   Extra options to pass to the JVM
#
# JETTY_HOME
#   Where Jetty is installed. If not set, the script will try go
#   guess it by first looking at the invocation path for the script,
#   and then by looking in standard locations as $HOME/opt/jetty
#   and /opt/jetty. The java system property "jetty.home" will be
#   set to this value for use by configure.xml files, f.e.:
#
#    /webapps/jetty.war
#
# JETTY_PORT
#   Override the default port for Jetty servers. If not set then the
#   default value in the xml configuration file will be used. The java
#   system property "jetty.port" will be set to this value for use in
#   configure.xml files. For example, the following idiom is widely
#   used in the demo config files to respect this property in Listener
#   configuration elements:
#
#    
#
#   Note: that the config file could ignore this property simply by saying:
#
#    8080
#
# JETTY_RUN
#   Where the jetty.pid file should be stored. It defaults to the
#   first available of /var/run, /usr/var/run, and /tmp if not set.
#  
# JETTY_PID
#   The Jetty PID file, defaults to $JETTY_RUN/jetty.pid
#   
# JETTY_ARGS
#   The default arguments to pass to jetty.
#
# JETTY_USER
#   if set, then used as a username to run the server as
#
JETTY_HOME=${service.home}
JETTY_USER=${service.user}
JETTY_PID=$JETTY_RUN/${service.user}.pid
usage()
{
    echo "Usage: ${0##*/} [-d] {start|stop|run|restart|check|supervise} [ CONFIGS ... ] "
    exit 1
}

[ $# -gt 0 ] || usage


##################################################
# Some utility functions
##################################################
findDirectory()
{
  local L OP=$1
  shift
  for L in "$@"; do
    [ "$OP" "$L" ] || continue
    printf %s "$L"
    break
  done
}

running()
{
  local PID=$(cat "$1" 2>/dev/null) || return 1
  kill -0 "$PID" 2>/dev/null
}

readConfig()
{
  (( DEBUG )) && echo "Reading $1.."
  source "$1"
}



##################################################
# Get the action & configs
##################################################
CONFIGS=()
NO_START=0
DEBUG=0

while [[ $1 = -* ]]; do
  case $1 in
    -d) DEBUG=1 ;;
  esac
  shift
done
ACTION=$1
shift

##################################################
# See if there's a default configuration file
##################################################
if [ -f /etc/default/${service.user} ] ; then
  . /etc/default/${service.user}
fi


##################################################
# Set tmp if not already set.
##################################################
TMPDIR=${TMPDIR:-/tmp}

##################################################
# Jetty's hallmark
##################################################
JETTY_INSTALL_TRACE_FILE="etc/jetty.xml"


##################################################
# Try to determine JETTY_HOME if not set
##################################################
if [ -z "$JETTY_HOME" ] 
then
  JETTY_SH=$0
  case "$JETTY_SH" in
    /*)   ;;
    ./*)  ;;
    *)    JETTY_SH=./$JETTY_SH ;;
  esac
  JETTY_HOME=${JETTY_SH%/*/*}

  if [ ! -f "${JETTY_SH%/*/*}/$JETTY_INSTALL_TRACE_FILE" ]
  then
    JETTY_HOME=
  fi
fi



##################################################
# if no JETTY_HOME, search likely locations.
##################################################
if [ -z "$JETTY_HOME" ] ; then
  STANDARD_LOCATIONS=(
        "/usr/share"
        "/usr/share/java"
        "${HOME}"
        "${HOME}/src"
        "${HOME}/opt"
        "/opt"
        "/java"
        "/usr/local"
        "/usr/local/share"
        "/usr/local/share/java"
        "/home"
        )
  JETTY_DIR_NAMES=(
        "jetty-7"
        "jetty7"
        "jetty-7.*"
        "jetty"
        "Jetty-7"
        "Jetty7"
        "Jetty-7.*"
        "Jetty"
        )

  for L in "${STANDARD_LOCATIONS[@]}"
  do
    for N in "${JETTY_DIR_NAMES[@]}"
    do
      POSSIBLE_JETTY_HOME=("$L/"$N)
      if [ ! -d "$POSSIBLE_JETTY_HOME" ]
      then
        # Not a directory. skip.
        unset POSSIBLE_JETTY_HOME
      elif [ ! -f "$POSSIBLE_JETTY_HOME/$JETTY_INSTALL_TRACE_FILE" ]
      then
        # Trace file not found. skip.
        unset POSSIBLE_JETTY_HOME
      else
        # Good hit, Use it
        JETTY_HOME=$POSSIBLE_JETTY_HOME
        # Break out of JETTY_DIR_NAMES loop
        break
      fi
    done
    if [ -n "$POSSIBLE_JETTY_HOME" ]
    then
      # We have found our JETTY_HOME
      # Break out of STANDARD_LOCATIONS loop
      break
    fi
  done
fi


##################################################
# No JETTY_HOME yet? We're out of luck!
##################################################
if [ -z "$JETTY_HOME" ]; then
  echo "** ERROR: JETTY_HOME not set, you need to set it or install in a standard location"
  exit 1
fi

cd "$JETTY_HOME"
JETTY_HOME=$PWD


#####################################################
# Check that jetty is where we think it is
#####################################################
if [ ! -r "$JETTY_HOME/$JETTY_INSTALL_TRACE_FILE" ]
then
  echo "** ERROR: Oops! Jetty doesn't appear to be installed in $JETTY_HOME"
  echo "** ERROR:  $JETTY_HOME/$JETTY_INSTALL_TRACE_FILE is not readable!"
  exit 1
fi

##################################################
# Try to find this script's configuration file,
# but only if no configurations were given on the
# command line.
##################################################
if [ -z "$JETTY_CONF" ] 
then
  if [ -f /etc/bluetang/${service.user}/jetty.conf ]
  then
     JETTY_CONF=/etc/bluetang/${service.user}/jetty.conf
  elif [ -f "${JETTY_HOME}/etc/jetty.conf" ]
  then
     JETTY_CONF="${JETTY_HOME}/etc/jetty.conf"
  fi
fi

##################################################
# Get the list of config.xml files from jetty.conf
##################################################
if [ -z "$CONFIGS" ] && [ -f "$JETTY_CONF" ] && [ -r "$JETTY_CONF" ]
then
  while read -r CONF
  do
    if expr "$CONF" : '#' >/dev/null ; then
      continue
    fi

    if [ -d "$CONF" ]
    then
      # assume it's a directory with configure.xml files
      # for example: /etc/jetty.d/
      # sort the files before adding them to the list of CONFIGS
      for file in "$CONF/"*.xml
      do
        if [ -r "$FILE" ] && [ -f "$FILE" ]
        then
          CONFIGS+=("$FILE")
        else
          echo "** WARNING: Cannot read '$FILE' specified in '$JETTY_CONF'"
        fi
      done
    else
      # assume it's a command line parameter (let start.jar deal with its validity)
      CONFIGS+=("$CONF")
    fi
  done < "$JETTY_CONF"
fi

#####################################################
# Find a location for the pid file
#####################################################
if [ -z "$JETTY_RUN" ]
then
  JETTY_RUN=$(findDirectory -w /var/run /usr/var/run /tmp)
fi

#####################################################
# Find a PID for the pid file
#####################################################
if [ -z "$JETTY_PID" ]
then
  JETTY_PID="$JETTY_RUN/jetty.pid"
fi

##################################################
# Setup JAVA if unset
##################################################
if [ -z "$JAVA" ]
then
  JAVA=$(which java)
fi

if [ -z "$JAVA" ]
then
  echo "Cannot find a Java JDK. Please set either set JAVA or put java (>=1.5) in your PATH." 2>&2
  exit 1
fi

#####################################################
# See if JETTY_PORT is defined
#####################################################
if [ "$JETTY_PORT" ]
then
  JAVA_OPTIONS+=("-Djetty.port=$JETTY_PORT")
fi

#####################################################
# See if JETTY_LOGS is defined
#####################################################
if [ "$JETTY_LOGS" ]
then
  JAVA_OPTIONS+=("-Djetty.logs=$JETTY_LOGS")
fi

#####################################################
# Are we running on Windows? Could be, with Cygwin/NT.
#####################################################
case "`uname`" in
CYGWIN*) PATH_SEPARATOR=";";;
*) PATH_SEPARATOR=":";;
esac


#####################################################
# Add jetty properties to Java VM options.
#####################################################
JAVA_OPTIONS+=("-Djetty.home=$JETTY_HOME" "-Djava.io.tmpdir=$TMPDIR")

[ -f "$JETTY_HOME/etc/start.config" ] && JAVA_OPTIONS=("-DSTART=$JETTY_HOME/etc/start.config" "${JAVA_OPTIONS[@]}")

#####################################################
# This is how the Jetty server will be started
#####################################################

JETTY_START=$JETTY_HOME/start.jar
[ ! -f "$JETTY_START" ] && JETTY_START=$JETTY_HOME/lib/start.jar

START_INI=$(dirname $JETTY_START)/start.ini
[ -r "$START_INI" ] || START_INI=""

RUN_ARGS=(${JAVA_OPTIONS[@]} -jar "$JETTY_START" $JETTY_ARGS "${CONFIGS[@]}")
RUN_CMD=("$JAVA" ${RUN_ARGS[@]})

#####################################################
# Comment these out after you're happy with what 
# the script is doing.
#####################################################
if (( DEBUG ))
then
  echo "JETTY_HOME     =  $JETTY_HOME"
  echo "JETTY_CONF     =  $JETTY_CONF"
  echo "JETTY_RUN      =  $JETTY_RUN"
  echo "JETTY_PID      =  $JETTY_PID"
  echo "JETTY_ARGS     =  $JETTY_ARGS"
  echo "CONFIGS        =  ${CONFIGS[*]}"
  echo "JAVA_OPTIONS   =  ${JAVA_OPTIONS[*]}"
  echo "JAVA           =  $JAVA"
  echo "RUN_CMD        =  ${RUN_CMD}"
fi

##################################################
# Do the action
##################################################
case "$ACTION" in
  start)
    echo -n "Starting ${service.name}: "

    if (( NO_START )); then
      echo "Not starting ${service.user} - NO_START=1";
      exit
    fi


    if type start-stop-daemon > /dev/null 2>&1
    then
      unset CH_USER
      if [ -n "$JETTY_USER" ]
      then
        CH_USER="-c$JETTY_USER"
      fi
      if start-stop-daemon -S -p"$JETTY_PID" $CH_USER -d"$JETTY_HOME" -b -m -a "$JAVA" -- "${RUN_ARGS[@]}" --daemon
      then
        sleep 1
        if running "$JETTY_PID"
        then
          echo "OK"
        else
          echo "FAILED"
        fi
      fi

    else

      if [ -f "$JETTY_PID" ]
      then
        if running $JETTY_PID
        then
          echo "Already Running!"
          exit 1
        else
          # dead pid file - remove
          rm -f "$JETTY_PID"
        fi
      fi

      if [ "$JETTY_USER" ]
      then
        touch "$JETTY_PID"
        chown "$JETTY_USER" "$JETTY_PID"
        # FIXME: Broken solution: wordsplitting, pathname expansion, arbitrary command execution, etc.
        su - "$JETTY_USER" -c "
          ${RUN_CMD[*]} --daemon &
          disown \$!
          echo \$! > '$JETTY_PID'"
      else
        "${RUN_CMD[@]}" &
        disown $!
        echo $! > "$JETTY_PID"
      fi

      echo "STARTED ${service.name} `date`"
    fi

    ;;

  stop)
    echo -n "Stopping ${service.name}: "
    if type start-stop-daemon > /dev/null 2>&1; then
      start-stop-daemon -K -p"$JETTY_PID" -d"$JETTY_HOME" -a "$JAVA" -s HUP

      TIMEOUT=30
      while running "$JETTY_PID"; do
        if (( TIMEOUT-- == 0 )); then
          start-stop-daemon -K -p"$JETTY_PID" -d"$JETTY_HOME" -a "$JAVA" -s KILL
        fi

        sleep 1
      done

      rm -f "$JETTY_PID"
      echo OK
    else
      PID=$(cat "$JETTY_PID" 2>/dev/null)
      kill "$PID" 2>/dev/null

      TIMEOUT=30
      while running $JETTY_PID; do
        if (( TIMEOUT-- == 0 )); then
          kill -KILL "$PID" 2>/dev/null
        fi

        sleep 1
      done

      rm -f "$JETTY_PID"
      echo OK
    fi

    ;;

  restart)
    JETTY_SH=$0
    if [ ! -f $JETTY_SH ]; then
      if [ ! -f $JETTY_HOME/bin/jetty.sh ]; then
        echo "$JETTY_HOME/bin/jetty.sh does not exist."
        exit 1
      fi
      JETTY_SH=$JETTY_HOME/bin/jetty.sh
    fi

    "$JETTY_SH" stop "$@"
    "$JETTY_SH" start "$@"

    ;;

  supervise)
    #
    # Under control of daemontools supervise monitor which
    # handles restarts and shutdowns via the svc program.
    #
    exec "${RUN_CMD[@]}"

    ;;

  run|demo)
    echo "Running Jetty: "

    if [ -f "$JETTY_PID" ]
    then
      if running "$JETTY_PID"
      then
        echo "Already Running!"
        exit 1
      else
        # dead pid file - remove
        rm -f "$JETTY_PID"
      fi
    fi

    exec "${RUN_CMD[@]}"

    ;;

  check)
    echo "Checking arguments to Jetty: "
    echo "JETTY_HOME     =  $JETTY_HOME"
    echo "JETTY_CONF     =  $JETTY_CONF"
    echo "JETTY_RUN      =  $JETTY_RUN"
    echo "JETTY_PID      =  $JETTY_PID"
    echo "JETTY_PORT     =  $JETTY_PORT"
    echo "JETTY_LOGS     =  $JETTY_LOGS"
    echo "START_INI      =  $START_INI"
    echo "CONFIGS        =  ${CONFIGS[*]}"
    echo "JAVA_OPTIONS   =  ${JAVA_OPTIONS[*]}"
    echo "JAVA           =  $JAVA"
    echo "CLASSPATH      =  $CLASSPATH"
    echo "RUN_CMD        =  ${RUN_CMD[*]}"
    echo

    if [ -f "$JETTY_RUN/jetty.pid" ]
    then
      echo "Jetty running pid=$(< "$JETTY_RUN/jetty.pid")"
      exit 0
    fi
    exit 1

    ;;

  *)
    usage

    ;;
esac

exit 0

然後把下面這塊存成 src/main/unix/scripts/post-install,並修改 USER=locadz-backend 這一行成跟你 ${service.user} 一樣的值
#!/bin/sh
set -e

USER=locadz-backend

case "$1" in
    configure)
        if ! id $USER > /dev/null 2>&1 ; then
            adduser --system --home /var/lib/bluetang/$USER --no-create-home \
                --group --disabled-password --shell /bin/false \
                $USER
        fi

        if [ ! -e /var/log/bluetang ]
        then
            mkdir -p /var/log/bluetang
        fi
        if [ ! -e /var/log/bluetang/$USER/ ]
        then
            ln -s /var/lib/bluetang/$USER/logs /var/log/bluetang/$USER
        fi
        if [ ! -e /var/cache/bluetang/$USER ]
        then
            mkdir -p /var/cache/bluetang/$USER
        fi


        chown -R $USER:adm /var/cache/bluetang/$USER /var/log/bluetang/$USER /var/lib/bluetang/$USER
        chmod 0744 /etc/init.d/$USER
        update-rc.d $USER defaults

    ;;

    abort-upgrade|abort-remove|abort-deconfigure)
    ;;

    *)
        echo "$0 called with unknown argument \`$1'" >&2
        exit 1
    ;;
esac

然後把下面這塊存成 src/main/unix/scripts/post-remove,並修改 USER=locadz-backend 這一行成跟你 ${service.user} 一樣的值
#!/bin/sh
set -e

USER=locadz-backend

#DEBHELPER#

# Remove cached files
rm -rf /var/cache/blutang/$USER/*

case "$1" in
    remove)
        # Remove ROOT webapp if not modified
        RWLOC="/var/lib/bluetang/$USER/webapps/root"
        RWFILES="$RWLOC/index.html $RWLOC/jetty_banner.gif"
        if [ "`(cat $RWFILES | md5sum -) 2>/dev/null | cut -d ' ' -f 1`" \
                            = "12471c4b3020defb7ebd30ef84c0f9dd" ] ; then
            rm $RWFILES
            rmdir --ignore-fail-on-non-empty \
                /var/lib/bluetang/$USER/webapps/root \
                /var/lib/bluetang/$USER/webapps \
                /var/lib/bluetang/$USER || true
        fi
        if [ -d "/var/cache/bluetang/$USER" ] ; then
            rm -rf /var/cache/bluetang/$USER
        fi
    ;;

    purge)
        # Remove user/group and log files (don't remove everything under
        # /var/lib/jetty because there might be user-installed webapps)
        deluser jetty || true
        rm -rf /var/log/bluetang/$USER
        if [ -d "/var/lib/bluetang/$USER" ] ; then
            rmdir --ignore-fail-on-non-empty /var/lib/bluetang/$USER || true
        fi
        rmdir --ignore-fail-on-non-empty /etc/bluetang/$USER/contexts /etc/bluetang/$USER || true
    ;;

    remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
        # Nothing to do here
    ;;

    *)
        echo "$0 called with unknown argument \`$1'" >&2
        exit 1
    ;;
esac
然後把下面這塊存成 src/main/unix/scripts/pre-install,並修改 USER=locadz-backend 這一行成跟你 ${service.user} 一樣的值
#!/bin/sh
#
# This is the preinst script for the Blue Tang Servers
#
# Written by Yung-Lin Ho yho@bluetangstudio.com

set -e
USER=locadz-backend
SCRIPT=/etc/init.d/$USER


case "$1" in
    upgrade|remove|purge)
        $SCRIPT stop
        ;;
    install|failed-upgrade|abort-install|abort-upgrade|disappear)
        ;;
    *)
        echo "preinst called with unknown argument \`$1'" >&2
        ;;
esac

exit 0
下面這塊存成 src/main/unix/scripts/pre-remove,並修改 USER=locadz-backend 這一行成跟你 ${service.user} 一樣的值
#!/bin/sh
#
# This is the prerm script for the Blue Tang Servers
#
# Written by Yung-Lin Ho yho@bluetangstudio.com

set -e
USER=locadz-backend
SCRIPT=/etc/init.d/$USER

case "$1" in
    upgrade)
        ;;
    remove|purge)
        $SCRIPT stop
        ;;
    failed-upgrade|abort-install|abort-upgrade|disappear)
        ;;
    *)
        echo "prerm called with unknown argument \`$1'" >&2
        ;;
esac

exit 0

Bravo to Spotify Apps.

spotify's app is pretty good business model. 之前我抱怨 #spotify 的 editorial material 比不上 #rhapsody. 用 Rhapsody 時,因為 Rhapsody 內有十多位人員負責編排本週推薦的音樂,還有客製化的播放清單及電台,所以訂閱 rhapsody 就像有 DJ 幫你找歌來聽。

但是用 Spotify ,就只能夠聽,你已知的歌手, spotify 沒有一個好用的推薦功能可用。

最近 spotify 的 gui client 開始可以放上第三方App的功能,馬上就把缺的地方補上,而且還更好。

像是串入 last.fm 就有推薦清單可用,soundrop可以當線上DJ,TuneWiki可以看歌詞,歌詞還會照時間來跳到現在唱的句子;這證明了 Open API的威力,你有缺的地方,用 3rd party app 來補上,而且可以做的更好。

其實 Rhapsody 內部早在 2005 已有這種 API ,我在 real.com 時就是做這一塊的,許多第三方的程式及硬體就是呼叫我們的API,如 sonos, sandisk sansa, 我自己也寫了隻程式給我的Nokia N82用,只是可惜不能放出來。

Rhapsody 再不把 API 放出來,恐怕就連美國這塊市場都要開始敗退了...

Wednesday, March 7, 2012

[Job]Job Market in US

這篇的草稿躺在我的部絡格兩個月了,看來完成遙遙無期,所以先把屍體放出來好了

要在美國求職,第一要務,就是先了解到,美國的求職市場是怎樣的環境。

大多人在台灣的求職的方式,是上 104 找適合的工作,然後再寄信過去應徵工作;然而,很不幸的是,你把求職市場放大十倍,這種方式就變得不是很有效率了。

首先,你要知道的是,在矽谷,一個工作被登上美國的求職網站,兩週內,可能吸引到2000個應徵者投遞履例表,這數字是什麼意思呢,這是說,當 HR 過濾履例表時,既使只花30秒在一份履例表上,也要花上整整16小時兩個工作天,才能篩檢完畢。我曾經看過同事在過濾履例時,就像在拿電風扇在吹一樣,看到過去工作過的公司有趣留下來,看到經驗有趣的留下來,看到名字念起來有趣的留下來;有時後,會不會被找來面識,真的是要靠一點運氣的。

所以說,要寫出一份好的履例表,讓 HR 一目了然,抓到重點,把你的履例表留下來細看,就是一件很重要的事了。


好的履例表,第一要鍵就是,格式要清楚,格式不清楚,當下就是被放一邊,不會有 HR 去細看的;第二則是,不管你經歷有多少,履例表的內容,一定要是一業滿版,比一頁多,你要刪成只有一頁,比一頁少,你不管怎樣就是要加到一頁滿版。

底下是我2005時,剛畢業時用的履例表;內容的格式有點小問題,不過這個板型也是我後來一直延用下去的格式。


這份履例表的排列是社會新鮮人用的,(未完待續)

Friday, February 17, 2012

Music for My Wedding.
















Tuesday, February 7, 2012

受夠了台灣的 Costco

真的有點受夠了台灣 Costco 老是在賣些美國沒賣的美國產品了。




https://www.google.com/search?q=vitamin%20goldmax 找下去,找不到一個條目的東西,台灣 Costco 也敢拿出來賣?這些採購人員是心理只有錢是吧?

以前住美國時,我大多的日用產品都是在 costco 買的,那是因為,美國 costco 設定的營業方針是『貨品以近乎進貨成本來賣、獲利全靠會員費』,賺不賺錢,就看選貨能力來吸引會員數。

Costco@USA選貨的標準又剛好跟我的收入很相近,所以,若是要買日用品又懶得做功課時,就去 costco 買;那是我相信 costco 的選貨標準,不用讓我在買日用品時,還要一個個拿起來看產地與成份,逛的很舒服

但是台灣的 Costco 逛起來給我的感覺卻不是如此, 50%的貨品是台灣採購的,讓我逛起來像是在逛市場一樣,要玩訛虞我詐的遊戲,許多商品的外包照片跟內容跟本不符,又有許多假美國商品,逛起來要心驚膽顫,一點都沒有消費的樂趣。

Tuesday, January 3, 2012

How to Protect A WebService.

最近做了不少Restful WebService的案子,也用了不少外部的WebService API,看到許多的系統,在安全性上,有許多的漏洞,因此想寫篇文章來說說,到底要怎樣保護好你的WebService,必竟,台灣主流網站的安全性已經夠糟了,不用再多幾個有問題的API來把環境弄更糟。

大家用Web API,最常碰到的保護機制是 ApiKey ,但是,請不要以為 ApiKey 有什麼保護做用,HTTP Request的東西都是明碼,你傳什麼,網路上流過的機器都可以看的一清二楚,假設你做的 client 端程式是用 ApiKey 來保護 client <-> server 間的通訊的話,要破的人,只要在 client 所在的區網內,安裝 packet sniffer 就可以拿到 client 內包的 ApiKey 了。

ApiKey通常是WebService Provider拿來統計流量用的,不要以為他有什麼安全機制存在。

再來,有些 Web API 要求你要登入,才可以使用他們的 API ,如 chargify or GMail,這些服務所使用的機制是HTTP Basic Authentication ;但是非常不幸的是,HTTP Basic Authentication只不過變形的ApiKey,Basic Auth的運作方式是把 username:password 用 base64 編碼後,放在 HTTP Request Header 當成密碼傳到 server 端,因此,一樣可以被 sniffer 直接偷出來用。

當然,你會問,我們不是有用 HTTPS 來把封包加密了嗎?然而,除非你把 certificate 預載在 client 端,要不然 client <-> server 第一次的通訊是危險的,會受到 Man in the middle attack ,直接從中間把雙方收送的 certificate 換掉。


較安全的做法是,使用HTTPS Client Authentication,在包 client 就把 client 端該用的 server-signed certificate 包進 client 端裡面;這樣一來,少了透過 internet 交換 certificate 的機制,能夠確保 certificate 不會被偷換掉。

另外,使用 Client Authentication 的好處是,只有 server 端認可的 client 可以使用 API ,縱始是第三方拿到了username, password, apikey,還是沒有辦法使用你的服務。

至於有那些大廠,用了不安全的方式來保護他們的 web service api 呢? unblock-us 這邊有幾個名單


unblock-us 是怎麼破解幾大影音網站的 geo-location check 的呢?

一般的解決方案是用 VPN,但是影音串流不適合透過 VPN 來做 proxy。

就我的猜測 unblock-us 等用的解決方案應該是這樣,在 DNS 那層把 server 端的 ip 反解成他們自己的機器,當 client 端送 request 給 http://api.poorguy.com, unblock-us 會先檢查這個 call 的用途,如果是做 geo-location 反解的,就送個假的回應給 client 端,如果是其他的方式,就轉送到正常的機器。

等 client 端把這些處理做完後,要做影音串流的 rstp://stream.poorguy.com 又是傳回正常的 ip ,所以這樣用到的流量最小。

這樣等於是直接的違反人家的授權去 hack API,這種服務可以活的很久嗎??我不清楚

不過從這一點也可以看到,各大廠做 operation 的能力優劣,像我的前雇主 rhapsody就有利用 client authentication 在各式各樣的 client 端上。所以讓我想在台灣用都沒得用 :(


公商服務時間:

小弟的公司,目前有時間接設計 WebService API 的案子,若是有合作機會的,請與我連絡。

Thursday, December 8, 2011

Lucene On Android

嘗試性的把 Lucene 放到 Android 上面來跑,結果不是太理想,但仍是一些心得分享出來,省去後人嘗試的時間。

Lucene 要跑在 Android 上,第一個碰上的問題是,如何把 index files 傳到手機上去,在 Lucene 中,對 index 的讀取,是以目錄為單位的,所以說,無法把 index files 放在 apk 中直接讀取,一定要存放在 device or external storage 上,才能夠使用;或者是自己弄個虛擬目錄出來,不過,這會耗用過多的計憶體空間。

我是選用把 index 放在 'src/res/raw' 底下,讓他變成 apk 的一部份,省去在網路上找個空間來放置 index 的問題,當要更新 index 時,就重編個 apk 叫使用者更新就好。

在放 lucene index 時,如果你想用 compound format 的話,可以用底下的指令,把多個 index files 包裹成單一檔案 .cfs

// run REPL with 'scala -cp luke-3.4.0_1-all.jar'

import org.apache.lucene.store.FSDirectory
import org.apache.lucene.index._
import org.apache.lucene.analysis.standard._

val source = FSDirectory.open(new java.io.File("source"))
val dest = FSDirectory.open(new java.io.File("dest"))

// open source index
val reader = IndexReader.open(source)

// create writer for compound index.
val analyzer = new StandardAnalyzer(org.apache.lucene.util.Version.LUCENE_34)
val writer = new org.apache.lucene.index.IndexWriter(dir, analyzer, IndexWriter.MaxFieldLength.UNLIMITED)

// force writer always use compound index format.
writer.getMergePolicy.asInstanceOf[LogByteSizeMergePolicy].setNoCFSRatio(1.0)


// add source index to dest index.
writer.addIndexes(reader)
writer.optimized
writer.close

reader.close

接著,是把產生的 segment, segments_1, _0.cfs 拷到 src/res/raw 底下,讓這些檔案變成 .apk 的一部份。


接著,是要在第一次執行時,把這些 index 從 apk 中覆製到 SD 卡上或是機子上,這邊,我寫了個小工具來做這件事

import android.content.Context
import android.os.Environment
import android.util.Log

import com.bluetangstudio.android.disastermap.TaipeiDisasterApp.LogTag

import org.apache.commons.io.FileUtils
import org.apache.lucene.store.{FSDirectory, Directory}

import scala.collection.JavaConversions._
import java.io.File

/**
 *  Helper class that search for lucene index directories on the device. The search order is
 *  external storage first then local storage. If lucene index does not exist on device, this
 *  class will copy the index from the apk to the device storage.
 *
 * @param context  the application context
 * @param path     the root folder name of the index directory to use and to look for.
 * @param source   the source of index resource to copy if index does not exist on the device.
 *                 format: Seq[(filename, resourceId)]
 */
case class LuceneOpenHelper(context: Context, path: String, source: Seq[Tuple2[String, Int]]) {

    /**
     * create or open an Directory.
     */
    def open(): Option[Directory] = {
        val candidates = Seq(externalFolder, internalFolder).flatten

        // find the folder with index in it.
        val folder = candidates.filter(f => f.exists() && f.list().length > 0).headOption
        val withIndex = folder.orElse(
            candidates.find(
                f => {
                    // ensure folder is available.
                    f.exists() || f.mkdirs() match {
                        // folder is not accessible
                        case false => false

                        case _ => {
                            Log.d(LogTag, "Duplicating index from apk to %s...".format(f))
                            source.foreach(
                                s => {
                                    val is = context.getResources.openRawResource(s._2)
                                    try {
                                        FileUtils.copyInputStreamToFile(is, new File(f, s._1))
                                    } finally {
                                        is.close()
                                    }
                                }
                            )
                            true
                        }
                    }
                }
            )
        )

        return withIndex.map(FSDirectory.open(_))
    }

    private def externalFolder: Option[File] = {
        Environment.getExternalStorageState match {
            case Environment.MEDIA_MOUNTED => Option(context.getExternalFilesDir(path))
            case _ => None
        }
    }

    private def internalFolder: Option[File] = {
        return Option(new File(context.getFilesDir, path))
    }

}

最後,是在 *App 上加上這段

object MyApp {
    private val INDEX_DIRECTORY = "idx"

    private val INDEX_FILES = Seq(
        ("_0.cfs", R.raw.idx_0), 
        ("segments", R.raw.segments), 
        ("segments_1", R.raw.segments_1)
    )
}
class MyApp extends android.app.Application {

    import MyApp._

    private var _luceneSearcher: Option[IndexSearcher] = None

    def luceneSearcher: Option[IndexSearcher] = {
        if (_luceneSearcher.isEmpty) {
            Log.d(LogTag, "Initializing new IndexSearcher...")
            _luceneSearcher = LuceneOpenHelper(this, INDEX_DIRECTORY, INDEX_FILES).open().map(new IndexSearcher(_))
        }
        _luceneSearcher
    }
   
    override def onLowMemory() {
        _luceneSearcher.foreach(s => s.close())
        _luceneSearcher = None
    }
}

這樣一來,就能在 Android 上面跑 lucene-core 了.