监视文件,直到找到一个字符串


60

我正在使用tail -f监视正在主动写入的日志文件。当某个字符串写入日志文件时,我想退出监视,并继续执行脚本的其余部分。

目前,我正在使用:

tail -f logfile.log | grep -m 1 "Server Started"

找到字符串后,grep会按预期退出,但是我需要找到一种方法也可以使tail命令退出,以便脚本可以继续。


我想知道原始海报运行在什么操作系统上。在Linux RHEL5系统上,令我惊讶的是,一旦grep命令找到了匹配项并退出了,tail命令就会死掉。
ZaSter 2013年

4
@ZaSter:tail死亡仅在下一行。尝试以下操作:date > log; tail -f log | grep -m 1 trigger然后在另一个外壳程序中:echo trigger >> log您将trigger在第一个外壳程序中看到输出,但是命令没有终止。然后尝试:date >> log在第二个外壳中,第一个外壳中的命令将终止。但是有时候这为时已晚。我们希望在触发线出现后立即终止,而不是在触发线之后的行结束时终止。
Alfe

这是一个很好的解释和示例,@ Alfe。
ZaSter 2015年


Answers:


41

一个简单的POSIX单线

这是一个简单的单线。它不需要特定于bash或非POSIX的技巧,甚至不需要命名管道。所有你真正需要的是分离的终止tailgrep。这样,grep脚本一旦结束,即使tail尚未结束也可以继续。因此,这种简单的方法将带您到达那里:

( tail -f -n0 logfile.log & ) | grep -q "Server Started"

grep将阻塞直到找到该字符串,然后退出。通过tail从其自己的子外壳运行,我们可以将其放置在后台,使其独立运行。同时,一旦grep退出,主外壳就可以自由继续执行脚本。tail将在其子外壳中徘徊,直到将下一行写入日志文件,然后退出(甚至在主脚本终止后也可以退出)。要点是管道不再等待tail终止,因此管道一退出就grep退出。

一些小的调整:

  • tail如果字符串早在日志文件中存在,则选项-n0 使其开始从日志文件的当前最后一行开始读取。
  • 您可能要给tail-F而不是-f。它不是POSIX,但是tail即使在等待期间旋转日志,它也可以工作。
  • grep在第一次出现后,选项-q而不是-m1 退出,但不打印出触发行。也是POSIX,不是-m1。

3
此方法将永远让tail后台运行。您将如何tail在后台子外壳中捕获PID并将其暴露在主外壳中?我只能通过使用杀死所有会话附加tail进程来提出次可选解决方案pkill -s 0 tail
里克·范·德·泽特

1
在大多数使用情况下,这应该不是问题。首先执行此操作的原因是,因为您希望将更多行写入日志文件。tail尝试写入损坏的管道时将终止。管道将grep在完成后立即中断,因此一旦grep完成,tail它将在日志文件中再插入一行后终止。
00prometheus

当我用这个解决方案我也没有背景tail -f
Trevor Boyd Smith,

2
@Trevor Boyd Smith,是的,在大多数情况下都可以使用,但是OP问题是grep直到尾巴退出后才会完成,并且直到grep退出日志文件中出现另一行时尾巴才退出(当tail尝试供给因grep结尾而中断的管道)。因此,除非您没有背景尾巴,否则脚本将不会继续执行,直到在日志文件中出现多余的一行,而不是完全在grep捕获的行上为止。
00prometheus

关于“在[所需的字符串模式]之后出现另一行之前,跟踪不会退出”:这非常微妙,我完全没想到。我没有注意到,因为我正在寻找的图案在中间,并且所有这些都很快被打印出来了。(再次,您描述的行为非常微妙)
Trevor Boyd Smith,

59

接受的答案对我不起作用,而且令人困惑,它更改了日志文件。

我正在使用这样的东西:

tail -f logfile.log | while read LOGLINE
do
   [[ "${LOGLINE}" == *"Server Started"* ]] && pkill -P $$ tail
done

如果日志行与模式匹配,则tail终止由该脚本启动的启动。

注意:如果您还想在屏幕上查看输出| tee /dev/tty,请在while循环中进行测试之前,或者回显该行。


2
此方法有效,但是pkillPOSIX未指定它,并且并非在所有地方都可用。
理查德·汉森

