#!/bin/bash

set -eu
__SILENT=TRUE
SHELL=/bin/bash
PATH="/sbin:/bin:/usr/sbin:/usr/bin/:/usr/local/bin/:/usr/local/sbin/:$PATH"
MAILTO="${MAILTO:-root}"
HOME="${HOME:-/}"
. /opt/fox_utils/crab_sys.sh

# ВНИМАНИЕ! Перед исправлением багов. SLA crab_job:
#  - если проблему решает перезапуск демона (например, не раньше 10-20 раз в сутки)
#    то фиксить не обязательно
#  - если баг приводит к поломке 1 клиента из 100 (например, новый клиент VPS) - тоже норм

# echo "${JOB_COMMAND[@]}"
# TODO1 для поддержки многих пользователей
#   - chmod o+w /var/spool/crab_job/
#   - chmod +t /var/spool/crab_job/
#   - chmod 777 /tmp/crab_job_add.atomic.tmp
#   - ARG_QNAME="${ARG_QNAME:-root}" -> `id -un`
#   - поддержка прав доступа chmod 770 chown root:wheel и помещать нужных пользователей

usage(){
	echo 'Info:'
	echo 'Usage: crab_job run [job_params] "<cmd> cmd_params"
	or crab_job run [job_params] "<cmd> cmd_params" ";" "<cmd2> cmd2_params;"
	[--timeout=seconds] время дольше то kill
	[--qname=queue-name] имя очереди испoлнения
	[--qlen=20] колво задач в очереди
	[--qmax-run=1000] максимум параллельных задач=1000, для root=unlimited
	[--no-uniq] разрешить добавить в очередь одинаковые команды
	[--wait] долждаться выполнения stdout stderr на экран
	[--euid=0] выполнить под пользователем --euid требуется запуск демона
	[--prio=10] приоритет выполнения 10-низкий 0-выский
	[--sterr=] файл вывода ошибок stderr дозапись
	[--stdout=] файл вывода stdout дозапись
	[--log-formatted] добавлять дату и pid и тп в stdout stderr
	[--comment=] добавить коммент в задачу
	[--quiet] не выводить лишнего
	[--after=other_job] запускать только после выполнения задачи
crab_job list [--qname=<queue-name>] - список задач, +10 последних завершенных
crab_job state <id> - статус задачи
crab_job show <id> - id state errno qname uid euid seconds pid cmd cmd_params
crab_job kill <id> TODO - послать сигнал
crab_job tail [--follow|-f] [--lines=10] <id> - tail на stdout и stderr
crab_job cat <id> cat stdout, cat stderr
crab_job cat-stdout <id> stdout
crab_job cat-stderr <id> stderr
crab_job errno <id> - вывод кода возврата команды
crab_job remove <id> - удалить задачу из очереди
crab_job wait <id>
crab_job daemon - запустить демон
crab_job stat - вывести статистику
crab_job remove <id> TODO'
	echo 'Example: crab_job run cf vm restart'
	exit 0
	return 0
}

# Ищем отдельный аргумент --help
# if [ -z "${@:-}" ]; then
#	usage
# fi
for var in "$@"; do
	if [ "$var" = "--help" ]; then
		usage
		exit 0
	fi
done

sys::arg_parse "$@"

export PATH="/opt/fox_utils/:$PATH"

# используем переменные по аналогии с супервизором

__mkdir(){
	mkdir -p $@
	chmod 0777 $@
	return 0
}

CMD="$ARG_1"
ARG_QNAME="${ARG_QNAME:-root}"

QBASEDIR="/var/spool/crab_job"
CRAB_JOB_UNIQ="$QBASEDIR/uniq"

__mkdir "$QBASEDIR" "$QBASEDIR/added/" "$QBASEDIR/bad/" \
	"$QBASEDIR/running/" "$QBASEDIR/done/" "$QBASEDIR/waiting/" \
	"$QBASEDIR/log/" "$CRAB_JOB_UNIQ"

declare JOB_COMMAND JOB_PRIO JOB_TIMEOUT JOB_QLEN JOB_QMAX_RUN JOB_STDOUT JOB_STDERR JOB_LOG_FORMAT
declare JOB_WAIT JOB_EUID JOB_COMMENT JOB_NO_UNIQ JOB_QNAME JOB_PID JOB_COMMAND_MD5 JOB_AFTER

declare ARG_COMMAND ARG_PRIO ARG_TIMEOUT ARG_QLEN ARG_QMAX_RUN ARG_STDOUT ARG_STDERR ARG_LOG_FORMAT
declare ARG_WAIT ARG_EUID ARG_COMMENT ARG_NO_UNIQ ARG_QNAME ARG_PID ARG_COMMAND_MD5

declare JOB_QDIR_ADDED JOB_QDIR_WAITING JOB_QDIR_DONE JOB_QDIR_RUNNING JOB_DIR JOB_JOB_PID RANDOM

declare JOB_STATE JOB_UID JOB_START_TIME JOB_FINISH_TIME JOB_RUNNING_TIME JOB_ERRNO

