#!/bin/bash
# 服务器管理控制脚本
# author huangyongxing@yeah.net
# 2016-11-24

# 获取脚本所在目录的父目录（即server目录）的全路径
BASE_DIR=$(dirname $(cd $(dirname $0); pwd))

# 以下参数，由脚本命令行c参数 指定外部配置文件来设定

# 配置文件示例：
# ===================================================
# 游戏运行参数配置
# SCREEN_RUN=TRUE
#
# IS_CROSS_CENTER=FALSE
# PROCESS_MAKE_LIMIT=2
#
# HOST=127.0.0.1
# PORT=9310
# SERVER_ID=1
# NODE_NAME_PREFIX=jh_develop_
# ERL_COOKIE_PREFIX=jianghu_
#
# ---------------------------
# 以下为可选项
# ---------------------------
#
# 默认节点Cookie分跨服和游戏服(如果已指定则不按以下规则生成):
# ERL_COOKIE=${ERL_COOKIE_PREFIX}center
# ERL_COOKIE=${ERL_COOKIE_PREFIX}${SERVER_ID}
#
# APP_CFG=gsrv
#
# ERL=/usr/local/bin/erl
#
# ===================================================

# ===================================================
# 其他说明
# ===================================================
# 默认节点名分跨服和游戏服(不能直接指定节点名):
# ${NODE_NAME_PREFIX}center@${HOST}
# ${NODE_NAME_PREFIX}${SERVER_ID}@${HOST}
#
# PS:节点间通讯端口由gsrv.config/cls.config中配置，不在启动参数中控制
# 注意，下文中使用eval "${ERL}"，目的是配合测试服使用，
#       可以通过修改ERL变量实现系统时间的单独设置，这些情况需要先对ERL展开，
#       所以需要在运行前执行eval将字符串转换为相关命令（${ERL}需要带引号才兼容各类情况）

# ===================================================

# 读取脚本参数
while getopts 'c:' OPT; do
    case $OPT in
        c) CONF_FILE="$OPTARG";;
        ?)
            fun_usage
            exit 1;;
    esac
done
shift $(($OPTIND - 1))

if [ -z "$CONF_FILE" ] ; then
    CONF_FILE="${BASE_DIR}/script/ctl.conf"
fi

# 如果存在外部配置文件，以上配置以外部配置为准
if [[ "$CONF_FILE" =~ ^/.* ]] ; then
    # 绝对路径
    if [ -f "$CONF_FILE" ] ; then
        source "$CONF_FILE"
    fi
else
    # 非绝对路径
    # 尝试在当前执行路径 及 script目录下查找文件
    if [ -f "$CONF_FILE" ] ; then
        # 使用./指明路径，避免出现 file not found 问题
        source "./$CONF_FILE"
    elif [ -f "$BASE_DIR/script/$CONF_FILE" ] ; then
        source "$BASE_DIR/script/$CONF_FILE"
    fi
fi

# 默认的游戏节点config，游戏服为gsrv.config，跨服为cls.config
if [ -z "${APP_CFG}" ] ; then
    if [ "${IS_CROSS_CENTER}" = "TRUE" ] ; then
        APP_CFG=cls
    else
        APP_CFG=gsrv
    fi
fi

if [ -z "$ERL_COOKIE" ] ; then
    if [ "$IS_CROSS_CENTER" = "TRUE" ] ; then
        ERL_COOKIE=${ERL_COOKIE_PREFIX}center
    else
        ERL_COOKIE=${ERL_COOKIE_PREFIX}${SERVER_ID}
    fi
fi

# ===================================================
# 必须/重要的基础默认参数设置
[ -z "$HOST" ] && HOST=127.0.0.1
[ -z "$ERL" ] && ERL="$(which erl 2>/dev/null || echo -n /usr/local/bin/erl)"
[ -z "$SCREEN_RUN" ] && SCREEN_RUN=TRUE
[ -z "$IS_CROSS_CENTER" ] && IS_CROSS_CENTER=FALSE
[ -z "$PROCESS_MAKE_LIMIT" ] && PROCESS_MAKE_LIMIT=4
# ===================================================