2
您不需要while循环。将watch与-g选项一起使用,您可以省去讨厌的pkill命令。
l1zard 2015年

@ l1zard你能充实吗?在出现特定行之前,您将如何查看日志文件的末尾?(不太重要,但是当添加watch -g时我也很好奇;我有一个带有该选项的较新的Debian服务器,而另一个没有它的基于RHEL的旧服务器)。
罗伯·惠兰

我还不清楚为什么在这里甚至需要尾巴。据我了解,此正确性是用户希望在日志文件中出现某个关键字时执行特定命令。下面使用watch给出的命令可以完成此任务。
l1zard 2015年

不太准确-它正在检查何时将给定的字符串添加到日志文件中。我用它来检查Tomcat或JBoss何时完全启动。他们每次都写“服务器启动”(或类似内容)。
罗布·惠兰

16

如果您使用的是Bash(至少,但是POSIX似乎没有定义它,因此它在某些shell中可能会丢失),则可以使用以下语法

grep -m 1 "Server Started" <(tail -f logfile.log)

它的工作原理与前面提到的FIFO解决方案非常相似,但是编写起来却简单得多。


1
这行得通,但是尾巴仍然运行,直到您发送SIGTERM(Ctrl + C,退出命令或将其杀死)
记忆

3
@mems,日志文件中的任何其他行都可以。该tail会读它,尝试输出它,然后收到一个SIGPIPE这将终止。因此,原则上您是对的;tail如果没有任何内容再次写入日志文件,则可能会无限期运行。实际上,对于许多人来说,这可能是一个非常巧妙的解决方案。
Alfe

14

有几种tail退出方法:

可怜的方法:强迫tail写另一行

找到匹配项并退出tail后,可以立即强制写另一行输出grep。这将导致tail获得SIGPIPE,导致退出。一种方法是tailgrep退出后修改被监视的文件。

这是一些示例代码:

tail -f logfile.log | grep -m 1 "Server Started" | { cat; echo >>logfile.log; }

在此示例中,在关闭其stdout cat之前不会退出grep,因此tailgrep有机会关闭其stdin 之前不太可能写入管道。 cat用于传播grep未修改的标准输出。

这种方法相对简单,但存在以下缺点:

  • 如果grep在关闭stdin之前先关闭stdout,则总会出现竞争状况: grep关闭stdout,触发cat退出,触发echo,触发tail输出行。如果此行被发送到grep之前grep有机会关闭stdin,tail则在SIGPIPE写入另一行之前不会得到。
  • 它需要对日志文件的写访问权。
  • 您必须可以修改日志文件。
  • 如果您恰巧与另一个进程同时写入,则可能会损坏日志文件(写入可能会交错,导致换行符出现在日志消息的中间)。
  • 此方法特定于tail-它不能与其他程序一起使用。
  • 第三管道阶段使得很难访问第二管道阶段的返回代码(除非您使用的是POSIX扩展名,例如bashPIPESTATUS数组)。在这种情况下这不是什么大问题,因为grep它将始终返回0,但一般而言,中间阶段可能会被您关心其返回代码的其他命令所代替(例如,检测到“服务器启动”时返回0的东西,1当检测到“服务器启动失败”时)。

接下来的方法可以避免这些限制。

更好的方法:避免流水线

您可以使用FIFO来避免流水线,一旦grep返回就允许继续执行。例如:

fifo=/tmp/tmpfifo.$$
mkfifo "${fifo}" || exit 1
tail -f logfile.log >${fifo} &
tailpid=$! # optional
grep -m 1 "Server Started" "${fifo}"
kill "${tailpid}" # optional
rm "${fifo}"

带有注释的行# optional可以删除,程序仍然可以运行。tail会一直持续到它读取另一行输入或被其他进程杀死为止。

这种方法的优点是:

  • 您不需要修改日志文件
  • 该方法除了适用于其他实用程序 tail
  • 它没有比赛条件
  • 您可以轻松获得grep(或您正在使用的任何替代命令)的返回值

这种方法的缺点是复杂性,尤其是管理FIFO:您需要安全地生成一个临时文件名,并且即使用户在中间按Ctrl-C,也需要确保删除该临时FIFO。剧本。这可以使用陷阱来完成。

替代方法:发送消息杀死 tail