declare CRAB_JOB_UNIQ
__job_lock(){
	local lock_file="$1" pid=
	for((i=0;i<10;i++)); do
		[ ! -f $lock_file ] && break
		read -r pid <"$lock_file" || true
		# Чтобы сработала защита с двумя строками, не делаем break
		[ ! -d /proc/$pid/ ] && rm -f "$lock_file"
		usleep 50000
	done
	echo $$ >> "$lock_file"
	read -r pid <"$lock_file" || true
	if [ "$pid" != "$$" ]; then
		# Коллизия, кто-то уже взял лок
		__job_lock "$@"
	fi
	return 0
}
__job_unlock(){
	rm -f "$1"
	return 0
}
prepare_arg() {
	if [ -z "${ARG_2:-}" ]; then
		echo "Укажите команду, которую нужно запустить" >&2
		exit 255
	fi

	# минимальная защита от eval
	JOB_COMMAND="${ARG_2}"
	# [ "$JOB_COMMAND" != "${ARG_2}" ] && { echo "LOG_ERROR unsupported char $JOB_COMMAND"; exit 255; }
	# trim space
	read -r JOB_COMMAND <<<"$JOB_COMMAND"
	JOB_PRIO=${ARG_PRIO:-9}
	JOB_TIMEOUT="${ARG_TIMEOUT:-0}"
	if [ "$JOB_TIMEOUT" != '0' ]; then
		read -r JOB_COMMAND <<< "timeout -s 15 ${JOB_TIMEOUT}s $JOB_COMMAND"
	fi
	JOB_QLEN="${ARG_QLEN:-1000}"
	JOB_QMAX_RUN="${ARG_QMAX_RUN:-1000}"
	JOB_STDOUT=
	JOB_STDERR=
	JOB_LOG_FORMAT="${ARG_LOG_FORMAT:-datetime}"
	JOB_WAIT="${ARG_WAIT:-FALSE}"
	JOB_UID="$(id -u)"
	JOB_EUID="${ARG_EUID:-$JOB_UID}"
	JOB_COMMENT="${ARG_COMMENT:-nocomment}"
	JOB_NO_UNIQ="${ARG_NO_UNIQ:-FALSE}"
	JOB_QNAME="$ARG_QNAME"
	JOB_PID="-"
	JOB_QDIR_ADDED="$QBASEDIR/added/$JOB_QNAME"
	JOB_QDIR_WAITING="$QBASEDIR/waiting/$JOB_QNAME"
	JOB_QDIR_RUNNING="$QBASEDIR/running/$JOB_QNAME"
	JOB_QDIR_DONE="$QBASEDIR/done/$JOB_QNAME"
	JOB_AFTER="${ARG_AFTER:--}"
	set -o pipefail
	JOB_COMMAND_MD5=$(echo "$JOB_COMMAND" | md5sum | cut -d' ' -f1)
	set +o pipefail
	return 0
}

job_set_state() {
	local new_state="$1"
	case "$new_state" in
	added)
		echo "-4" > "$JOB_DIR/errno"
		echo "added" > "$JOB_DIR/state"
		;;
	forked)
		[ "$JOB_STATE" != "added" ] && return 1
		echo "-3" > "$JOB_DIR/errno"
		__job_lock "/tmp/crab_job_update_${JOB_NAME}" $$
		echo "forked" > "$JOB_DIR/state"
		[ ! -d "$JOB_QDIR_WAITING" ] && __mkdir "$JOB_QDIR_WAITING"
		mv "$JOB_DIR" "$JOB_QDIR_WAITING"
		__job_unlock "/tmp/crab_job_update_${JOB_NAME}" $$
		get_job "$JOB_NAME"
		;;
	waiting)
		# не делаем mv, т.к. он сделан уже вызовом job_set_state forked
		[ "$JOB_STATE" != "forked" ] && return 1
		echo "-2" > "$JOB_DIR/errno"
		echo "waiting" > "$JOB_DIR/state"
		get_job "$JOB_NAME"
		;;
	running)
		[ "$JOB_STATE" != "waiting" ] && return 1
		echo "-1" > "$JOB_DIR/errno"
		echo "running" > "$JOB_DIR/state"
		[ ! -d "$JOB_QDIR_RUNNING" ] && __mkdir "$JOB_QDIR_RUNNING"
		__job_lock "/tmp/crab_job_update_${JOB_NAME}" $$
		mv "$JOB_DIR" "$JOB_QDIR_RUNNING"
		__job_unlock "/tmp/crab_job_update_${JOB_NAME}" $$
		get_job "$JOB_NAME"
		;;
	done)
		# в это состояние можно из любого [ "$JOB_STATE" != "running" ] && return 1
		echo "done" > "$JOB_DIR/state"
		[ ! -d "$JOB_QDIR_DONE" ] && __mkdir "$JOB_QDIR_DONE"
		__job_lock "/tmp/crab_job_update_${JOB_NAME}" $$
		mv "$JOB_DIR" "$JOB_QDIR_DONE"
		__job_unlock "/tmp/crab_job_update_${JOB_NAME}" $$
		get_job "$JOB_NAME"
		;;
	*)
		echo "Нельзя перемещать в added" >&2
		__job_unlock "/tmp/crab_job_update_${JOB_NAME}" $$
		return 0
		;;
	esac
	return 0
}