# ===================================================
# 参数版本差异管理
ERTS_VER=$(eval "${ERL}" +V 2>&1 | grep -o "[0-9\.]\+")
ERTS_VER_MAIN=${ERTS_VER/.*/}

# 粗略以erts 9分界，省去查erts release版本历史
if [[ "$ERTS_VER_MAIN" < "9" ]] ; then
    ERL_OPT_S_SINGLE="+P 204800 +K true +A 100 +spp true -smp disable -hidden +sbwt none"
    ERL_OPT_NORMAL="+P 204800 +K true +A 100 +spp true -smp enable -hidden +sbwt none"
else
    ERL_OPT_S_SINGLE="+P 204800 +K true +A 100 +spp true +S 1 -hidden +sbwt none"
    ERL_OPT_NORMAL="+P 204800 +K true +A 100 +spp true +S 0 -hidden +sbwt none"
fi
# ===================================================

function fun_usage()
{
    script_name=`basename $0`
    cat << EOF
usage:
    启动/关闭/重启服务器
    sh $script_name [-c ConfigFile] <start | stop | restart>
    完整编译代码（erlang工程目录完整编译）
    sh $script_name [-c ConfigFile] make [ MakeOptions ]
        MakeOption以逗号分隔，例如： "{d,'LANG_VER',\"taiwan\"},load"
    快速编译代码（以ebin目录更新时间来筛选编译内容，如果后台单更配置会影响准确性）
    sh $script_name [-c ConfigFile] fast_make [ MakeOptions ]
        MakeOption以逗号分隔，例如： "{d,'LANG_VER',\"taiwan\"},load"
    热更服务器
    sh $script_name [-c ConfigFile] hot
    执行erl cmd
    sh $script_name [-c ConfigFile] do <LINE_NUM | all> <ERL_CMD>
    备注：-c ConfigFile 指定配置文件，便于开发版本在同一目录下运行跨服
EOF
}

function fun_get_node_name()
{
    if [ "$IS_CROSS_CENTER" = "TRUE" ] ; then
        echo ${NODE_NAME_PREFIX}center@${HOST}
    else
        echo ${NODE_NAME_PREFIX}${SERVER_ID}@${HOST}
    fi
}

# 这种只适应于LINUX系统，WINDOWS下的mingw环境中无法取得对应的cookie
function fun_get_cookie_by_node_name()
{
    ps -ef | grep "beam" | grep "\-name $1" | sed -n 's/.*setcookie \([^ ]*\) .*/\1/p'
}

# LINUX系统使用
# fun_kill_node
function fun_kill_node()
{
    local pids=""
    if [ -z "$1" ] ; then
        # 什么都没指定
        pids=$(ps -ef | grep beam | grep -v "\bgrep\b" | grep "\b${ERL_COOKIE}\b" | awk '{printf $2" ";}')
    elif [ -z "$2" ] ; then
        # 只指定了节点名
        pids=$(ps -ef | grep beam | grep -v "\bgrep\b" | grep "\b${ERL_COOKIE}\b" | grep "\b$1\b" | awk '{printf $2" ";}')
    else
        # 指定了节点名和cookie
        pids=$(ps -ef | grep beam | grep -v "\bgrep\b" | grep "\b${2}\b" | grep "\b$1\b" | awk '{printf $2" ";}')
    fi
    if [ -n "$pids" ] ; then
        kill -9 "$pids"
    fi
}

function fun_create_script()
{
    local node_name script_file port erl_option
    node_name=$1
    script_file=${BASE_DIR}/script/game_server.sh
    if [ "$IS_CROSS_CENTER" = "TRUE" ] ; then
        script_file=${BASE_DIR}/script/game_center.sh
    fi
    port=$PORT
    erl_option="${ERL_OPT_NORMAL}"
    if [ "$SCREEN_RUN" != "TRUE" ] ; then
        # local os=$(uname)
        # if [[ ! "${os}" =~ ^.*NT.*$ ]] ; then
        #     erl_option="-noinput ${ERL_OPT_NORMAL}"
        # fi
        # 非screen运行，统一后台运行，需要加-noinput之类后台运行参数
        erl_option="-noinput ${ERL_OPT_NORMAL}"
    fi
    cat > $script_file <<EOF
#!/bin/bash
cd ${BASE_DIR}/config
ulimit -HSn 204800
ulimit -c 409600
# ulimit -c unlimited
eval "${ERL}" ${erl_option} -name ${node_name} -setcookie ${ERL_COOKIE} -boot start_sasl -config ${APP_CFG} -pa ../ebin -s main start -extra ${HOST} ${port}
EOF
    echo $script_file
}

