#!/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] 完整编译代码(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 备注:-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 < "${screenrcfile}" <$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 "$@"