__get_job_dirs(){
	JOB_DIR="$( echo $QBASEDIR/*/*/$JOB_NAME )"
	# Иногда мы попадаем на mv и мы получаем /*/*/
	# (когда форк был прямо перед __sweeper_zombie)
	# Иногда мы получаем running и waiting, но когда доходим
	# до [ ! -d "$JOB_DIR" ] директории уже нет - хз как чинить
	if [[ "${JOB_DIR}" == *'/*/*/'* ]]; then
		echo "LOG_WARNING не смогли найти ${JOB_NAME}, пробуем еще раз"
		usleep 50000
		JOB_DIR="$( echo $QBASEDIR/*/*/$JOB_NAME )"
	fi
	JOB_DIR="${JOB_DIR%% *}"
	JOB_QNAME="${JOB_DIR#*crab_job/*/}"
	JOB_QNAME="${JOB_QNAME%%/*}"

	JOB_QDIR_ADDED="$QBASEDIR/added/$JOB_QNAME"
	JOB_QDIR_WAITING="$QBASEDIR/waiting/$JOB_QNAME"
	JOB_QDIR_RUNNING="$QBASEDIR/running/$JOB_QNAME"
	JOB_QDIR_DONE="$QBASEDIR/done/$JOB_QNAME"


	if [ ! -d "$JOB_DIR" ]; then
		# TODO если эта ошибка будет вылазить
		# в этом месте можно попробовать сделать usleep
		# и попробовать еще 1 раз вызвать __get_job_dirs
		# с защитой от бесконечной рекурсии
		echo "$JOB_DIR не существует"
		return 255
	fi

	# JOB_QDIR="${JOB_DIR%/*}"

	return 0
}

__get_job_env(){
	local curtime="$(date +%s)"
	JOB_PRIO="${JOB_NAME:0:1}"
	read -r JOB_QMAX_RUN < "$JOB_DIR/max_run"
	read -r JOB_EUID < "$JOB_DIR/euid"
	read -r JOB_COMMAND < "$JOB_DIR/command"

	# не трогайте read -r JOB_QNAME < "$JOB_DIR/qname"
	# state перезаписывается без блокировки, поэтому делаем повтор чтения
	if ! read -r JOB_STATE < "$JOB_DIR/state"; then
		echo "LOG_WARNING не смогли прочитать $JOB_DIR/state, пробуем еще раз"
		usleep 50000
		read -r JOB_STATE < "$JOB_DIR/state"
	fi
	read -r JOB_UID < "$JOB_DIR/uid"

	# TODO: проверять наличие файла
	read -r JOB_PID < "$JOB_DIR/pid" \
		|| JOB_PID="-"
	read -r JOB_JOB_PID < "$JOB_DIR/job_pid" \
		|| JOB_JOB_PID="-"
	read -r JOB_START_TIME < "$JOB_DIR/start_time" \
		|| JOB_START_TIME="-"
	read -r JOB_FINISH_TIME < "$JOB_DIR/finish_time" \
		|| JOB_FINISH_TIME="-"
	read -r JOB_ERRNO < "$JOB_DIR/errno" \
		|| JOB_ERRNO="-"
	read -r JOB_AFTER < "$JOB_DIR/after" \
		|| JOB_AFTER="-"
	[ -f "$JOB_DIR/command_md5" ] \
		&& read -r JOB_COMMAND_MD5 < "$JOB_DIR/command_md5" \
		|| JOB_COMMAND_MD5="-"
	if [ "$JOB_START_TIME" != "-" ]; then
		if [ "$JOB_FINISH_TIME" != "-" ]; then
			JOB_RUNNING_TIME="$(( JOB_FINISH_TIME - JOB_START_TIME ))"
		else
			JOB_RUNNING_TIME="$(( curtime - JOB_START_TIME ))"
		fi
	else
		JOB_RUNNING_TIME="-"
	fi
	return 0
}

get_job() {
	JOB_NAME="$1"
	if [ -z "${JOB_NAME:-}" ]; then
		echo "Укажите имя job" >&2
		exit 255
	fi

	__job_lock "/tmp/crab_job_update_${JOB_NAME}" $$

	__get_job_dirs
	__get_job_env

	__job_unlock "/tmp/crab_job_update_${JOB_NAME}" $$

	return 0
}