fun_start_node()
{
    local node_name=$1
    script_file=$(fun_create_script $node_name)
    screen_name=${node_name%@*}
    fun_run_script $script_file $screen_name
}

function fun_start()
{
    node_name=$(fun_get_node_name)
    fun_start_node ${node_name}
}

# fun_run_script script_file screen_name
function fun_run_script()
{
    chmod +x $1
    local logfile=${BASE_DIR}/logs/${screen_name}_`date +"%Y%m%d_%H%M%S.txt"`
    if [ "${SCREEN_RUN}" == "TRUE" ] ; then
        local screenrcfile="${BASE_DIR}/script/screenrc"
        cat > "${screenrcfile}" <<EOF
multiuser on
acladd root
defencoding utf8
deflog on
# defwritelock on
logfile $logfile
EOF
        # 以detached模式启动，自动记录日志
        screen -U -dmS $2 -c "${screenrcfile}" -s $1
    else
        # local os=$(uname)
        # if [[ "${os}" =~ ^.*NT.*$ ]] ; then
        #     start sh $1
        # else
        #     sh $1 >$logfile 2>&1 &
        # fi
        sh $1 >$logfile 2>&1 &
    fi
}

function fun_restart()
{
    fun_stop "$@"
    fun_start "$@"
}

# LINUX系统使用
function fun_get_alive_num()
{
    if [ -z "$1" ] ; then
        echo $(ps -ef | grep beam | grep -v "\bgrep\b" | grep -c "\b${ERL_COOKIE}\b")
    else
        line_num=$1
        node_name=$(fun_get_node_name ${line_num})
        echo $(ps -ef | grep beam | grep -v "\bgrep\b" | grep "\b${ERL_COOKIE}\b" | grep -c "\b${node_name}\b")
    fi
}

function fun_stop()
{
    cd ${BASE_DIR}/ebin
    local cookie node_name stop_node_name
    if [ -z "$1" ] ; then
        node_name=$(fun_get_node_name)
        cookie=${ERL_COOKIE}
    else    # 指定了节点
        node_name=$1
        cookie=$(fun_get_cookie_by_node_name "${node_name}")
    fi
    stop_node_name=${node_name/@/_stop_${RANDOM}@}
    eval "${ERL}" -noshell +S 1 -hidden -name ${stop_node_name} -setcookie ${cookie} -pa ../ebin -s main stop_server -extra ${node_name}
    fun_kill_node "${node_name}" "${cookie}"
}

# 判断编译版本（非语言版本）
# 主要指DEBUG、DEBUG_CMD、DEBUG_SQL、RELEASE，影响日志打印
# 参数用DEV_SERVER代替DEBUG的原因是写日志的宏名叫DEBUG，为使定义清晰，区别定义
function judge_make_ver() {
    local params="$@"
    if [ -z "$params" ] ; then
        echo -n "RELEASE"
        return 0
    fi
    case "$params" in
        *"'DEV_SERVER'"*)
            echo -n "DEBUG";;
        *"'DEBUG_CMD'"*)
            echo -n "DEBUG_CMD";;
        *"'DEBUG_SQL'"*)
            echo -n "DEBUG_SQL";;
        *)
            echo -n "RELEASE";;
    esac
}

