続: 結局は psutil の方がより救世主なの (psutil.Process の nice)

psutil に触れたその時点でもう言わずもがなな人相手には言わずもがな、な内容なので、「ワタシが書くことか?」とも思うようなそんな。

Unix ユーザと Windows ユーザの違いの一つとして、「タスク(あるいは「ジョブ」、「プロセス」)」についての「優先度」が身近なのかどうか、てのがある。

命名の妙だよな、と思うのだよね。「Unix の教科書」的なものを開けばかなり最初の方に登場しそうなのが「やったねあんたはえらいnice値」で、なんでそう教科書的かというと、「Unix文化」をよく表した命名だから、なの。

しょっぱなから「パーソナルなコンピュータ」としてのスタートだった MSDOS, Windows, Mac OS らと違って、もともと Unix は「多重ログイン上等」のもとに誕生した OS である。つまり、「たくさんのユーザが一台の Unix 機にログインして使うので、「譲り合いの心が必要である」」と。そうして「プロセスの優先度を調整するプログラム」に「nice」という名前がついた。そう、「譲ってあげるオレ、ナイスでしょ?」てこと。ゆえ、Unix ユーザは、(少なくとも昔は)初心者であっても「プロセスの優先度を変更できる」ことを常識として知っていた。

一方で、Windows にはそんな文化はない上に、マイクロソフトが「そういうことはあなたは意識すべきではない」として、原則巧妙に隠しているので、実は Windows でも同じことが出来ることを、おそらく一般ユーザの多くが知らない。Unix 経験者のほうが気づきやすいとは思うが、そうでない場合でこれを知っているのは、特に Windows サービスなどのシステムプログラミング経験者で、一般のデスクトップアプリケーション開発しか経験がないと、技術者であっても結構知らないんではないかと想像する。出来るよ:

問題は、今の Windows 10 タスクマネージャでこの「優先度の設定」メニューを「どうやって出すか」だ。わからない人はわからない。気づかない人は多分一生気づかない。

そんなわけで、Windows 文化において、マイクロソフト自身以外でこの「優先度」を活用していると思われるのは「Windows を何かしらのサーバに採用した(奇特な)組織」くらいではないのかと思う。以外では、個々のアプリケーションが自身の優先度を変えるために使う例がわずかばかりある程度である(たとえば 7-zip などのアーカイバや、ビデオ編集ソフトなどの重いやつら)。

てわけで、Windows で Unix と同じようなノリで優先度を変えようとするのは「タスクマネージャがあぁなので」鬱陶しくて、普通はそんなに日常にはしてないだろう。そんなあなたに?:

suspend, resume ネタで書いたコードとほとんど同じである
 1 # -*- coding: utf-8 -*-
 2 from __future__ import print_function
 3 from __future__ import unicode_literals
 4 
 5 import os
 6 import sys
 7 import pipes
 8 import re
 9 import psutil
10 
11 
12 def _str(s):
13     return s.encode(sys.getfilesystemencoding(), errors="xmlcharrefreplace").decode(
14         sys.getfilesystemencoding())
15 
16 
17 _prio_nmap = {
18     "1": ("ABOVE_NORMAL_PRIORITY_CLASS", psutil.ABOVE_NORMAL_PRIORITY_CLASS),
19     "2": ("BELOW_NORMAL_PRIORITY_CLASS", psutil.BELOW_NORMAL_PRIORITY_CLASS),
20     "3": ("HIGH_PRIORITY_CLASS", psutil.HIGH_PRIORITY_CLASS),
21     "4": ("IDLE_PRIORITY_CLASS", psutil.IDLE_PRIORITY_CLASS),
22     "5": ("NORMAL_PRIORITY_CLASS", psutil.NORMAL_PRIORITY_CLASS),
23     "6": ("REALTIME_PRIORITY_CLASS", psutil.REALTIME_PRIORITY_CLASS),
24 }
25 
26 
27 if __name__ == '__main__':
28     import argparse
29     ap = argparse.ArgumentParser()
30     ap.add_argument("--search_cmdline_regexp", default=".*")
31     ap.add_argument("--search_cmd_regexp", default=".*")
32     args = ap.parse_args()
33     def _catcmdl(cmdl):
34         return " ".join([pipes.quote(a) for a in cmdl])
35     ma = {
36         "cmd": re.compile(args.search_cmd_regexp),
37         "cmdline": re.compile(args.search_cmdline_regexp),
38         }
39     for p in psutil.process_iter():
40         cmdl = ""
41         try:
42             cmdl = p.cmdline()
43         except Exception as e:
44             print(e, file=sys.stderr)
45         if not cmdl:
46             continue
47         tcmdl = _catcmdl(cmdl)
48         if ma["cmd"].search(cmdl[0]) and ma["cmdline"].search(tcmdl):
49             orig = p.nice()
50             print(_str(tcmdl), "|", p.nice())
51             q = input("{}? ".format(" / ".join(
52                 ["{}: {}".format(n, _prio_nmap[n][0]) for n in sorted(_prio_nmap.keys())])))
53             if q in _prio_nmap:
54                 p.nice(_prio_nmap[q][1])
55                 print(orig, "=>", p.nice())