# работа с существующей задачей
# не трогайте [[ " run list daemon " != *" $CMD "* ]] && get_job "${ARG_2:-}"
__check_uniq() {
	local tmpqname tmpjob t
	if [ -f "$CRAB_JOB_UNIQ/$JOB_COMMAND_MD5" ]; then
		read -r tmpqname tmpjob t < "$CRAB_JOB_UNIQ/$JOB_COMMAND_MD5"
		if [ -d $QBASEDIR/added/$tmpqname/$tmpjob ]; then
			echo "LOG_WARNING $tmpjob exists уже добавлена" >&2
			echo "$tmpjob"
			exit 0
		elif [ -d $QBASEDIR/waiting/$tmpqname/$tmpjob ]; then
			echo "LOG_WARNING $tmpjob exists ожидает выполнения" >&2
			echo "$tmpjob"
			exit 0
		elif [ -d $QBASEDIR/running/$tmpqname/$tmpjob ]; then
			echo "LOG_WARNING $tmpjob exists уже выполняется" >&2
			echo "$tmpjob"
			exit 0
		else
			echo "LOG_WARNING $tmpjob повисшая задача. rm -f '$CRAB_JOB_UNIQ/$JOB_COMMAND_MD5'" >&2
			rm -f "$CRAB_JOB_UNIQ/$JOB_COMMAND_MD5"
			return 0
		fi
	fi
	return 0
}

__add(){
	local ms job_dir_not_new

	[ "$JOB_NO_UNIQ" = FALSE ] && __check_uniq

	for i in 1 2 3 4 5; do
		__job_lock "/tmp/crab_job_add.lock"
		ms="$( date +'%Y%m%d-%H%M%S-%N' )"
		ms="${JOB_PRIO}-${ms:0:19}"
		JOB_NAME="$ms"
		JOB_DIR="$JOB_QDIR_ADDED/$JOB_NAME.new"
		job_dir_not_new="$JOB_QDIR_ADDED/$JOB_NAME"
		if [ -d "$JOB_DIR" -o -d "$job_dir_not_new" ] \
			|| [ -d "$QBASEDIR/*/*/$JOB_NAME" ] \
			|| [ -d "$QBASEDIR/*/*/$JOB_NAME.new" ]; then
			# другая очередь уже заняла [ -d $QBASEDIR/*/*/$JOB_NAME ] ##
			# кто то уже занял вероятность крайне мала, но при старте пачки сразу возможна
			echo "LOG_WARNING crab_job уже занял $JOB_DIR" >&2
			usleep 50000;
			__job_unlock "/tmp/crab_job_add.lock"
			continue
		fi
		__mkdir "$JOB_DIR"
		__job_unlock "/tmp/crab_job_add.lock"
		touch "$JOB_DIR/errno"
		touch "$JOB_DIR/state"
		job_set_state "added"
		echo "$JOB_COMMAND" > "$JOB_DIR/command"
		echo "$JOB_COMMAND_MD5" > "$JOB_DIR/command_md5"
		echo "$JOB_QNAME" > "$JOB_DIR/qname"
		echo "$JOB_EUID" > "$JOB_DIR/euid"
		echo "$JOB_QMAX_RUN" > "$JOB_DIR/max_run"
		echo "$JOB_QNAME $JOB_NAME" > "$CRAB_JOB_UNIQ/$JOB_COMMAND_MD5"
		echo "$JOB_AFTER" > "$JOB_DIR/after"
		touch "$JOB_DIR/stdout.out"
		touch "$JOB_DIR/stderr.out"
		touch "$JOB_DIR/job_stderr.out"
		touch "$JOB_DIR/pid"
		touch "$JOB_DIR/job_pid"
		touch "$JOB_DIR/start_time"
		touch "$JOB_DIR/finish_time"
		touch "$JOB_DIR/start_time_fork"
		id -u > "$JOB_DIR/uid"

		chmod -R u+rwX,g+rwX,o+rwX "$JOB_DIR"
		chmod u+rwX,g+rwX,o+rwX "$CRAB_JOB_UNIQ/$JOB_COMMAND_MD5" || true
		mv "$JOB_DIR" "$job_dir_not_new"

		# перечитаем для уверенности
		get_job "$JOB_NAME"
		__mkdir "$JOB_QDIR_ADDED" "$JOB_QDIR_DONE" "$JOB_QDIR_RUNNING" "$JOB_QDIR_WAITING"
		[ "${ARG_QUIET:-}" != TRUE ] && __show "$JOB_NAME"
		break
	done
	# skip strongbash010
	touch /tmp/__daemon_process_queue_cont
	return 0
}

