基於 systemd 的背景執行程式與備忘錄的方法

我目前主要是用 nushell 。nushell 沒有原生支援 backend process有點困擾。又我又很常在 nvim terminal 下執行指令,因此能夠在背景執行又不依賴現在 termianl很重要

過去我有用 pueue 做為背景執行程式,不過使用上沒有很順手(我不喜歡每次看東西都要先查 id)。於是考慮用 systemd-run 實做一個。目前感覺起來很可以。

systemd-run 執行程式時,需要指定 unit。除非程式正常結束,否則 unit 不能重複(當然這行為是根據你參數而定,目前我是這樣使用)

以下程式碼皆是 nushell 語法

def --wrapped "run" [ --doc="", ...command ] {
  mut desc = ($command | str join ' ')
  if ($doc != "") {
    $desc = $doc
  }
  let _unit = (not-used-units|first)
  systemd-run --user -u $_unit --service-type=oneshot -d --no-block --description $desc ...$command
  $_unit
}

目前我的 unit 範圍是 "run", "run1", "run2", ..., "run9"。有太多 unit 可以使用就和用 pueue 沒兩樣了

當程式丟到 systemd 執行後,我會想要知道有哪些工作已經完成,有哪些正在執行,有哪些失敗。因為我很常在下指令,因此就做在 prompt

def bg-running [ ] {
  running-units | each {|it| $"[($it.Id|str replace '.service' ''|str replace 'run' ''):(if ($it.ActiveState == "inactive") {""})(if ($it.ActiveState == "failed") {""})($it.Description)]"} | str join ' '|str trim
}

這樣我就可以很方便的在 shell prompt 中看到我放到 systemd 中的程序狀態了

如果想要看程式執行結果,可以透過 journalctl 指令

def log [ $unit?:string@all-unit-name , --follow (-f)] {
  let stdin = $in
  mut extra = [ ]
  mut _unit = $unit
  if $follow {
    $extra = ($extra | append ["-f"])
  }
  if ($_unit == null) {
    $_unit = $stdin
    $extra = ($extra | append ["-f"])
  }
  if ($env.IN_VIM? == "1") {
    journalctl --user -u $_unit -e --no-hostname --no-pager ...$extra
  } else {
    journalctl --user -u $_unit -e --no-hostname ...$extra
  }
}

有時候我們將程序放進背景後,想要直接看 log 輸出了(不然就和 pueue 差不多),就可以用 nushell 函數組合

run ps |log

有時候會忘記東西放到哪個 unit,所以就寫個指令印出所有 unit 的 log

def all-log [ -n:int=5 ] {
  all-unit-name | par-each -t 4 {|it| {name: $it, log: (journalctl --user -u $it -n $n --no-hostname)} }|sort-by name
}

既然我們的 prompt 會顯示背景執行的程式,那麼如果我把要記錄的資訊放到 sleep infinity 裡面,是不是就可以當作備忘錄了。於是多了一個備忘錄功能(目前是將資訊記錄在 unit 的 description 欄位)

def note [ -t="infinity", --after (-a): string="", text ] {
  let _unit = (not-used-units|first)
  mut extra = []
  if ($after != "") {
    $extra = [--on-active $after]
  }
  systemd-run --user -u $_unit --service-type=oneshot -d --no-block --description $"📓($text)" -G ...$extra sleep $t 
}

就這樣完成了一個背景執行程式與備忘錄了

完整程式碼如下

$env.max_jobs = 9

def --wrapped "run" [ --doc="", ...command ] {
  mut desc = ($command | str join ' ')
  if ($doc != "") {
    $desc = $doc
  }
  let _unit = (not-used-units|first)
  systemd-run --user -u $_unit --service-type=oneshot -d --no-block --description $desc ...$command
  $_unit
}

def not-used-units [ ] {
  let running_unit_names = (all-unit-info|filter {|it|
    $it.ExecStart? != null
  }|get Id|each {|it| $it|str replace '.service' ''})
  all-unit-name |filter {|it| $it not-in $running_unit_names}
}

def note [ -t="infinity", --after (-a): string="", text ] {
  let _unit = (not-used-units|first)
  mut extra = []
  if ($after != "") {
    $extra = [--on-active $after]
  }
  systemd-run --user -u $_unit --service-type=oneshot -d --no-block --description $"📓($text)" -G ...$extra sleep $t 
}

def log [ $unit?:string@all-unit-name , --follow (-f)] {
  let stdin = $in
  mut extra = [ ]
  mut _unit = $unit
  if $follow {
    $extra = ($extra | append ["-f"])
  }
  if ($_unit == null) {
    $_unit = $stdin
    $extra = ($extra | append ["-f"])
  }
  if ($env.IN_VIM? == "1") {
    journalctl --user -u $_unit -e --no-hostname --no-pager ...$extra
  } else {
    journalctl --user -u $_unit -e --no-hostname ...$extra
  }
}

def all-log [ -n:int=5 ] {
  all-unit-name | par-each -t 4 {|it| {name: $it, log: (journalctl --user -u $it -n $n --no-hostname)} }|sort-by name
}

def show [ unit:string@running-units-complete ] {
  systemctl --user status $unit
}

def get-systemd-info [ unit: string ] {
  systemctl --user show $unit|lines|each {|it| split row '=' -n 2|{ $in.0 : $in.1 }}|reduce {|a, b| $a | merge $b}
}

def stop [ ...units:string@running-units-complete ] {
  $units | par-each -t 2 {|unit|
    if (systemctl --user show $unit|find 'ActiveState=inactive'|is-not-empty) {
      systemctl --user stop $"($unit|str replace '.service' '').timer"
    } else  if (systemctl --user show $unit|find 'ActiveState=failed'|is-not-empty) {
      systemctl --user reset-failed $unit
    } else {
      systemctl --user stop $unit
    }
  }
  null
}

def clean [ ] {
  running-units | par-each {|it| stop $it.Id }
  null
}

def all-unit-name [ ] {
  ["run"] | append (seq 1 $env.max_jobs|each {|it| $"run($it)"})
}

def all-unit-info [ ] {
  all-unit-name | par-each -t 4 {|it| get-systemd-info $it}
}

def running-units [ ] {

all-unit-info|filter {|it|

$it.ExecStart? != null

} | sort-by Id

}

def running-units-complete [ ] {

running-units | each {|it| {value: $in.Id , description: $in.Description}}

}

def bg-running [ ] {

running-units | each {|it| $"[($it.Id|str replace '.service' ''|str replace 'run' ''):(if ($it.ActiveState == "inactive") {"⏰"})(if ($it.ActiveState == "failed") {"❌"})($it.Description)]"} | str join ' '|str trim

}


You'll only receive email when they publish something new.

More from kjelly
All posts