どういうときに使いたいかって、たぶんだけど「優先度を上げたい」時ではない気がする。まさに ffmpeg なんだけど、「どうせ何時間もかかるんなら 1時間も2時間も同じろ」として優先度を下げてノンビリ処理してもらおうぜ、みたいなことをね、たぶんしたいことが多いと思うんだ。他のあらゆる作業ができなくなるほど凄まじく CPU 時間を使ってしまうことも多いからね。


2021-04-11追記:
思った以上に日常使い出来ると感じ始めてる。ゆえに、ちと整理して Gist 管理してみることにする:

まぁこういうの、欲張りだせばまだまだ色々あるんだけどさ。

なお、「WM_CLOSE を SendMessage」みたいなことも出来たらいいなぁと思ったが、やっぱりノリが Unix なんで、これは psutil はまかなってくれないみたい。これをやりたい場合、どこまでさかのぼらなければならんだろうか? ウィンドウハンドルが必要だろうから…、C まで戻らんと出来ないか? うーん、そのうち考えたいなぁ。


2021-10-11追記:
ほんとは別ネタとして独立して書いてもいいような話なんだけれど、どうもこのページに割と人が集まってるみたいなんで、ここに。

「より救世主」の意味するところが果たして伝わったのかいな、てのがね、書いた後ずっと気になってはいたのよね。話の半分以上を nice の話に費やしてしまっていたけれど、もともとの話は「glances のような汎用を目指した完成品が得てして痒いところに手が届かなくて困るので psutil そのものがうれしんだぜぃ」てハナシ。あと細かい話としては、ワタシは Windows ユーザとしての嬉しさばかり強調しがちだけれど、実際はほぼ Unix 系しか使わない人にとっても「ユニバーサルなプログラミング」が出来て嬉しい、てことにはなる。生粋の Unix ユーザほどポータビリティ至上主義みたいなところがあって、「Unix でしか使えない」ものを思ったよりは嫌う、少なくともワタシはそう。てなことどもがまぁ「より救世主」の意味だよ。

で、この補足説明だけなら追記しようとは思わないわけよ。「痒いところに手」のいい例があったんで、それをちょっと紹介しとこうかと。

(翻訳のヒドさで有名な)「Unix 原典」などを読むとわかることだったりするのだけれど、もともと「ウィンドウシステム」は、「(マルチユーザ・)マルチタスクOSであることをフルに活かすために不可欠」なものとして考えられた。今の現代的な OS ばかりをみているとなかなかこのことはわかりにくくなってしまっているけれど、Windows ユーザであれば「DOS窓フルスクリーンモード」を想像すると良い。これがいわゆる「ウィンドウシステムのない世界」が想像出来る、現代に残る遺跡だ。ウィンドウシステムがあれば、「たくさんのDOS窓を同時に動かせる」てぇことになるわけだ、「マルチタスクさまさまっ」。そう、「プロセスが同時並行で複数動かせる」だけでは人間にとっての有用性としては不十分で、「対人間インターフェイス部分の多重化」が必要だった、てこと。

この、「対人間インターフェイス部分の多重化」なんだけどね、確かに「複数ウィンドウを開くことで、複数のプロセスを同時進行出来る」のではあるけれど、「GUI」として設計されていないプロセスの多くが「人間が途中で介入できない」ものとして設計されることがほとんどなわけよ、特に自分で作るスクリプトなんぞでは「終わるまでは終わらないよ」、要は「作業Aが終わってから作業Bを実行する」というのを「後から思った」場合にかなり悲しいことになる:

実行してしまったら最後、この ffmpeg が終わるまでこのコンソールウィンドウは使えない
1 [me@host: ~]$ ffmpeg -y -i huge_video.mp4 -c:v libx265 huge_video.mkv
2    ... (とてつもなく時間がかかる、とか。数時間、的な) ...
常にこういう↓賢いスケジューリングで作業出来るとは限らない
1 [me@host: ~]$ ffmpeg -y -i huge_video.mp4 -c:v libx265 huge_video.mkv && \
2 > ffmpeg -y -i huge_video2.mp4 -c:v libx265 huge_video2.mkv 
3    ... (とてつもなく時間がかかる、とか。数時間、的な)...
4    ... (だけれども最初のが終われば2つ目は即座に実行される) ...

後者の例のようなのがいわゆる「賢いシェル使い」だとはいえ、常にこういうことを思い描けるわけではないし、作業の都合でこういうことが出来ないこともある。たとえば huge_video.mp4 処理開始時点で huge_video2.mp4 がまだない、とかね。