__fork_job(){
	get_job "$1"

	local stdin='/dev/null'
	# при некоторых вредных командах надо иметь tty:
	# stdin=/dev/tty16, но ситуация потеряна, поэтому баг вернем чтоб поймать
	# cas-like атомартный запуск
	# state=?added, lock, state=?added, state->forked; unlock; run
	# __job_lock замедляет fork до 100 задач/сек (было 2000/сек)
	job_set_state "forked"
	# usleep 200000
	date +%s > "$JOB_DIR/start_time_fork"

	setsid crab_job __do_job "$JOB_NAME" --quiet \
		"$JOB_DIR/stdout.out" &>>"$JOB_DIR/job_stderr.out" <"$stdin" & disown $!
	return 0
}
__set_done(){
	date +%s > "$JOB_DIR/finish_time"
	echo "$1" >"$JOB_DIR/errno"
	job_set_state "done"
	rm -f "$CRAB_JOB_UNIQ/$JOB_COMMAND_MD5"
	get_job "$JOB_NAME"
	return 0
}
__do_job(){
	local msg ret run_count first_job wait_errno
	get_job "$1"
	if [ "$JOB_JOB_PID" = "-" ]; then
		if [ "$JOB_STATE" != "forked" ]; then
			echo "Задачу запустил другой обработчик" >&2
			exit 0
		fi
		# job_pid это pid враппера
		echo "$$" > "$JOB_DIR/job_pid"
		job_set_state "waiting"
	else
		echo "LOG_WARNING по какой-то причине job_process $JOB_DIR умер. Разрешаем 2-й раз"
		if [ ! -d "/proc/$JOB_JOB_PID" ]; then
			echo "$$" > "$JOB_DIR/job_pid"
		fi
	fi
	get_job "$JOB_NAME"
	__wait_server_state
	# TODO: добавить ожидание приоритетов (первая часть имени JOB_NAME)
	# приоритеты уже работают при max_run=1 за счет ls -1
	if [ "${JOB_AFTER}" != - ]; then
		echo "LOG_DEBUG задача ${JOB_DIR} ожидаю after=$JOB_AFTER" >&2
		( __wait "${JOB_AFTER}" )
		wait_errno=$( __errno "${JOB_AFTER}" )
		if [ "$wait_errno" != 0 ]; then
			echo "LOG_ERROR ошибка в after задаче $JOB_AFTER errno=$wait_errno" \
				>>"$JOB_DIR/job_stderr.out"
			__set_done "$wait_errno"
			return 0
		fi
	fi
	if [ "$JOB_QMAX_RUN" = 1 ]; then
		for ((i=0; i<3600*24; i++)); do
			first_job="$(__queue_first_job  "$JOB_QDIR_WAITING")"
			[ "$JOB_NAME" == "$first_job" ] && break
			msg="LOG_DEBUG задача ${JOB_DIR} qmax-run=1, ожидаю first_job=$first_job"
			echo "$msg" >>"$JOB_DIR/job_stderr.out"
			sleep 1
		done
	fi

	if [ "$JOB_QMAX_RUN" -gt 0 ]; then
		for ((i=0; i<3600*24; i++)); do
			__wait_server_state
			run_count=$(__queue_job_count "$JOB_QDIR_RUNNING")
			if [ "$run_count" -ge "$JOB_QMAX_RUN" ]; then
				msg="LOG_DEBUG задача ${JOB_DIR} ожидает очереди(cur $run_count, max $JOB_QMAX_RUN)"
				echo "$msg" >>"$JOB_DIR/job_stderr.out"
				sleep 1
				continue
			fi
			break
		done
	fi
	job_set_state "running"
	get_job "$JOB_NAME"

	local command="$(<${JOB_DIR}/command)"
	date +%s > "$JOB_DIR/start_time"
	# зачем здесь был eval $command 1>"$JOB_DIR/stdout.out" 2>"$JOB_DIR/stderr.out" &
	bash -ec "$command" 1>"$JOB_DIR/stdout.out" 2>"$JOB_DIR/stderr.out" &
	echo $! > "$JOB_DIR/pid"
	wait $! && ret=0 || ret=$?
	__set_done "$ret"
	return 0
}
__show(){
	local after=
	get_job "$1"
	[ "$JOB_AFTER" != '-' ] && after="--after=$JOB_AFTER"
	echo -n "$JOB_NAME $JOB_STATE $JOB_ERRNO $JOB_QNAME $JOB_UID $JOB_EUID"
	echo " $JOB_RUNNING_TIME $JOB_PID $JOB_COMMAND" "$after"
	return 0
}
__list(){
	local q line n
	# echo "$QBASEDIR"
	find $QBASEDIR/ -mindepth 2 -maxdepth 3 -type d | grep '/[0-9]-' \
		| awk -F '/' '{print $NF}' | sort -t '-' -k2,4 | tail -n 100 > /tmp/crab_job.list.$$
#	find $QBASEDIR/waiting/ -mindepth 2 -maxdepth 3 -type d -printf "%T+\t%p\n" | sort \
#		>> /tmp/crab_job.list.$$
#	find $QBASEDIR/running/ -mindepth 2 -maxdepth 3 -type d -printf "%T+\t%p\n" | sort \
#		>> /tmp/crab_job.list.$$
#	find $QBASEDIR/done/ -mindepth 2 -maxdepth 3 -type d -printf "%T+\t%p\n" | sort | tail -n 20 \
#		>> /tmp/crab_job.list.$$
#	find $QBASEDIR/ -mindepth 2 -maxdepth 3 -type d |grep '[0-9]-' \
#		| tail -n 100  >/tmp/crab_job.list.$$
	while read -r line; do
		__show "${line}" || true
	done < /tmp/crab_job.list.$$
	rm -f /tmp/crab_job.list.$$
	return 0
}
__cat(){
	get_job "$1"
	[ "$CMD" != "cat-stderr" ] && cat "$JOB_DIR/stdout.out"
	[ "$CMD" != "cat-stdout" ] && cat "$JOB_DIR/stderr.out"
	[ "$CMD" != "cat-stderr" -a "$CMD" != "cat-stdout" ] && cat "$JOB_DIR/job_stderr.out"
	return 0
}
__tail(){
	local i
	local pid_str
	JOB_NAME="$1"
	while true; do
		get_job "$JOB_NAME"

		[ "$JOB_PID" != "-" ] && break;
		[ "$JOB_STATE" = "done" ] && break;
		if [ "${ARG_QUIET:-}" != TRUE ]; then
			__show "$JOB_NAME"
			tail -n 1 "$JOB_DIR/job_stderr.out"
		fi
		for i in 1 2 3 4 5 6 7 8 9 10; do
			sleep 0.1
			get_job "$JOB_NAME"
			[ "$JOB_PID" != "-" ] && break
		done
	done
	# TODO: Проверять, что pid существует
	[ "${ARG_QUIET:-}" != TRUE ] && __show "$JOB_NAME"
	pid_str="-f --pid=$JOB_PID"
	if [ ! -d "/proc/${JOB_PID:-FALSE}/" ]; then
		pid_str=''
		# JOD_DIR может переехать running->done
		get_job "$JOB_NAME"
	fi
	tail -n 200 -s 0.1 -q ${pid_str} "$JOB_DIR/job_stderr.out" \
		"$JOB_DIR/stdout.out" "$JOB_DIR/stderr.out"
	# иногда tail завершался раньше чем устанавливался статус done
	# и можно было получить неверный errno
	while true; do
		get_job "$JOB_NAME"
		[ "$JOB_STATE" = "done" ] && break;
		sleep 0.1
	done
	[ "${ARG_QUIET:-}" != TRUE ] && __show "$JOB_NAME"
	return 0
}
__wait(){
	get_job "$1"
	while true; do
		get_job "$JOB_NAME"
		[ "$JOB_PID" != "-" ] && break;
		[ "$JOB_STATE" = "done" ] && return 0;
		usleep 100000
	done
	tail -qfs 0.2 --pid="$JOB_PID" "$JOB_DIR/stdout.out" "$JOB_DIR/stderr.out" &>/dev/null
	return 0
}
__errno(){
	set -e
	get_job "$1"
	echo "$JOB_ERRNO"
	return 0
}
__state(){
	get_job "$1"
	echo "$JOB_STATE"
	return 0
}