# 拷贝依赖项
function fun_cp_dep() {
    cd ${BASE_DIR}
    local makeVer needCopy=FALSE
    if [ -d "dep/ebin" ] ; then
        # 奇怪的现象，在web中调用的时候，$PATH没问题，
        # 但是$(which rsync)子进程中，$PATH环境变量丢失，变成null了
        # 为什么date、find之类的命令不会有问题？
        export PATH=$PATH
        local rsync=$(which rsync)
        makeVer=$(judge_make_ver "$@")
        if [ -e "ebin/game_logger.beam" ] ; then
            needCopy=TRUE
        else
            cmp -s "dep/ebin/${makeVer,,}/game_logger.beam" "ebin/game_logger.beam"
            [ "$?" != "0" ] && needCopy=TRUE
        fi
        if [ -z "$rsync" ] ; then
            \cp -a -u -t ebin dep/ebin/*.beam
        else
            "$rsync" -avhc --partial --include=*.beam --exclude=* dep/ebin/ ebin/
        fi
        # 版本如有差异则进行复制
        [ "$needCopy" == "TRUE" ] && \cp -a "dep/ebin/${makeVer,,}/game_logger.beam" "ebin/"
    fi
}

# 如无参数，则按默认规则编译，无指定特殊参数，即makeOptions为[]
# 需要指定编译参数时，示例：
# fun_make "{d,'DEBUG'}"， 即 [{d, 'DEBUG'}]
function fun_make()
{
    cd ${BASE_DIR}
    local make_options="[$1]"
    local startTime endTime
    startTime=$(date +%s)
    echo "开始于 "$(date +"%Y-%m-%d %H:%M:%S" -d @${startTime})
    eval "${ERL}" ${ERL_OPT_NORMAL} -noinput -pa ./ebin -eval '"mmake:all(${PROCESS_MAKE_LIMIT}, ${make_options}), erlang:halt()"'
    endTime=$(date +%s)
    echo "结束于 "$(date +"%Y-%m-%d %H:%M:%S" -d @${endTime})
    echo "时间消耗 $((${endTime} - ${startTime})) seconds"
}

function fun_fast_make()
{
    cd ${BASE_DIR}
    local startTime=$(date +%s)
    local startTimeStr=$(date +"%Y-%m-%d %H:%M:%S" -d @${startTime})
    echo "开始于 ${startTimeStr}"
    local make_options="[$1]"
    local makeTimeFile="${BASE_DIR}/script/.maketime"
    local newerThenFile="${BASE_DIR}/ebin"
    local lastMakeTime=""
    if [ -e "${makeTimeFile}" ] ; then
        lastMakeTime="$(cat ${makeTimeFile})"
        if [[ ! "${lastMakeTime}" =~ ^[0-9]{4,4}(-[0-9]{1,2}){1,2}\ [0-9]{1,2}(:[0-9]{1,2}){1,2}$ ]] ; then
            lastMakeTime=""
        fi
    fi
    if [ "${lastMakeTime}" = "" ] ; then
        # 以ebin目录的最后修改时间作为上次编译时间
        lastMakeTime=$(stat --format="%y" "${newerThenFile}")
        lastMakeTime=${lastMakeTime%%.*}
    fi
    local modules=$(fun_get_update_modules "${lastMakeTime}")
    if [ "$modules" != "" ] ; then
        eval "${ERL}" ${ERL_OPT_NORMAL} -noinput -pa ./ebin -eval '"case mmake:files(${PROCESS_MAKE_LIMIT}, [${modules}], ${make_options}) of up_to_date -> file:write_file(\"${makeTimeFile}\", unicode:characters_to_binary(\"${startTimeStr}\")); _Error -> ignore end, erlang:halt()"'
    else
        echo "找不到需要编译的模块"
    fi
    local endTime=$(date +%s)
    echo "结束于 "$(date +"%Y-%m-%d %H:%M:%S" -d @${endTime})
    echo "时间消耗 $((${endTime} - ${startTime})) seconds"
}

# join数组，参数1为分隔符，参数2为数组
# 示例： join_array "," ${arr[@]}
function join_array() {
    local sep=$1 ret=$2 IFS=
    shift 2 || shift $#
    echo "${ret}${*/#/$sep}"
}