つまり「&&」みたいな、シェルによる連鎖実行という手段を取らなかった場合・取れなかった場合に、別ウィンドウ内でプロセスAの終了検出をしたい、て話ね、たとえば。すなわち「プロセス群の探索」、すわ、psutil の出番、てことだ:

 1 #! py -3
 2 # -*- coding: utf-8 -*-
 3 from __future__ import print_function
 4 from __future__ import unicode_literals
 5 
 6 import os
 7 import sys
 8 import re
 9 import time
10 import psutil  # pip install psutil
11 
12 
13 def _exists(det):
14     myproc = psutil.Process(os.getpid())
15     for p in psutil.process_iter():
16         if (p.pid == myproc.pid) or (p.pid == myproc.ppid()):
17             continue
18         cmdl = []
19         if args.search_mode in ("CMD",):
20             try:
21                 cmdl = p.cmdline()
22             except Exception as e:
23                 #print("cannot get commandline:", e, file=sys.stderr)
24                 pass
25             if not cmdl:
26                 continue
27         if any([cf(cmdl, p.pid) for cf in det]):
28             print(p, file=sys.stderr)
29             break
30     else:
31         return False
32     return True
33 
34 
35 if __name__ == '__main__':
36     import argparse
37     ap = argparse.ArgumentParser()
38     ap.add_argument("search_target", nargs="+")
39     ap.add_argument("--search_mode", choices=["PID", "CMD"], default="CMD")
40     args = ap.parse_args()
41     #
42     if args.search_mode == "CMD":
43         det = [lambda cmdl, pid: re.search(p, cmdl[0]) for p in args.search_target]
44     else:
45         try:
46             det = [lambda cmdl, pid: int(p) == pid for p in args.search_target]
47         except ValueError as e:
48             ap.error(e)
49     #
50     while _exists(det):
51         time.sleep(30)
52         if not _exists(det):
53             time.sleep(30)

たとえばこれを ps_sleepuntil.py という名前のスクリプトにするとして(なおかつ実行可能にするとして):

1 [me@host: ~]$ ps_sleepuntil.py ffmpeg && ffmpeg -y -i huge_video2.mp4 -c:v libx265 huge_video2.mkv

「検出対象が見つからなかった」のあとの判定がいい加減なので実用にはまだあまりならんけれど、言いたいことは伝わる、よね? こういうことを psutil を使えば、割と簡単に出来る、て話。これが「glances のような完成品としての価値よりはインフラとしての psutil が救世主」の一つの姿。


2021-10-20追記:
「2021-10-11追記」で、肝心なことを書き忘れていたことに気付いた。書き始める前は意識してるんだけどね、書き始めると書こうと思ってたことを書き忘れるなんてまぁ、よくある話。

実は「世界には Unix しかない」なら、「ps_sleepuntil.py」的な道具はいらない。問題の本質は、仮想端末一個まるまるを「フォアグラウンドプロセスが専有してしまう」(ので他のことが出来ない)ということなのだけれど、「フォアグラウンドで始めてしまった」ことを中断出来ることは実は利用できる。Unix で普通使われているシェルであれば、ジョブコントロールはかなり柔軟で、「サスペンド後、レジュームしたプロセス終了を待つ」ことが出来る:

 1 [me@host: ~]$ python
 2    ...
 3 >>> import os, time
 4 >>> while True:
 5 ...     if os.path.exists("result.txt"):
 6 ...         break
 7 ...     time.sleep(30)
 8 >>>
 9    ... Ctrl-Z でサスペンド…
10 [me@host: ~]$ fg %1 && echo "owata..."

「Ctrl-Z で…」とか「fg」が各々のシェルの頑張りで実現されているジョブコントロール機能なので、シェルが違えば違う可能性はあるが、まぁおおむねほとんどがこれ。これで出来るのであれば、「あ、しまった、これ、終わらないと終わらないや」とはならない。

問題はこれが Windows では「出来たり出来なかったりする」こと。「Unix もどき環境」の場合であっても、たとえば cygwin 系の場合「cygwin な DLL依存 or DIE」となってて、cygwin のお仲間プログラム以外はこれが出来ない。上の python の例だと、多くの Windows ユーザが選んでいる公式 CPython は cygwin ではないので、Ctrl-Z での中断が出来ない。あとね、上で gist 管理してる pscmdlines.py の suspend/resume を実際に活用し続けてみればわかるんだけれど、Windows の suspend/resume って、ちょっと安定しないんだよね。resume できなくなることが結構ある。のであまり日常使いは出来ない。

そんなこんなで、ようやっと「ps_sleepuntil.py」的なものが嬉しい、救世主だ、となる。ここまでの内容を「2021-10-11追記」を書く前にはここまで書こうと思ってたんだよ。うん、書くべきことをメモする癖をつけた方がいいよなぁ…。