You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1011 lines
20KB

  1. #!/bin/bash
  2. set -euo pipefail
  3. test -z "${B_DIR:-}" && export B_DIR="$HOME/.beacon"
  4. export B_REPOS_DIR="$B_DIR/repos"
  5. export B_INDEX_DIR="$B_DIR/index"
  6. export B_STAGING_DIR="$B_DIR/staging"
  7. export B_HOOK_DIR="$HOME/.config/beacon/hooks"
  8. export B_SVC_DIR="$HOME/.config/beacon/svc"
  9. export B_CP_OPTS=(--no-dereference "--preserve=mode,links" --no-target-directory --remove-destination)
  10. export B_RSYNC_OPTS=(--quiet --recursive --links --perms --executability --exclude=.git/ --exclude=.gitignore --exclude=.beacon/)
  11. export B_DIFF_OPTS=(-r --no-dereference --unidirectional-new-file --unified=10)
  12. if diff --color /dev/null /dev/null >/dev/null 2>&1; then
  13. B_DIFF_OPTS+=(--color)
  14. fi
  15. test -f "$HOME/.beaconrc" && source "$HOME/.beaconrc"
  16. export B_LOG_DIR="$B_DIR/log"
  17. export B_NEEDED_DIR="$B_STAGING_DIR/needed"
  18. export B_PROVIDED_DIR="$B_STAGING_DIR/provided"
  19. export B_RUN_DIR="$B_DIR/run"
  20. if tput sgr0 2>/dev/null; then
  21. red="$(tput setaf 1)" ; export red
  22. green="$(tput setaf 2)" ; export green
  23. yellow="$(tput setaf 3)"; export yellow
  24. blue="$(tput setaf 4)" ; export blue
  25. cyan="$(tput setaf 6)" ; export cyan
  26. reset="$(tput sgr0)" ; export reset
  27. defwidth="$(tput cols)" ; export defwidth
  28. else
  29. export red=""
  30. export green=""
  31. export yellow=""
  32. export blue=""
  33. export cyan=""
  34. export reset=""
  35. export defwidth="80"
  36. fi
  37. export lvl
  38. test -z "${lvl:-}" && lvl=0
  39. function prefix {
  40. for i in $(seq 1 "$lvl"); do
  41. echo -n " "
  42. done
  43. }
  44. export -f prefix
  45. function status {
  46. (
  47. prefix
  48. [ -n "${1:-}" ] && echo -n "$blue$1$reset "
  49. shift
  50. echo "$@" | sed "2,$ s:^: $(prefix):g"
  51. ) | xargs -d '\n' echo
  52. }
  53. export -f status
  54. function sbegin {
  55. prefix
  56. [ -n "${1:-}" ] && echo -n "$blue$1$reset "
  57. shift
  58. echo -n "$@" "..."
  59. }
  60. export -f sbegin
  61. function send {
  62. local width; width="$(tput cols)" || width="$defwidth"
  63. local pos; pos=$((width-8))
  64. echo -ne "\033[999D\033[${pos}C"
  65. if [ "$1" -eq 0 ]; then
  66. echo "[ ${green}OK$reset ]"
  67. else
  68. echo "[ ${red}ERR$reset ]"
  69. fi
  70. }
  71. export -f send
  72. function sidle {
  73. local width; width="$(tput cols)" || width="$defwidth"
  74. local pos; pos=$((width-8))
  75. echo -ne "\033[999D\033[${pos}C"
  76. if [ "$1" -eq 0 ]; then
  77. echo "[ $blue-$reset ]"
  78. else
  79. echo "[ ${red}ERR$reset ]"
  80. fi
  81. }
  82. export -f sidle
  83. function send_ {
  84. local width; width="$(tput cols)" || width="$defwidth"
  85. local pos; pos=$((width-8))
  86. echo -ne "\033[999D\033[${pos}C"
  87. case "$1" in
  88. 0) echo "[ ${green}OK$reset ]" ;;
  89. 202) echo "[ $blue-$reset ]" ;;
  90. *) echo "[ ${red}ERR$reset ]" ;;
  91. esac
  92. }
  93. export -f send_
  94. function swait {
  95. local n="$1"
  96. shift
  97. if [ "$1" == "!" ]; then
  98. local inv="1"
  99. shift
  100. fi
  101. while [ "$n" -gt 0 ]; do
  102. if "$@" >/dev/null 2>&1; then
  103. test -n "${inv:-}" || break
  104. else
  105. test -z "${inv:-}" || break
  106. fi
  107. local width; width="$(tput cols)" || width="$defwidth"
  108. local pos; pos=$((width-8))
  109. echo -ne "\033[999D\033[${pos}C"
  110. printf "[ $cyan%2d$reset ]" "$n"
  111. sleep 1
  112. n=$((n-1))
  113. done
  114. set +e
  115. "$@" >/dev/null 2>&1
  116. local ret="$?"
  117. set -e
  118. if [ -n "${inv:-}" ]; then
  119. if [ "$ret" -eq 0 ]; then
  120. send 1
  121. return 1
  122. else
  123. send 0
  124. return 0
  125. fi
  126. else
  127. send "$ret"
  128. return "$ret"
  129. fi
  130. }
  131. export -f swait
  132. function swarn {
  133. echo
  134. prefix
  135. echo -n "$yellow$1$reset: "
  136. shift
  137. echo "$@" | sed "2,$ s:^: $(prefix):g"
  138. echo
  139. }
  140. export -f swarn
  141. function serr {
  142. echo
  143. prefix
  144. echo -n "$red$1$reset: "
  145. shift
  146. echo "$@" | sed "2,$ s:^: $(prefix):g"
  147. echo
  148. }
  149. export -f serr
  150. function sin {
  151. lvl=$((lvl + 1))
  152. }
  153. export -f sin
  154. function sout {
  155. lvl=$((lvl - 1))
  156. }
  157. export -f sout
  158. # setup
  159. # Make sure the beacon environment is correctly set up.
  160. function setup {
  161. mkdir -p \
  162. "$B_DIR" \
  163. "$B_REPOS_DIR" \
  164. "$B_INDEX_DIR" \
  165. "$B_STAGING_DIR" \
  166. "$B_NEEDED_DIR" \
  167. "$B_PROVIDED_DIR" \
  168. "$B_HOOK_DIR" \
  169. "$B_SVC_DIR" \
  170. "$B_LOG_DIR" \
  171. "$B_RUN_DIR"
  172. chmod ugo+rwxt "$B_INDEX_DIR" 2>/dev/null || true
  173. return 0
  174. }
  175. # update_ repo
  176. # Update the specified repo.
  177. function update_() (
  178. local src="$B_GIT_PREFIX$1$B_GIT_SUFFIX"
  179. local dst="$1"
  180. cd "$B_REPOS_DIR"
  181. if [ -f "$dst/.beacon/visited" ]; then
  182. return 0
  183. fi
  184. sbegin Repo "$dst"
  185. if [ -d "$dst" ]; then
  186. (
  187. cd "$dst"
  188. local before; before="$(git log -n 1 --pretty=format:%H 2>/dev/null || true)"
  189. git pull -q
  190. local e="$?"
  191. local after; after="$(git log -n 1 --pretty=format:%H 2>/dev/null || true)"
  192. if [ "$before" != "$after" ]; then
  193. send "$e"
  194. else
  195. sidle "$e"
  196. fi
  197. )
  198. else
  199. git clone -q "$src" "$dst"
  200. send $?
  201. fi
  202. # Fetch script
  203. if [ -x "$dst/.beacon/fetch" ]; then
  204. (
  205. cd "$dst"
  206. sin
  207. sbegin "Fetching"
  208. if ! .beacon/fetch >"$B_LOG_DIR/$dst.fetch.log" 2>&1; then
  209. send 1
  210. serr "Fetch failure" "$1"
  211. exit 1
  212. else
  213. send 0
  214. fi
  215. sout
  216. )
  217. fi
  218. grep -F -q "visited" "$dst/.gitignore" 2>/dev/null || echo "visited" >> "$dst/.gitignore"
  219. touch "$dst/.beacon/visited"
  220. sin
  221. if [ -f "$dst/.beacon/depend" ]; then
  222. while read -r dep; do
  223. if [ -n "${dep:-}" ]; then
  224. update_ "$dep"
  225. fi
  226. done < "$dst/.beacon/depend"
  227. fi
  228. sout
  229. )
  230. # update
  231. # Update all repos from $B_DIR/sources
  232. function update {
  233. status "Update"
  234. sin
  235. rm "$B_REPOS_DIR/"*"/.beacon/visited" 2>/dev/null || true
  236. update_ "$rootpkg"
  237. for theme in "${themes[@]}"; do
  238. update_ "$theme"
  239. done
  240. rm "$B_REPOS_DIR/"*"/.beacon/visited" 2>/dev/null || true
  241. sout
  242. }
  243. # clear
  244. # Clear the staging area and sed directory
  245. function clear {
  246. rm -rf "$B_STAGING_DIR" 2>/dev/null || true
  247. mkdir -p "$B_STAGING_DIR" "$B_NEEDED_DIR" "$B_PROVIDED_DIR"
  248. rm "$B_DIR/sedfile"{,.pre,.post} 2>/dev/null || true
  249. touch "$B_DIR/sedfile"{,.pre,.post}
  250. rm "$B_REPOS_DIR/"*"/.beacon/visited" 2>/dev/null || true
  251. }
  252. # stage <reponame> ...
  253. # Add the repo to the staging area according to the rules in `$1/.beacon/index`.
  254. # Append sed rules from `$1/.beacon/sed`
  255. # Touch needed / provided files.
  256. function stage() (
  257. while [ -n "${1:-}" ]; do
  258. # Basic sanity checks
  259. if [ ! -d "$B_REPOS_DIR/$1" ]; then
  260. serr "Staging error" "repo $1 not found."
  261. return 1
  262. elif [ ! -d "$B_REPOS_DIR/$1/.beacon" ]; then
  263. serr "Staging error" "repo $1 does not contain a .beacon folder."
  264. return 1
  265. elif [ ! -f "$B_REPOS_DIR/$1/.beacon/index" ]; then
  266. swarn "No index file" "$1"
  267. shift
  268. continue
  269. elif [ -f "$B_REPOS_DIR/$1/.beacon/visited" ]; then
  270. shift
  271. continue
  272. fi
  273. status "Staging" "$1"
  274. sin
  275. cd "$B_REPOS_DIR/$1"
  276. # Sed rules
  277. for s in .pre "" .post; do
  278. touch "$B_DIR/sedfile$s"
  279. if [ -f ".beacon/sed$s" ]; then
  280. sbegin "Checking" ".beacon/sed$s"
  281. set +e
  282. local err; err="$(sed -f ".beacon/sed$s" /dev/null 2>&1)"
  283. if [ "$?" -ne "0" ]; then
  284. set -e
  285. send 1
  286. serr "Sed error" "$err"
  287. exit 1
  288. else
  289. set -e
  290. echo -e "\n\n#=-=# Repo $1 #=-=#" >>"$B_DIR/sedfile$s"
  291. cat ".beacon/sed$s" >> "$B_DIR/sedfile$s" 2>/dev/null || true
  292. send 0
  293. fi
  294. fi
  295. done
  296. # Recursion handling
  297. touch ".beacon/visited"
  298. if [ -f ".beacon/depend" ]; then
  299. stagelist ".beacon/depend"
  300. fi
  301. cd "$B_REPOS_DIR/$1"
  302. # Build script
  303. if [ -x ".beacon/build" ]; then
  304. sbegin "Building"
  305. if .beacon/build >"$B_LOG_DIR/$1.build.log" 2>&1; then
  306. send 0
  307. else
  308. send 1
  309. serr "Build failure" "$1"
  310. exit 1
  311. fi
  312. fi
  313. # File staging
  314. while read -r line; do
  315. if [ "$line" != "" ]; then
  316. local mode; mode="$(cut -f 1 - <<< "$line")"
  317. local src; src="$(cut -f 2 - <<< "$line")"
  318. local destprefix; destprefix="$(cut -f 3 - <<< "$line")"
  319. case "$mode" in
  320. "@") dest="$B_STAGING_DIR/prim" ;;
  321. "$") dest="$B_STAGING_DIR/home" ;;
  322. "/") dest="$B_STAGING_DIR/root" ;;
  323. *)
  324. serr "Staging error in repo $1" "invalid mode: $mode ."
  325. return 1
  326. ;;
  327. esac
  328. if [ -n "${destprefix:-}" ]; then
  329. dest="$dest/$destprefix"
  330. fi
  331. mkdir -p "$dest"
  332. sbegin "rsync" "$src -> $dest"
  333. rsync "${B_RSYNC_OPTS[@]}" "$src/" "$dest" >/dev/null 2>&1
  334. send $?
  335. fi
  336. done < ".beacon/index"
  337. # Need / Provide
  338. if [ -f ".beacon/need" ]; then
  339. while read -r n; do
  340. if [ -n "${n:-}" ]; then
  341. echo "$1" >> "$B_NEEDED_DIR/$n"
  342. fi
  343. done < ".beacon/need"
  344. fi
  345. if [ -f ".beacon/provide" ]; then
  346. while read -r p; do
  347. if [ -n "${p:-}" ]; then
  348. echo "$1" >> "$B_PROVIDED_DIR/$p"
  349. fi
  350. done < ".beacon/provide"
  351. fi
  352. sout
  353. shift
  354. done
  355. )
  356. # dosed
  357. # Apply sed rules from all staged repos
  358. function dosed {
  359. status "Checking" "needs"
  360. sin
  361. # Needs checking
  362. for f in "$B_NEEDED_DIR/"*; do
  363. if [ -f "$f" ]; then
  364. sbegin "Need" "${f##*/}"
  365. if [ ! -f "$B_PROVIDED_DIR/${f#$B_NEEDED_DIR}" ]; then
  366. send 1
  367. serr "Deploy error" "${f#$B_NEEDED_DIR/} not provided, but needed by the following repos:"
  368. sed 's:^:'"$(prefix)"' - :g' "$f"
  369. return 1
  370. else
  371. send 0
  372. fi
  373. fi
  374. done
  375. sout
  376. sbegin "Sed"
  377. cat "$B_DIR/sedfile"{.pre,,.post} >"$B_DIR/sedfile.all"
  378. find -O3 "$B_STAGING_DIR" -type f -size -64k -exec sed -i -f "$B_DIR/sedfile.all" '{}' '+'
  379. send $?
  380. rm "$B_DIR/sedfile.all"
  381. }
  382. # deploy <src> <dest> <quiet>
  383. # Deploy all staged files from `$B_STAGING_DIR/$1` to `$2`
  384. # If "$3" != "" don't confirm each write
  385. function deploy {
  386. status "Deploy" "${2:-/}"
  387. local entire_dir=""
  388. local idx; idx="$B_INDEX_DIR/$1-$(whoami)"
  389. for f in $(find "$B_STAGING_DIR/$1" \( -type f -o -type l \) -print | LC_COLLATE=C sort); do
  390. # Get the destination file name
  391. local dest="$2${f#$B_STAGING_DIR/$1}"
  392. # Check for differences
  393. if ! diff "${B_DIFF_OPTS[@]}" "$f" "$dest" >/dev/null 2>&1 \
  394. || [ "$(stat -c %A "$f" 2>/dev/null)" != "$(stat -c %A "$dest" 2>/dev/null)" ]; then
  395. echo "${yellow}--------------------------------------------------------------------------------${reset}"
  396. echo "${yellow}$f ${dest}${reset}"
  397. # Diff both files
  398. diff "${B_DIFF_OPTS[@]}" "$dest" "$f"
  399. sedopts=(-e "s/r/${green}r$reset/g" -e "s/w/${yellow}w$reset/g" -e "s/x/${blue}x$reset/g" -e "s/s/${red}s$reset/g")
  400. echo "${yellow}--------------------------------------------------------------------------------${reset}"
  401. echo -n "old: "
  402. { stat -c %A "$dest" 2>/dev/null || echo; } | sed "${sedopts[@]}"
  403. echo -n "new: "
  404. { stat -c %A "$f" 2>/dev/null || echo; } | sed "${sedopts[@]}"
  405. while true; do
  406. echo -n "Write $yellow$dest$reset ? [ ${green}(y)es${reset} | ${red}(n)o${reset} | ${blue}yes to (d)irectory${reset} ] "
  407. if [ -n "${3:-}" ] || [ "$entire_dir" == "$(dirname "$dest")" ]; then
  408. echo "y"
  409. answer="y"
  410. break
  411. else
  412. read -rn 1 answer
  413. echo
  414. case "${answer,,}" in
  415. y|n|d)
  416. break
  417. ;;
  418. esac
  419. fi
  420. done
  421. if [ "${answer,,}" == "d" ]; then
  422. entire_dir="$(dirname "$dest")"
  423. answer="y"
  424. elif [ "$(dirname "$dest")" != "$entire_dir" ]; then
  425. entire_dir=""
  426. fi
  427. # Copy the file
  428. if [ "${answer,,}" == "y" ]; then
  429. mkdir -p "$(dirname "$dest")"
  430. cp "${B_CP_OPTS[@]}" "$f" "$dest"
  431. echo "$dest" >> "$idx"
  432. echo "${green}Ok$reset"
  433. else
  434. echo "${red}Not written$reset"
  435. fi
  436. else
  437. echo "$dest" >> "$idx"
  438. fi
  439. done
  440. # Clean up index file
  441. sort "$idx" > "$idx.tmp"
  442. uniq "$idx.tmp" > "$idx"
  443. rm "$idx.tmp"
  444. # Check for deleted files
  445. mapfile -t buf <"$idx"
  446. for f in "${buf[@]}"; do
  447. # If the indexed file is under the current deploy directory
  448. # If the indexed file doesn't exist in the staging dir
  449. grep -q "^$2" <<<"$f" && if [ ! -e "$B_STAGING_DIR/$1/${f#$2}" ] && [ ! -L "$B_STAGING_DIR/$1/${f#$2}" ]; then
  450. # but exists in the destination dir
  451. if [ -e "$f" ]; then
  452. echo -n "Delete $yellow$f$reset ? [ ${green}(y)es$reset | ${red}(N)o$reset ] "
  453. read -rn 1 answer
  454. echo
  455. if [ "${answer,,}" == "y" ]; then
  456. rm -f "$f"
  457. # Recursively remove containing directories when empty
  458. local d; d="$(dirname "$f")"
  459. while rmdir "$d" 2>/dev/null; do
  460. echo "Also removing empty directory $yellow$d$reset ..."
  461. d="$(dirname "$d")"
  462. done
  463. # Remove index entry
  464. grep -v "$f" "$idx" > "$idx.tmp"
  465. mv "$idx.tmp" "$idx"
  466. fi
  467. # and doesn't exist in the destination dir either
  468. else
  469. # Remove index entry
  470. grep -v "$f" "$idx" > "$idx.tmp"
  471. mv "$idx.tmp" "$idx"
  472. fi
  473. fi
  474. done
  475. }
  476. export -f deploy
  477. # deployhome <username>
  478. # equal to `deploy home $HOME` as user `$1`
  479. function deployhome {
  480. if [ "$(whoami)" != "$1" ]; then
  481. status "su" "$1"
  482. sin
  483. sudo sudo -u "$1" B_DIR="$B_DIR" lvl="$lvl" "$(realpath "$0")" deployhome "$1" || exit 1
  484. sout
  485. else
  486. status "Deploy" "home"
  487. sin
  488. deploy home "$HOME" "${2:-}" || exit 1
  489. sout
  490. if [ "$1" == "${users[0]:-}" ]; then
  491. status "Deploy" "prim"
  492. sin
  493. deploy prim "$HOME" "${2:-}" || exit 1
  494. sout
  495. fi
  496. fi
  497. }
  498. # deployroot
  499. # equal to `deploy root /` as user root
  500. function deployroot {
  501. if [ -d "$B_STAGING_DIR/root" ]; then
  502. if [ "$(whoami)" != root ]; then
  503. status "su" "root"
  504. sin
  505. sudo B_DIR="$B_DIR" lvl="$lvl" "$(realpath "$0")" deployroot || exit 1
  506. sout
  507. else
  508. status "Deploy" "root"
  509. sin
  510. deploy root "" || exit 1
  511. sout
  512. fi
  513. fi
  514. }
  515. # upgrade
  516. function upgrade {
  517. clear
  518. stage "$rootpkg" || exit 1
  519. if [ -f "$B_DIR/current-theme" ]; then
  520. stage "$(cat "$B_DIR/current-theme")" || exit 1
  521. else
  522. stage "${themes[0]}" || exit 1
  523. fi
  524. dosed || exit 1
  525. for u in "${users[@]}"; do
  526. deployhome "$u" || exit 1
  527. done
  528. if [ -n "${rootfiles:-}" ]; then
  529. deployroot || exit 1
  530. fi
  531. hook reload || exit 1
  532. clear
  533. }
  534. # theme <name>
  535. function theme {
  536. clear
  537. echo "$1" >"$B_DIR/current-theme"
  538. stage "$rootpkg" "$1" || exit 1
  539. dosed || exit 1
  540. deployhome "$(whoami)" quiet || exit 1
  541. hook theme || exit 1
  542. clear
  543. }
  544. # stagelist <listfile>
  545. # Stage all repos listed, one per line.
  546. function stagelist {
  547. while read -r repo; do
  548. if [ -n "${repo:-}" ]; then
  549. if ! stage "$repo"; then
  550. echo "In file $1"
  551. return 1
  552. fi
  553. fi
  554. done <"$1"
  555. }
  556. # hook <event> [args] ...
  557. function hook() (
  558. cd "$HOME"
  559. status "Hook" "$@"
  560. sin
  561. export sgn="$1"
  562. function on {
  563. local found=
  564. while [ -n "${1:-}" ]; do
  565. if [ "$1" == "$sgn" ]; then
  566. found=1
  567. export did_something=1
  568. break
  569. fi
  570. shift
  571. done
  572. if [ -z "${found:-}" ]; then
  573. if [ -z "${did_something:-}" ]; then
  574. echo "Skipped, not running for: $sgn"
  575. exit 202
  576. else
  577. exit 0
  578. fi
  579. fi
  580. }
  581. export -f on
  582. function check {
  583. if [ "$1" == "!" ]; then
  584. shift
  585. if "$@"; then
  586. echo "Skipped, command: !" "$@"
  587. exit 202
  588. fi
  589. else
  590. if ! "$@"; then
  591. echo "Skipped, command:" "$@"
  592. exit 202
  593. fi
  594. fi
  595. }
  596. export -f check
  597. sbegin "Checking" "internet connectivity"
  598. if swait 10 ping -c 1 8.8.4.4; then
  599. sbegin "Checking" "DNS availability"
  600. if swait 3 nslookup "en.wikipedia.org"; then
  601. export online=1
  602. else
  603. unset online 2>/dev/null || true
  604. fi
  605. fi
  606. rm "$B_LOG_DIR/"* 2>/dev/null || true
  607. if [ -n "${parallel_hooks:-}" ]; then
  608. function _do_hook {
  609. local f="$1"
  610. shift
  611. status "$@" "$(basename "$f")"
  612. local logfile; logfile="$B_LOG_DIR/$(basename "$f")"
  613. set +e
  614. "$f" "$@" >"$logfile" 2>&1
  615. local s="$?"
  616. if [ "$s" -ne 0 ] && [ "$s" -ne 202 ]; then
  617. swarn "Warning" "$f returned $s"
  618. notify-send "$f returned $s" "$(cat "$logfile")"
  619. fi
  620. set -e
  621. }
  622. for i in $(seq 1 99); do
  623. local n; n="$(printf '%02d' "$i")"
  624. for f in "$B_HOOK_DIR/$n-"*; do
  625. test -x "$f" || continue
  626. _do_hook "$f" "$@" &
  627. done
  628. wait
  629. done
  630. else
  631. for f in "$B_HOOK_DIR/"*; do
  632. test -x "$f" || continue
  633. sbegin "$@" "$(basename "$f")"
  634. local logfile; logfile="$B_LOG_DIR/$(basename "$f")"
  635. set +e
  636. "$f" "$@" </dev/null >"$logfile" 2>&1
  637. local s="$?"
  638. set -e
  639. send_ "$s"
  640. if [ "$s" != 0 ]; then
  641. notify-send "$f returned $s" "$(cat "$logfile")"
  642. fi
  643. done
  644. fi
  645. sout
  646. unset online
  647. )
  648. function repos() (
  649. cd "$B_REPOS_DIR"
  650. for d in *; do
  651. test -d "$d" || continue
  652. (
  653. cd "$d"
  654. local s; s="$(git status --porcelain=v2 --branch)"
  655. status "$d"
  656. sin
  657. if echo "$s" | grep -vq "^#"; then
  658. status "" "Uncomitted changes in repo $d"
  659. fi
  660. if echo "$s" | grep "^# branch.ab" | grep -vq "^# branch.ab +0 -0"; then
  661. status "" "Unsynchronized changes in repo $d"
  662. fi
  663. sout
  664. )
  665. done
  666. )
  667. function start (
  668. local name="$1"
  669. if [ ! -x "$B_SVC_DIR/$name" ]; then
  670. echo "start: $name: not found"
  671. return 1
  672. fi
  673. if ! running "$name" >/dev/null; then
  674. cd
  675. setsid "$B_SVC_DIR/$name" </dev/null >"$B_LOG_DIR/$name" 2>&1 &
  676. local pid="$!"
  677. disown "$pid"
  678. echo "$pid" > "$B_RUN_DIR/$name"
  679. echo "start: command started with pid $pid"
  680. echo -n " "
  681. cat "/proc/$pid/cmdline" | xargs -0 -n 1 printf " '%s'"
  682. echo
  683. else
  684. echo "start: $name: already running"
  685. fi
  686. )
  687. function stop {
  688. local timer=10
  689. if [ "$1" == "-t" ]; then
  690. timer="${2}0"
  691. shift 2
  692. fi
  693. local name="$1"
  694. if [ ! -f "$B_RUN_DIR/$name" ]; then
  695. echo "stop: $name: not started"
  696. return 1
  697. fi
  698. function killproc {
  699. if kill -0 "$1" 2>/dev/null; then
  700. echo "stop: found pid $1"
  701. echo -n " "
  702. cat "/proc/$1/cmdline" | xargs -0 -n 1 printf " '%s'"
  703. echo
  704. echo "stop: TERM $1"
  705. kill -TERM "$1" 2>/dev/null || return 0
  706. kill -CONT "$1"
  707. for i in $(seq 0 "$timer"); do
  708. sleep 0.1
  709. kill -0 "$1" 2>/dev/null || return 0
  710. echo "stop: still alive after ${i}00ms ..."
  711. done
  712. echo "stop: KILL $1"
  713. kill -9 "$1"
  714. fi
  715. }
  716. function ptree {
  717. local children; children="$(ps -o pid= --ppid "$1")"
  718. for pid in $children; do
  719. ptree "$pid"
  720. done
  721. echo "$children"
  722. }
  723. local pid; pid="$(cat "$B_RUN_DIR/$name")"
  724. rm "$B_RUN_DIR/$name"
  725. if [ -z "$pid" ]; then
  726. echo "stop: $name: not started"
  727. return 1
  728. fi
  729. local children; children="$(ptree "$pid")"
  730. killproc "$pid"
  731. echo "stop: checking children"
  732. for c in $children; do
  733. killproc "$c"
  734. done
  735. }
  736. function signal {
  737. local name="$1"
  738. local sig="$2"
  739. if [ ! -f "$B_RUN_DIR/$name" ]; then
  740. echo "signal: $name: not started"
  741. return 1
  742. fi
  743. local pid; pid="$(cat "$B_RUN_DIR/$name")"
  744. kill -s "$sig" "$pid"
  745. }
  746. function running {
  747. if [ -n "${1:-}" ]; then
  748. local name="$1"
  749. if [ -f "$B_RUN_DIR/$name" ]; then
  750. local pid; pid="$(cat "$B_RUN_DIR/$name")"
  751. if kill -0 "$pid" 2>/dev/null; then
  752. printf "%5d %s%-8s${reset} %s :" "$pid" "${green}" "Running" "$name"
  753. cat "/proc/$pid/cmdline" | xargs -0 -n 1 printf " '%s'"
  754. echo
  755. return 0
  756. else
  757. printf "%5d %s%-8s${reset} %s\n" "$pid" "${red}" "Crashed" "$name"
  758. test ! -e "$B_LOG_DIR/$name" || ( tail -n 10 "$B_LOG_DIR/$name" | sed 's:^:'$'\t'':' )
  759. return 2
  760. fi
  761. else
  762. printf " - %s%-8s${reset} %s\n" "${blue}" "Inactive" "$name"
  763. test ! -e "$B_LOG_DIR/$name" || ( tail -n 10 "$B_LOG_DIR/$name" | sed 's:^:'$'\t'':' )
  764. return 1
  765. fi
  766. else
  767. local name; for name in $(find "$B_SVC_DIR" -type f -printf "%f\n" | sort); do
  768. running "$name" || true
  769. done
  770. fi
  771. }
  772. function toggle {
  773. local timer=""
  774. if [ "$1" == "-t" ]; then
  775. timer="$2"
  776. shift 2
  777. fi
  778. local name="$1"
  779. if [ -f "$B_RUN_DIR/$name" ]; then
  780. if [ -n "${timer:-}" ]; then
  781. stop -t "$timer" "$name"
  782. else
  783. stop "$name"
  784. fi
  785. else
  786. start "$name"
  787. fi
  788. }
  789. function reapd {
  790. cd "$B_RUN_DIR"
  791. while true; do
  792. for f in *; do
  793. if ! kill -0 "$(cat "$f")" 2>/dev/null; then
  794. local logfile; logfile="$B_LOG_DIR/$(basename "$f")"
  795. notify-send "reapd: $f died" "$(tail -n 20 "$logfile")"
  796. rm "$f"
  797. fi
  798. done
  799. sleep 5
  800. done
  801. }
  802. function log {
  803. tail -f -n +1 "$B_LOG_DIR/"*"$1"*
  804. }
  805. function run {
  806. local fails=0
  807. local cmd="$(cat)"
  808. local input=
  809. local timing=60
  810. local failtiming=1
  811. local maxfails=3
  812. local title=
  813. while [ -n "${1:-}" ]; do
  814. case "${1:-}" in
  815. "--stdin")
  816. input="$(bash -c "$2")"
  817. shift 2
  818. ;;
  819. "--sleep")
  820. timing="$2"
  821. shift 2
  822. ;;
  823. "--retry-sleep")
  824. failtiming="$2"
  825. shift 2
  826. ;;
  827. "--retry")
  828. maxfails="$2"
  829. shift 2
  830. ;;
  831. "--title")
  832. title="$2"
  833. shift 2;
  834. ;;
  835. *)
  836. echo "Unknown option: $1"
  837. exit 1
  838. ;;
  839. esac
  840. done
  841. printf "run-%s" "${title:-?}" > /proc/$$/comm
  842. set +e
  843. while true; do
  844. bash -c "$cmd" <<<"$input"
  845. if [ "$?" -eq 0 ]; then
  846. sleep "$((timing))"
  847. else
  848. fails="$((fails + 1))"
  849. echo "command failed ($fails/$maxfails)"
  850. if [ "$fails" -ge "$maxfails" ]; then
  851. echo "giving up"
  852. exit 1
  853. fi
  854. sleep "$failtiming"
  855. fi
  856. done
  857. }
  858. setup
  859. "$@"