__queue_job_count() {
	# Выводим количество запущенных задач в очереди
	set -e
	local count
	set -o pipefail
	count="$(ls -1 $1 | wc -l)"
	set +o pipefail
	echo "$count"
	return 0
}

__queue_first_job() {
	# Выводит первую задачу в очереди $1
	set -e
	local first_job
	set -o pipefail
	first_job="$(ls -1 $1 | head -n 1)"
	set +o pipefail
	echo "$first_job"
	return 0
}

__wait_server_state() {
	set -e

	local state=FALSE cur_state=FALSE t

	while true; do
		[ ! -f "/STATE" -a ! -f "/CUR_STATE" ] && break
		[ -f "/STATE" ] && read -r state t <"/STATE"
		[ -f "/CUR_STATE" ] && read -r cur_state t <"/CUR_STATE"

		[ "$state" = "running" -a "$cur_state" = "running" ] && break
		echo "__wait_server_state /STATE=$state /CUR_STATE=$cur_state" >&2
		sleep 1
	done

	return 0
}

__daemon_process_queue() {
	# Обработка очереди задач
	local queue="$1"
	local qdir="${QBASEDIR}/added/${queue}"
	local qjob ret
	local oneslot=10
	# todo: кто чистит "зависшие" таски?
	#   - старый state(не added), но нет пидфайла
	#   - пидфайл не актуален
	for qjob in $(ls -1 "$qdir"|head -n $oneslot); do
		[[ "$qjob" = *.new ]] && continue
		[ -n "${qjob//[0-9-]/}" ] && continue
		__wait_server_state
		( __fork_job "$qjob" ) && ret=0 || ret=$?
		# skip strongbash010
		touch /tmp/__daemon_process_queue_cont
		if [ "$ret" = "0" ]; then
			echo "Форкнул задачу $qdir $qjob"
		else
			__mkdir "${QBASEDIR}/bad/${queue}/"
			echo "Не получилось сделать форк $qdir $qjob. Перемещаю в bad"
			[ -d "${QBASEDIR}/added/${queue}/$qjob" ] \
				&& mv "${QBASEDIR}/added/${queue}/$qjob" "${QBASEDIR}/bad/${queue}/$qjob"
		fi
	done
	return 0
}


