【问题标题】:Spawn server and wait, repeat on SIGHUP生成服务器并等待,在 SIGHUP 上重复
【发布时间】:2013-02-19 18:07:26
【问题描述】:

我正在尝试编写一个脚本,它将启动我的(节点)开发服务器,并且每当它收到 SIGHUP 时,它应该重新启动服务器。

我已经完成了生成服务器、关闭它并在SIGHUP 上重新启动服务器。但是因为我在生成代码中使用了wait,所以SIGHUP 处理程序永远不会真正返回,这导致信号永远不会再次触发。

这是我的脚本的精简版:

SERVER_PID=""

start_server() {
    npm start &
    SERVER_PID=$!
    wait $SERVER_PID
}
terminate_server() {
    [ ! "xSERVER_PID" = "X" ] && kill -SIGTERM $SERVER_PID
    SERVER_PID=""
}
refresh_server() {
    terminate_server
    start_server
}

trap refresh_server SIGHUP
start_server

正如我所提到的,它可以正常启动服务器,并且在第一个 SIGHUP 上按预期工作,但由于 start_server 中的 wait 导致 refresh_server 永远不会返回,后续信号不会触发任何操作.

目前,我已经通过在start_server 中取出wait 并在底部添加一个无限的“while-true-sleep”循环(在初始调用start_server 之后)解决了这个问题,但我确信必须有更好的方法来完成我想要实现的目标。另外,我不喜欢睡眠循环方法导致的信号触发延迟。

【问题讨论】:

  • 通常,您使用 init(或 upstartsystemd 或任何您的操作系统使用的)来进行此类服务监控。
  • @chepner 自然而然。这适用于在我的本地主机上运行的开发服务器。 SIGHUP'ing 将由 watch+build 脚本在代码的相关部分发生更改时完成。用手做这件事开始让我生气了。

标签: bash shell signals


【解决方案1】:

while-true-wait 循环怎么样?

#!/bin/bash
SERVER_PID=""
SERVER_NAME="npm start"

start_server() {
    $SERVER_NAME &
    SERVER_PID=$!
    SERVER_ACTIVE=true
}
terminate_server() {
    [ ! "xSERVER_PID" = "X" ] && kill -SIGTERM $SERVER_PID
    SERVER_PID=""
}
refresh_server() {
    terminate_server
    start_server
}

trap refresh_server SIGHUP
start_server
while $SERVER_ACTIVE; do
  SERVER_ACTIVE=false
  wait $SERVER_PID
done

需要某种等待事件循环,如果应该重复,要么在脚本中显式地重复,要么在 bash 中隐藏的某个地方

【讨论】:

  • 非常好。我非常喜欢这个。
  • @F.Hauri:这真的没关系,SERVER_PID 不是“”,当调用终止时,或者你有其他问题......即使是这样,没有参数的 callnig kill 确实不做任何伤害
  • 好吧,$ 在我原来的 sn-p 中也不见了,无论如何我都不会投反对票。
  • 你错了:Trap 必须不执行而只需标记,以防止反弹。这项工作,但不是很好
  • [ ! "xSERVER_PID" = "X" ] 是什么意思?你似乎错过了一些$ 和一些测试......
【解决方案2】:

这是一个带有尾递归 start_server 的解决方案,只需对您的代码进行少量更改。 (去掉一行,增加三行包含HUPPED

HUPPED=false
SERVER_PID=""

start_server() {
    npm start &
    SERVER_PID=$!
    wait $SERVER_PID
    if $HUPPED; then HUPPED=false; start_server; fi
}
terminate_server() {
    [ ! "xSERVER_PID" = "X" ] && kill -SIGTERM $SERVER_PID
    SERVER_PID=""
}
refresh_server() {
    HUPPED=true
    terminate_server
    #start_server
}

trap refresh_server SIGHUP
start_server

【讨论】:

    【解决方案3】:

    bash 下的信号陷阱

    异常返回

    就像在其他编程语言中使用信号一样,信号捕获很容易以错误的方式完成;

    当使用trap 时,您必须在陷阱评估内部处理您的函数,而只需设置一个 标志 main 程序可以在 trap 异常 结束后进行检查,以尽可能缩短异常执行

    特别是,您必须在陷阱执行级别向子进程发起fork

    一个正确的例子

    #!/bin/bash
    
    SERVER_PID=""
    CMD_TRAP=""
    
    npm() { #Doing something that could be checked from external
        if [ "$1" ] && [ "$1" == "start" ] ;then
        while :;do
            date "+%s%N" >/tmp/dummyfile.txt
            sleep .333
              done
        fi
    }
    
    start_server() {
        npm start &
        SERVER_PID=$!
    }
    terminate_server() {
        [ "$SERVER_PID" ] && ps $SERVER_PID &>/dev/null && kill -TERM $SERVER_PID
        SERVER_PID=""
    }
    refresh_server() {
        terminate_server
        start_server
    }
    
    printf "for:\n   server restart, hit: 'kill -USR2 %d'\n" $$
    printf "   server stop, hit: 'kill %d' (or Ctrl+C)\n" $$
    
    trap 'CMD_TRAP=refresh' USR2 HUP
    trap 'CMD_TRAP=terminate' TERM INT
    
    start_server
    while [ "$SERVER_PID" ];do 
        wait $SERVER_PID
        case "$CMD_TRAP" in
            refresh   ) refresh_server   ;;
            terminate ) terminate_server ;;
            *         ) refresh_server   ;;        # in case server just end.
          esac;
        CMD_TRAP=""
        [ "$SERVER_PID" ] && echo "LOOP." || echo "EXIT."
      done
    

    特点

    这个演示脚本做:

    • 服务将在脚本启动时启动,
    • 如果收到USR2HUP 信号,服务将重新启动,
    • 服务将在刚刚完成或收到任何信号时重新启动,并且
    • 服务将被正确停止是一个TERM信号被接收或
      • 如果Ctrl-C 在控制台上被点击。
    • 异常处理正确(立即返回主程序)
    • 没有不需要的/不受管理的错误消息

    输出样本

    window1

    tty
    /dev/pts/0
    

    window2

    ps --tty pts/0 fw
      PID TTY      STAT   TIME COMMAND
     2996 pts/0    Ss     0:01 bash
     5187 pts/0    S+     0:00  \_ bash
    

    window1

    ./serverScript.sh 
    for:
       server restart, hit: 'kill -USR2 11469'
       server stop, hit: 'kill 11469' (or Ctrl+C)
    

    window2

    ps --tty pts/0 fw
      PID TTY      STAT   TIME COMMAND
     2996 pts/0    Ss     0:01 bash
     5187 pts/0    S      0:00  \_ bash
    11469 pts/0    S+     0:00      \_ /bin/bash ./servermon.sh
    11470 pts/0    S+     0:00          \_ /bin/bash ./servermon.sh
    
    cat /tmp/dummyfile.txt 
    1361603642256133674
    
    cat /tmp/dummyfile.txt 
    1361603648712606114
    
        ps --tty pts/0 fw
      PID TTY      STAT   TIME COMMAND
     2996 pts/0    Ss     0:01 bash
     5187 pts/0    S      0:00  \_ bash
    11469 pts/0    S+     0:00      \_ /bin/bash ./servermon.sh
    11470 pts/0    S+     0:01          \_ /bin/bash ./servermon.sh
    16814 pts/0    S+     0:00              \_ sleep .333
    
    kill -USR2 11469
    

    window1

    LOOP.
    

    window2

        ps --tty pts/0 fw
      PID TTY      STAT   TIME COMMAND
     2996 pts/0    Ss     0:01 bash
     5187 pts/0    S      0:00  \_ bash
    11469 pts/0    S+     0:00      \_ /bin/bash ./servermon.sh
    17152 pts/0    S+     0:00          \_ /bin/bash ./servermon.sh
    17532 pts/0    S+     0:00              \_ sleep .333
    
    cat /tmp/dummyfile.txt
    1361604208069564188
    cat /tmp/dummyfile.txt
    1361604209103660589
    
    kill -USR2 11469
    

    window1

    LOOP.
    

    window2

    cat /tmp/dummyfile.txt
    1361604278583723517
    
    cat /tmp/dummyfile.txt
    1361604279605292149
    
    kill 11469
    

    window1

    EXIT.
    $
    

    Window 1 终止循环服务器子进程。

    window1

    ./serverScript.sh 
    for:
       server restart, hit: 'kill -USR2 19232'
       server stop, hit: 'kill 19232' (or Ctrl+C)
    

    那么如果Ctrl+C被按下:

    window1

    ^CEXIT.
    $
    

    window2

    ps --tty pts/0 fw
      PID TTY      STAT   TIME COMMAND
     2996 pts/0    Ss     0:01 bash
     5187 pts/0    S+     0:00  \_ bash
    

    说明

    无法堆叠异常。因此,在执行异常时,可以忽略另一个中断:

    如果作为示例,这可能变得很重要,您的 refresh_server() 函数必须在重新启动服务器之前压缩和轮换一些日志:

    refresh_server() {
        terminate_server
        lockedfilename=-$(date +%F_%H-%M-%S-$$)
        mv /srv/logfile /srv/logfile-$lockedfilename
        gzip /srv/logfile-$lockedfilename
        start_server
    }
    

    许多中断可以在主循环中汇总或忽略,但处理只能在主级别完成。

    有一个关于问题所在的小演示:

    window1

    trap "echo USR2 sleep 4;sleep 4" USR2
    while :;do printf "\r%s " $(date +%s%N);sleep .333;done
    1361606565xxxxxxxxx
    

    (xxxxxxxxx改变3x/秒)

    window2

    for ((i=5;i--;));do echo KILL;kill -USR2 5187;sleep .5;done
    KILL
    KILL
    KILL
    KILL
    KILL
    
    1361606722582175969 USR2 sleep 4
    USR2 sleep 4
    1361606770xxxxxxxxx
    

    我的循环中只捕获了超过 5 个中断。

    如果您搜索解释:5 x 0,5 = 2,5 秒。睡眠时间远远少于 4 秒,为什么我收到了第二个中断,但不是全部五个??

    在主循环中解除陷阱

    在主循环中有一个小技巧:

    while [ "$SERVER_PID" ];do 
        wait $SERVER_PID
    
        OLDIFS="$IFS" IFS=$'\n'
        TRAPS=($(trap))                            # save traps
        IFS="$OLDIFS"
        eval "$(printf "trap -- %s\n" ${TRAPS[@]##*\'})"  # untrap
    
        case "$CMD_TRAP" in
            refresh   ) refresh_server   ;;
            terminate ) terminate_server ;;
            *         ) refresh_server   ;;        # in case server just end.
          esac;
        CMD_TRAP=""
        if [ "$SERVER_PID" ]
        then echo "LOOP."
        else echo "EXIT."
    
            eval "$(printf "%s\n" "${TRAPS[@]}")"      # restore traps
        fi
      done
    

    【讨论】:

    • @nikc.org 你试过了吗?
    • 是的,我做到了。我在您的第一个示例中采用了标记方法。
    猜你喜欢
    • 1970-01-01
    • 2018-06-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-13
    • 2014-05-10
    • 1970-01-01
    相关资源
    最近更新 更多