Note: The Listed platform will permanently shut down December 31, 2026. Your data published on Listed will still be available in your personal Standard Notes account. Learn more

基於 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