__sweeper_archive_job(){
	local d job d_relative exemptions
	job="$1"
	d="$2"
	d_relative="${d##*done/}"
	d_relative="${d_relative///}"
	exemptions=( "vm_--json" "node_--json" )
	if [[ "${exemptions[*]}" =~ $d_relative ]] \
		&& grep --exclude="stdout.out" -rH "" "$d/$job" >> "$QBASEDIR/log/done.log"; then
		# Исключения, которые пишут очень много stdout
		echo "$d_relative в списке исключений, пропускаем stdout" >> "$QBASEDIR/log/done.log"
		echo "LOG_DEBUG __sweeper удаляю $d/$job"
		rm -rf "$d/$job"
	elif [[ "${exemptions[*]}" =~ $d_relative ]]; then
		# Исключение, упавшее на grep
		echo "LOG_WARNING __sweeper перемещаю плохой $d/$job" >&2
		__mkdir "${d/done/bad}"
		mv "$d/$job" "${d/done/bad}"
	elif grep -rH "" "$d/$job" >> "$QBASEDIR/log/done.log"; then
		# Все прочие команды
		echo "LOG_DEBUG __sweeper удаляю $d/$job"
		rm -rf "$d/$job"
	else
		# Все прочие, если упал grep
		echo "LOG_WARNING __sweeper перемещаю плохой $d/$job" >&2
		__mkdir "${d/done/bad}"
		mv "$d/$job" "${d/done/bad}"
	fi
	return 0
}


# TODO: чистить после __do_job но с учетом прав на каталоги
__sweeper() {
	local start_time="$(date +%s)"
	local d jobs_for_deletion job
	[ "$(( $RANDOM % 100 ))" != 0 ] && return 0
	# чистим большие очереди, где больше 1000 джобов
	for d in $QBASEDIR/done/*/; do
		# оставляем только 1000 самых свежих
		jobs_for_deletion="$(ls -1t "$d" | tail -n +1001)"
		if [ ! -z "${jobs_for_deletion}" ]; then
			echo "LOG_INFO __sweeper Удаляю старые файлы из $d"
		fi
		for job in $jobs_for_deletion; do
			__sweeper_archive_job $job $d
		done
	done

	# Удаляем очереди, которыми не пользовались неделю
	# Делаем по одной очереди, чтобы снизить вероятность проблем многопоточности
	find /var/spool/crab_job/done/ -maxdepth 1 -mindepth 1 -type d -mtime +7 | head -n 1 \
		| while read -r d; do
			if ! echo "$d" | grep -E -q "^/var/spool/crab_job/done/[^/]+$"; then
				echo "LOG_ERROR Пытался удалить не очередь! Кривой find. $d"
				exit 1
			fi
			echo "LOG_INFO __sweeper Удаляю старую очередь ${d}"
			jobs_for_deletion="$(ls -1t "$d")"
			for job in $jobs_for_deletion; do
				__sweeper_archive_job $job $d
			done
			rm -rf "${d}"
		done

	# Удаляем пустые очереди, которыми не пользовались неделю
	# Делаем по одной очереди, чтобы снизить вероятность проблем многопоточности
	find /var/spool/crab_job/{added,running,waiting}/ \
		-maxdepth 1 -mindepth 1 -type d -empty -mtime +7 \
		| head -n 1 | while read -r d; do
			if ! echo "$d" | grep -E -q "^/var/spool/crab_job/[a-z]+/[^/]+$"; then
				echo "LOG_ERROR Пытался удалить не очередь! Кривой find. $d"
				# exit 1
			fi
			echo "LOG_INFO __sweeper Удаляю старую пустую очередь ${d}"
			rmdir "${d}"
		done
	local end_time end_time_human
	end_time="$(date +%s)"
	end_time_human="$(date -d @$end_time '+%Y-%m-%d %H:%M:%S')"
	echo "$end_time_human" \
		"LOG_INFO __sweeper finish. Time: $((end_time-start_time))"
	return 0
}

__sweeper_zombie() {
	local start_time="$(date +%s)"
	local d job_list job_name
	# Защита find от пустых каталогов (! -empty) _значительно_ быстрее обхода этих каталогов башем.
	# `find | while` вместо `for i in $(find)` чтобы bash не треснул при большом количестве
	# виртуалок из-за буферизации stdout сабшелла в аргументы. [Не проверено].
	# Хочется засунуть выхлоп find в файл, но не хочу завязывать супервизор на свободное дисковое
	# пространство ещё в одном месте. В переменную тоже страшно из-за \n при пустом выхлопе, см:
	# x="$(true)"; set -x; while read -r var; do echo $var; done <<< "$x"; set +x
	find $QBASEDIR/{waiting,running}/ -maxdepth 1 -mindepth 1 -type d  ! -empty \
		| while read -r d; do
			# echo "LOG_INFO __sweeper_zombie ищу зомби-задачи из $d"
			(
				cd "${d}"
				# Не используем ls -1t, т.к. падает когда файл мувают во время работы ls
				job_list="$(ls -1 | sort)"
				for job_name in ${job_list}; do
					get_job "$job_name"
					[ -d "/proc/${JOB_JOB_PID}" ] && continue
					get_job "$job_name"
					# TODO передалть на контроль времени вместо state=forked
					[ "$JOB_STATE" = "forked" ] && continue
					[ "$JOB_STATE" = "done" ] && continue
					echo "$job_name - зомби. Переносим в done. JOB_JOB_PID=${JOB_JOB_PID} JOB_STATE=${JOB_STATE}"
					__set_done 254
				done
			)
		done
	local end_time end_time_human
	end_time="$(date +%s)"
	end_time_human="$(date -d @$end_time '+%Y-%m-%d %H:%M:%S')"
	echo "$end_time_human" \
		"LOG_INFO __sweeper_zombie finish. Time: $((end_time-start_time))"
	return 0
}

__daemon(){
	local i
	echo "LOG_INFO Daemon started"
	local queue src dst
	touch /tmp/__daemon_process_queue_cont
	# муваем старые варианты очередей
	for src in $QBASEDIR/*; do
		[[ " added done done.log log running uniq waiting bad " = *" ${src##*/} "* ]] && continue
		dst="${QBASEDIR/crab_job/crab_job_bk.$$}"
		__mkdir "$dst"
		echo "LOG_INFO Муваю неизвестный файл $src в $dst "
		mv  "$src" "$dst"
	done

	while true; do
		__wait_server_state
		# skip strongbash010
		rm -f /tmp/__daemon_process_queue_cont
		for queue in $(ls "${QBASEDIR}/added/"); do
			__daemon_process_queue "$queue" &
		done
		wait
		# команды __sweeper могут падать
		if ! crab_job __run_sweeper; then
			echo "LOG_WARNING __sweeper упал. Если падает чаще 1 раза в час - проблема."
		fi
		# skip strongbash010
		for ((i=0;i<50;i++)); do
			[ -f /tmp/__daemon_process_queue_cont ] && break
			sleep 0.1
		done
		if [ -f "/var/run/crab_job_daemon.stop" ]; then
			echo "LOG_INFO Обнаружен /var/run/crab_job_daemon.stop. останавливаемся"
			# Дожидаемся завершения всех команд и выходим
			# TODO1
			break
		fi
	done
	echo "LOG_INFO Daemon stopped"
	return 0
}