您可以tail通过发送信号来使管道阶段退出SIGTERM。面临的挑战是可靠地知道代码中同一位置的两件事: tailPID和是否grep退出。

使用类似tail -f ... | grep ...这样的管道,可以很容易地修改第一个管道阶段,以tail通过后台tail和读取将PID 保存在变量中$!。修改第二个管道阶段以killgrep退出时运行也很容易。问题在于流水线的两个阶段在单独的“执行环境”(以POSIX标准的术语)中运行,因此第二个流水线阶段无法读取第一个流水线阶段设置的任何变量。在不使用外壳变量的情况下,第二阶段必须以某种方式找出tailPID,以便它可以tailgrep返回时终止,或者必须以某种方式在grep返回时通知第一阶段。

第二阶段可用于pgrep获取tail的PID,但这将是不可靠的(您可能匹配错误的过程)并且不可移植(pgrepPOSIX标准未指定)。

第一级可以通过echo输入PID 通过管道将PID发送到第二级,但是此字符串将与tail的输出混合。根据的输出,对两者进行解复用可能需要复杂的转义方案tail

您可以使用FIFO使第二个管道阶段在grep退出时通知第一个管道阶段。然后第一阶段就可以杀死tail。这是一些示例代码:

fifo=/tmp/notifyfifo.$$
mkfifo "${fifo}" || exit 1
{
    # run tail in the background so that the shell can
    # kill tail when notified that grep has exited
    tail -f logfile.log &
    # remember tail's PID
    tailpid=$!
    # wait for notification that grep has exited
    read foo <${fifo}
    # grep has exited, time to go
    kill "${tailpid}"
} | {
    grep -m 1 "Server Started"
    # notify the first pipeline stage that grep is done
    echo >${fifo}
}
# clean up
rm "${fifo}"

除了更复杂之外,此方法具有以前方法的优点和缺点。

关于缓冲的警告

POSIX允许对stdin和stdout流进行完全缓冲,这意味着tail的输出可能不会在grep任意长时间内被处理。在GNU系统上应该没有任何问题:GNU grep使用read(),可以避免所有缓冲,并且GNU 在写入stdout时tail -f会定期进行调用fflush()。非GNU系统可能必须执行一些特殊的操作才能禁用或定期刷新缓冲区。


您的解决方案(像其他人一样,我不会怪您)将丢失在监视开始之前已经写入日志文件的内容。该tail -f只输出的最后十行,然后所有的以下内容。为了改善这一点,您可以-n 10000在尾部添加选项,以便最后给出10000行。
Alfe

另一个想法:我认为,可以tail -f通过将fifo 的输出传递给fifo并对其进行grepping 来理顺您的fifo解决方案: mkfifo f; tail -f log > f & tailpid=$! ; grep -m 1 trigger f; kill $tailpid; rm f
Alfe

@Alfe:我可能是错的,但是我相信tail -f log写入FIFO将导致某些系统(例如,GNU / Linux)使用基于块的缓冲而不是基于行的缓冲,这意味着grep当它进入时可能看不到匹配的行出现在日志中。系统可能会提供一个实用程序来更改缓冲,例如stdbuf从GNU coreutils。但是,这种实用程序将是不可移植的。
理查德·汉森

1
@Alfe:实际上,看起来POSIX只是在与终端交互时没有对缓冲进行任何说明,因此从标准角度来看,我认为您的简单解决方案与我的复杂解决方案一样好。但是,我不确定100%每种情况下各种实现的实际行为。
理查德·汉森

实际上,我现在转向grep -q -m 1 trigger <(tail -f log)其他地方提出的更简单的建议,并接受这样的事实,即tail在后台运行的行比实际需要的行长。
Alfe

9

让我扩展一下@ 00prometheus答案(这是最好的答案)。

也许您应该使用超时而不是无限期地等待。

下面的bash函数将阻塞,直到出现给定的搜索词或达到给定的超时为止。

如果在超时时间内找到字符串,则退出状态将为0。

wait_str() {
  local file="$1"; shift
  local search_term="$1"; shift
  local wait_time="${1:-5m}"; shift # 5 minutes as default timeout

  (timeout $wait_time tail -F -n0 "$file" &) | grep -q "$search_term" && return 0

  echo "Timeout of $wait_time reached. Unable to find '$search_term' in '$file'"
  return 1
}