# 获取可能的更新模块
function fun_get_update_modules()
{
    cd ${BASE_DIR}
    local lastMakeTime="$1"
    declare -A modules=()
    declare -A hrls=()
    local file ext
    # 获取erl和hrl更新文件，并归类记录
    #（默认路径中不存在空格，如果存在空格，使用for读取会有问题）
    # find -newer "ebin" -name "*.*rl"
    for file in $(find -newermt "${lastMakeTime}" -name "*.*rl") ; do
        ext=${file##*.}
        if [ "${ext}" = "erl" ] ; then
            modules[${file/.\//}]=1
        elif [ "${ext}" = "hrl" ] ; then
            hrls[${file##*/}]=1
        fi
    done
    # 查找受到hrl影响的erl文件，并追加记录到更新模块文件列表
    local hrlsRegex=$(join_array '\|' "${!hrls[@]}")
    if [ "${hrlsRegex}" != "" ] ; then
        for file in $(grep -rl "^\s*\-\s*include\s*(\s*\"\(${hrlsRegex}\)\"\s*)" * --include=*.erl) ; do
            modules[$file]=1
        done
    fi
    if [ "${#modules[@]}" -eq 0 ] ; then
        echo ""
    else
        echo "\""$(join_array "\",\"" "${!modules[@]}")"\""
    fi
}

function fun_hot()
{
    cd ${BASE_DIR}
    local cookie node_name hot_node_name
    if [ -z "$1" ] ; then
        node_name=$(fun_get_node_name)
        cookie=${ERL_COOKIE}
    else
        node_name=$1
        cookie=$(fun_get_cookie_by_node_name "${node_name}")
    fi
    hot_node_name=${node_name/@/_hot_$(date +%s%N)@}
    # 添加timer:sleep/1是为了使热更结果尽可能完整打印到这里
    eval "${ERL}" -noinput -name ${hot_node_name} -setcookie ${cookie} -pa ../ebin -eval '"rpc:call('\''${node_name}'\'', u, u, [md5]), timer:sleep(3000), erlang:halt()"'
}

function fun_do()
{
    cd ${BASE_DIR}/ebin/
    local node_name=$1 cookie
    if [[ -n "$2" && "$node_name" =~ ^[a-zA-Z0-9_]+@[0-9a-zA-Z.]+$ ]] ; then
        # 存在第二个参数且第一个参数是节点名形式，按指定节点执行的方式处理
        shift 1
        cookie=$(fun_get_cookie_by_node_name "${node_name}")
    else
        node_name=$(fun_get_node_name)
        cookie="${ERL_COOKIE}"
    fi
    local erl_cmd=$(echo "$@")
    if [[ ! "$erl_cmd" =~ ^[^.]*\.\ *$ ]] ; then
        echo -e "执行的erlang命令有误，必须是以.号结束，并且只允许有一个.号的完整erlang语句，例如 \"A=123, myfun(A).\""
        return 1
    fi
    local do_node_name=${node_name/@/_do_$(date +%s%N)@}
    RPC_CMD="R = rpc:call('${node_name}', util, eval, [\"$erl_cmd\"]), io:format(\"eval result: ~w~n\", [R])."
    eval "${ERL}" -noinput -name ${do_node_name} -setcookie ${cookie} -eval '"io:setopts([{encoding, unicode}]), $RPC_CMD"' -eval '"erlang:halt(0)."'
}

function fun_main()
{
    local do=$1
    shift
    if [ -z "$ERL_COOKIE" -a "$do" != "make" ] ; then
        echo "ERL COOKIE 未设置!"
        return 1
    fi
    case $do in
        fast_make)
            fun_cp_dep "$@"
            fun_fast_make "$@"
            ;;
        make)
            fun_cp_dep "$@"
            fun_make "$@"
            ;;
        start)
            fun_start "$@"
            ;;
        stop)
            fun_stop "$@"
            ;;
        restart)
            fun_restart "$@"
            ;;
        hot)
            fun_hot "$@"
            ;;
        do)
            fun_do "$@"
            ;;
        *)
            fun_usage
            ;;
    esac
}

fun_main "$@"