__stat() {
	local f f_short num_jobs pid

	if service crab_job_daemon status; then
		: # демон не работает
	fi

	echo

	for f in $QBASEDIR/*; do
		[ ! -d "$f" ] && continue
		f_short="${f##*/}"
		[ "$f_short" == "log" ] && continue
		if [ "$f_short" = "uniq" ]; then
			num_jobs="$(ls -1 "$f" | wc -l)"
		else
			num_jobs="$(find "$f" -mindepth 2 -maxdepth 2 -type d | wc -l)"
		fi
		printf "%7s %5s\n" "$f_short" "$num_jobs"
	done
	return 0
}

case "$CMD" in
run)
	prepare_arg
	__add
	change_euid=FALSE
	# Если uid = euid - запускаем сразу, иначе запускать должен демон
	if [ "$JOB_UID" != "$JOB_EUID" ]; then
		change_euid=TRUE
	fi
	if [ -s /var/run/crab_job_daemon.pid ]; then
		# если демон запущен, мы ничего не делаем. он сделает сам
		:
	else
		if [ "$(id -u)" = 0 -o "$change_euid" = FALSE ]; then
			__fork_job "${JOB_NAME}" "$@"
			__sweeper
		else
			echo "LOG_ERROR Нельзя менять euid если вы не root и демон не запущен"
			echo "LOG_ERROR Запустите демон service crab_job_daemon start"
			exit 255
		fi
	fi

	if [ "${ARG_WAIT:-}" = TRUE ]; then
		sleep 0.05
		__tail "$JOB_NAME"
	fi

	;;
__do_job)
	__do_job "$ARG_2" "$@"
	;;
__fork_job)
	__fork_job "$ARG_2" "$@"
	;;
__run_sweeper)
	__sweeper
	__sweeper_zombie
	;;
list)
	__list "$@"
	;;
cat|cat-stdout|cat-stderr)
	__cat "$ARG_2"
	;;
tail)
	__tail "$ARG_2"
	;;
wait)
	__wait "$ARG_2"
	;;
show)
	__show "$ARG_2"
	;;
errno)
	__errno "$ARG_2"
	;;
state)
	__state "$ARG_2"
	;;
daemon)
	if [ "$(id -un)" != "root" ]; then
		echo "LOG_ERROR демон можно запускать только от root"
		exit 255
	fi
	# Нужное окружение, которого может не быть
	export USER=root LOGNAME=root HOME=/root
	__daemon
	;;
stat)
	__stat
	;;
*)
	# "__${1}" ${@:2}
	# exit
	echo "$CMD not supported"
	;;
esac
exit 0
# TODO3:
#  - vzctl без TTY не работает, нужна эмуляция TTY или mknod TTY
#  - нулевой дескриптор связать с TTY или с /dev/null
# logger formated date
# DNS_NAME="$ARG_1"
exit 0