启动服务器后,日志文件可能还不存在。在这种情况下,您应该等待它出现后再搜索字符串:

wait_server() {
  echo "Waiting for server..."
  local server_log="$1"; shift
  local wait_time="$1"; shift

  wait_file "$server_log" 10 || { echo "Server log file missing: '$server_log'"; return 1; }

  wait_str "$server_log" "Server Started" "$wait_time"
}

wait_file() {
  local file="$1"; shift
  local wait_seconds="${1:-10}"; shift # 10 seconds as default timeout

  until test $((wait_seconds--)) -eq 0 -o -f "$file" ; do sleep 1; done

  ((++wait_seconds))
}

使用方法如下:

wait_server "/var/log/server.log" 5m && \
echo -e "\n-------------------------- Server READY --------------------------\n"

那么,timeout命令在哪里?
ayanamist

实际上,使用timeout是不无限期挂起无法启动且已经退出的服务器的唯一可靠方法。
gluk47 '17

1
这个答案是最好的。只需复制函数并调用它,它就非常容易且可重用
Hristo Vrigazov

6

因此,在进行了一些测试之后,我找到了一种快速的1行方法来完成这项工作。当grep退出时,tail -f似乎会退出,但是有一个陷阱。它似乎仅在打开和关闭文件时触发。我通过在grep找到匹配项时将空字符串附加到文件中来完成此操作。

tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> logfile \;

我不确定为什么文件的打开/关闭会触发tail来意识到管道已关闭,因此我不会依赖此行为。但目前看来行得通。

原因关闭,请查看-F标志和-f标志。


1
之所以可行,是因为追加到日志文件会导致tail输出另一行,但是到那时grep就退出了(可能-那里存在竞争条件)。如果grep已经退出时tail写另一行,tail将会得到一个SIGPIPE。这导致tail立即退出。
理查德·汉森

1
这种方法的缺点:(1)存在争用条件(它可能并不总是立即退出)(2)它需要对日志文件的写访问权(3)您必须可以修改日志文件(4)您可以破坏日志文件日志文件(5)仅适用于tail(6),您不能轻易地根据不同的字符串匹配(“服务器启动”与“服务器启动失败”)来调整其行为,因为您无法轻松获取返回代码管道的中间阶段。有一种避免所有这些问题的替代方法-请参阅我的答案。
理查德·汉森

6

目前,如给定的那样,tail -f此处的所有解决方案都存在冒起以前记录的“服务器启动”行的风险(在您的特定情况下,可能会或可能不会出现问题,具体取决于记录的行数和日志文件轮换/截断)。

就像使用bmike在perl snippit中显示的tail那样,不要使用复杂的东西,而只是使用更智能的东西。最简单的解决方案是retail集成了正则表达式支持以及启动停止条件模式的解决方案:

retail -f -u "Server Started" server.log > /dev/null

这将像普通文件一样跟随文件,tail -f直到出现该字符串的第一个实例,然后退出。(-u在正常的“跟随”模式下,该选项不会在文件的最后10行中的现有行上触发。)


如果使用GNU tail(来自coreutils),则下一个最简单的选择是使用--pidFIFO(命名管道):

mkfifo ${FIFO:=serverlog.fifo.$$}
grep -q -m 1 "Server Started" ${FIFO}  &
tail -n 0 -f server.log  --pid $! >> ${FIFO}
rm ${FIFO}

使用FIFO是因为必须分别启动进程才能获取并传递PID。FIFO仍然存在同样的问题,即为了及时tail接收写入而徘徊,导致接收SIGPIPE,请使用该--pid选项,以便tail在它注意到grep已终止时退出(通常用于监视写入器进程而不是读取器,但tail不会)。真的很在乎)。Option -n 0与一起使用,tail以便旧行不会触发匹配。


最后,您可以使用有状态的tail,它将存储当前文件的偏移量,因此后续调用仅显示新行(它也处理文件旋转)。本示例使用旧的FWTK retail*:

retail "${LOGFILE:=server.log}" > /dev/null   # skip over current content
while true; do
    [ "${LOGFILE}" -nt ".${LOGFILE}.off" ] && 
       retail "${LOGFILE}" | grep -q "Server Started" && break
    sleep 2
done

*注意,名称相同,程序与上一个选项不同。

与其将CPU占用时间作为循环,不如将文件的时间戳与状态文件(.${LOGFILE}.off)进行比较,然后进入休眠状态。-T如果需要,请使用“ ”指定状态文件的位置,以上假设当前目录。随意跳过该条件,或者在Linux上,您可以使用效率更高的方法inotifywait

retail "${LOGFILE:=server.log}" > /dev/null
while true; do
    inotifywait -qq "${LOGFILE}" && 
       retail "${LOGFILE}" | grep -q "Server Started" && break
done

我是否可以将retail超时与之结合,例如:“如果120秒过去了,而零售仍未读取该行,则给出错误代码并退出零售”?
kiltek

@kiltek使用GNU timeout(coreutils)启动retail并仅在超时时检查退出代码124(timeout将在您设置的时间后
终止

4

这将有些棘手,因为您将不得不进入过程控制和信令。使用PID跟踪的两个脚本解决方案会更加麻烦。最好使用这样的命名管道

您正在使用什么shell脚本?

快速而又肮脏的一种脚本解决方案-我将使用File:Tail制作一个perl脚本

use File::Tail;
$file=File::Tail->new(name=>$name, maxinterval=>300, adjustafter=>7);
while (defined($line=$file->read)) {
    last if $line =~ /Server started/;
}

因此,除了在while循环内打印之外,您还可以过滤字符串匹配并突破while循环,以使脚本继续。

这两种方法都应该只涉及一点点学习,以实现您正在寻找的观看流控制。


使用bash。我的perl-fu没那么强壮,但是我会试一试。
Alex Hofsteede 2011年

使用管道-他们喜欢bash,bash喜欢它们。(当您的备份软件碰到您的管道之一时,您的备份软件就会尊重您)
bmike 2011年

maxinterval=>300表示它将每五分钟检查一次文件。由于我知道我的行会立即出现在文件中,因此我使用了更具侵略性的轮询:maxinterval=>0.2, adjustafter=>10000
Stephen Ostermiller 2014年

2

等待文件出现

while [ ! -f /path/to/the.file ] 
do sleep 2; done

等待文件中的字符串

while ! grep "the line you're searching for" /path/to/the.file  
do sleep 10; done

https://superuser.com/a/743693/129669


2
这种轮询有两个主要缺点:1.通过一次又一次地浏览日志,浪费了计算时间。考虑一个/path/to/the.file1.4GB的大容量;那么显然这是一个问题。2.出现日志条目时,它的等待时间比必要时间要长,最坏的情况是10秒。
Alfe

2

我无法想象有比这更清洁的解决方案:

#!/usr/bin/env bash
# file : untail.sh
# usage: untail.sh logfile.log "Server Started"
(echo $BASHPID; tail -f $1) | while read LINE ; do
    if [ -z $TPID ]; then
        TPID=$LINE # the first line is used to store the previous subshell PID
    else
        echo "$LINE"; [[ "$LINE" == *"${*:2}"* ]] && kill -3 $TPID && break
    fi
done

好吧,这个名字可能会有所改善...

好处:

  • 它不使用任何特殊实用程序
  • 它不会写入磁盘
  • 它优雅地退出尾巴并关闭管道
  • 它很短,很容易理解

2

您不必这样做。我认为watch命令就是您想要的。watch命令监视文件的输出,当输出更改时,可以使用-g选项终止。

watch -g grep -m 1 "Server Started" logfile.log && Yournextaction

1
由于此操作每两秒钟运行一次,因此只要该行出现在日志文件中,它就不会立即退出。另外,如果日志文件很大,它也无法正常工作。
理查德·汉森


1

亚历克斯,我认为这将对您有很大帮助。

tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> /dev/null ;

此命令将永远不会在日志文件中提供条目,但会默默grep ...


1
这将行不通-您必须追加,logfile否则可能要花费任意长时间才能tail输出另一行并检测到grep已死(通过SIGPIPE)。
理查德·汉森

1

这是一个更好的解决方案,不需要您写入日志文件,这在某些情况下是非常危险的,甚至是不可能的。

sh -c 'tail -n +0 -f /tmp/foo | { sed "/EOF/ q" && kill $$ ;}'

当前它只有一个副作用,该tail过程将一直在后台运行,直到将下一行写入日志为止。


tail -n +0 -f从文件的开头开始。 tail -n 0 -f从文件末尾开始。
Stephen Ostermiller 2014年

1
我得到的另一个副作用:myscript.sh: line 14: 7845 Terminated sh -c 'tail...
Stephen Ostermiller 2014年

我相信在此答案中“下一个列表”应为“下一行”。
Stephen Ostermiller 2014年

这可以工作,但是tail进程仍在后台运行。
cbaldan

1

这里的其他解决方案有几个问题:

  • 如果日志记录过程已经关闭或在循环期间关闭,则它们将无限期运行
  • 编辑只应查看的日志
  • 不必要地写入其他文件
  • 不允许其他逻辑

这是我使用tomcat作为示例的想法(如果希望在启动日志时查看日志,请删除哈希值):

function startTomcat {
    loggingProcessStartCommand="${CATALINA_HOME}/bin/startup.sh"
    loggingProcessOwner="root"
    loggingProcessCommandLinePattern="${JAVA_HOME}"
    logSearchString="org.apache.catalina.startup.Catalina.start Server startup"
    logFile="${CATALINA_BASE}/log/catalina.out"

    lineNumber="$(( $(wc -l "${logFile}" | awk '{print $1}') + 1 ))"
    ${loggingProcessStartCommand}
    while [[ -z "$(sed -n "${lineNumber}p" "${logFile}" | grep "${logSearchString}")" ]]; do
        [[ -z "$(ps -ef | grep "^${loggingProcessOwner} .* ${loggingProcessCommandLinePattern}" | grep -v grep)" ]] && { echo "[ERROR] Tomcat failed to start"; return 1; }
        [[ $(wc -l "${logFile}" | awk '{print $1}') -lt ${lineNumber} ]] && continue
        #sed -n "${lineNumber}p" "${logFile}"
        let lineNumber++
    done
    #sed -n "${lineNumber}p" "${logFile}"
    echo "[INFO] Tomcat has started"
}

1

tail命令可以在后台运行,其pid会回显到grep子外壳。在grep子外壳中,EXIT上的陷阱处理程序可以终止该tail命令。

( (sleep 1; exec tail -f logfile.log) & echo $! ; wait ) | 
     (trap 'kill "$pid"' EXIT; pid="$(head -1)"; grep -m 1 "Server Started")

1

阅读所有。tldr:将grep的tail终止解耦。

最方便的两种形式是

( tail -f logfile.log & ) | grep -q "Server Started"

如果你有重击

grep -m 1 "Server Started" <(tail -f logfile.log)

但是,如果坐在后台的那条尾巴困扰着您,那么这里有一个比FIFO或其他任何答案更好的方法。需要重击。

coproc grep -m 1 "Server Started"
tail -F /tmp/x --pid $COPROC_PID >&${COPROC[1]}

或者,如果输出的不是尾巴,

coproc command that outputs
grep -m 1 "Sever Started" ${COPROC[0]}
kill $COPROC_PID

0

尝试使用inotify(inotifywait)

您可以为任何文件更改设置inotifywait,然后使用grep检查文件,如果找不到,则重新运行inotifywait,如果找到,退出循环...类似


这样,每次写入文件时都必须重新检查整个文件。不适用于日志文件。
grawity 2011年

1
另一种方法是制作两个脚本:1. tail -f logfile.log | grep -m 1“服务器已启动”> / tmp / found 2. firstscript.sh&MYPID = $!; inotifywait -e修改/ tmp / found; kill -KILL-$ MYPID
Evengard 2011年

我希望您编辑答案以显示捕获的PID,然后使用inotifywait-一个优雅的解决方案,对于以前使用grep的人来说很容易掌握,但需要更复杂的工具。
2011年

您要捕获什么的PID?如果您能进一步解释自己想要什么,我可以尝试解决
Evengard 2011年

0

您希望在写完该行后立即离开,但也想在超时后离开:

if (timeout 15s tail -F -n0 "stdout.log" &) | grep -q "The string that says the startup is successful" ; then
    echo "Application started with success."
else
    echo "Startup failed."
    tail stderr.log stdout.log
    exit 1
fi

-2

这个怎么样:

虽然是真的 如果[!-z $(grep“ myRegEx” myLog.log)]; 然后休息 fi; 做完了

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.