水曜日, 12月 20, 2017

邪悪なxonsh(の予定だったんだけどなぁ・・・)

Xonshを邪悪に使おうとしたが失敗した話から

beepcap

2017/12/20

はじめに

10日ぶりである。
この記事はXonsh Advent Calendar 2017の20日目だ。


Xonshに対する解説は他の人がたくさん書いているので、 この記事では邪悪な使い方を書こうと思っていた。 まず僕の用途だとXonshはとてもいいものだけど、 ちょっとまだ実用に至ってない。 zshのほうがちょっとだけ便利だ。 補間の学習具合や、historyに溜め込んだコマンドの記録が僕を拘束する。 historyを移植してやろうと思ったが、Xonshの履歴はちょっとばかし 扱いがめんどくさかったんでやめた。 そんなわけで気合が入らないなか、試してみたことの 記録を展開していこうと思う。 なお、この記事はXonsh 0.5.12をベースに書いている。 もしも改善されていたり、間違っていたらどんどん指摘してくれ。 Twitterの@beepcapで待ってるぞ。

最初の不満は

最初の不満はリダイレクトにあった。 Xonshはその性質上Pythonで書いた関数をシェル上で利用できるが、 これをシェルの中の人に渡すにはaliasを使わねばならず、 そうすると折角関数で作ったのに引数が使えずという苦しみが出てくるのだ。 もちろん、そこはこう・・・パラメータを与えてやればいい

$ echo 'hogehoge' | fuga > out.txt

・・・・・・ いやいや正気か? どう考えてもこうやりたい[*]

$ fuga('hogehoge') > out.txt

そこで考えた。 「aliasを使うから引数を付けられないのである」 「!そうだaliasなしでリダイレクト制御すればよくね!?」 Pythonのコードをこうすれば、>を再現することが出来る。

class fuga:
  gtxt = ""
  def __init__(self, in_t):
    fuga.gtxt = in_t
  def __gt__(self, other):
    f = open(other, "w")
    f.write(fuga.gtxt)
    f.close()

$ fuga("hogehoge") > "out.txt"

Oh!Yes!!!
ほんのちょっとばかし"が増えてしまったが、 おおよそ思い通りではないか?!・・・・・・ ・・・うん。そうなんだ。 Pythonは文字列を扱うときには""が必須で、これを外せなかったんだ・・・ 悲しい。 挫折した。 [*]aliasを使う方法に対するメリットがあるとすれば、渡されるオブジェクトが ファイル名でなくても良いという点だろうか。 例えば、


$ fuga("hogehoge") > "twitter"

こういう使い方も考えられるわけだ。 ツイートするのが簡単になる。 [*]
がまぁ、テキストの指定がダサいので失敗とする。

xonshの秘密をあかしてやろうとしたんだが・・・

xonshを今回いじるにあたって、自分に一つのルールを課した。 それは、「ソースコードを見ない」ということである。 我々は開発者であり、しかもxonshを喜んで扱うような人間であるからして Pythonには詳しい[*]。 その我々がソースコードを見ながらhackすれば、そりゃ何でも出来てしまうはずだ。 あくまでも、プロプライエタリ・ソフトウェアに対するように、 ユーザに開示されている情報のみで解析しなければ、面白くないだろう。

そんなわけでxonshをポチポチ遊んでいたのだが、 Twitterでばんくし(@vaaaaanquish)と会話している時に こんな発見があった。

Xonshは色々と強力だが、長くつけっぱなしで使用していると ゴミが多くなってくる。 これはzshなども同じはずなのだが、あいつらはそれが見えないので なんとなく気にされてない。 しかしXonshはPythonなので、dir()を呼べば一覧が出てくる。 もちろんdelで解放することも出来る・・・はずだった。


$ for i in dir():
    del i

このコードを実行してみてほしい。 バージョンによってコードは予想外の動きをする。 例えば私の手元の0.5.12では、「何も起こらない」。 意味が分かるだろうか? dir()内の要素が消せないのだ。 ところがどっこい、例えば


$ del __name__

では、ちゃんとname要素が消えてなくなるのである。 どうもbuiltinsを消そうとするとなにか強制力が働いて、 処理が中断されるようなのだ。 実際、dir()を使った解放コードはある時まで [*]Xonshの本体を巻き込んで死亡していた。

このbuiltins、何か秘密が隠れているに違いない。

調査のために、以下のように確認してみる。 [*]


$ dir(__builtins__)

ふむふむ、ちょっと中身は違うが標準的なbuiltinsの要素を含んでいそうだ・・・ オブジェクトの名前でも確認してやろうかな・・・


$ print(__builtins__.__name__)

AttributeError: 'dict' object has no attribute '__name__'

は?

え?いやいや、オブジェクトならそれが引けないって致命的でしょ・・・

ちょっとまて、あ、手が滑った


$ print(__builtins__)

'__name__': 'builtins', '__doc__': "Built-in functions,
 exceptions, and other objects.\n\nNoteworthy: None is the `nil' object;
 Ellipsis represents `...' in slices.", '__package__': ”, '__loader__':
 <class '_frozen_importlib.BuiltinImpor
...略
 

お、おい・・・お前これ・・・ dictじゃねーか!!!!!!!!!!!!!!!!!!!

そう、そういうことなのだ。 Xonshはbuiltinsの中身をまるごと置き換えていたのだ。 しかも、built-inモジュールではなく、辞書型のオブジェクトとして。 この中身は大変面白いので、みんなもぜひいじってみてほしい。 例えば、


$ __builtins__['testtxt'] = "test text."
$ testtxt
'test text.'

といった感じである。 aliasなどの定義もこの中にある。

が、時間不足でこの遊びも失敗した。 subprocessを利用している処理を一部置き換えることには成功したのだが、 まだ、根幹の定義済み命令を置き換えると、xonshがハングアップしてしまう。 [*]これでは大して悪いことに使えない。

ということで、失敗である。

イカロスの翼

神話の時代にイカロスは翼を作って飛び立ったが、 太陽の熱に焼かれて翼が壊れ、落下して死んだそうだ。 あれから得られる教訓は、「パワーが足りない」である。 なぜロケット推進にしなかったのか。

そんなわけで、xonshでも処理するにパワーが足りない時がある。 パワーといえばマルチプロセスだ!!! しかし、bashやzshのように、pipeでつなげばプロセスが起きたり、 xargsを使った今流行りのhackも使えない。 Pythonにはmultiprocessing()があるが、 CPythonがベースのxonshではGILがある。 性能への影響がどの程度とかそういうの関係なく嫌だ。 Cとの結合をしようとしたらGILが働く設計にしとけばよかったじゃんか [*]

ということで、os.fork()を使うことにした。

fork()を使用したマルチプロセス化の問題点として fork()は子と親でメモリ以外の資源を共有する、という点が挙げられる。 例えばxonshで


$ import os
$ os.fork()

とやると、とんでもないことになる。 入出力が多重化し、まともに入力を受け付けなくなるだけではなく、 表示も二重に出て、有り体に言えばバグる。 [*]そして、親プロセスを終わらせたが最後、ゾンビと化した子供は エラーを吐き続けターミナルを埋め尽くすのである。

何が悪いのか。 xonshはターミナルの上で動くCUIプログラムなので、 ターミナルソフトとの通信には当然、stdin, stdoutを利用している。 fork()するということは、これらを複数のプロセスで共有してしまうことになる。

ここで、経験の多い人はこう思うだろう。

「stdinとstdoutを閉じればいいんだよ馬鹿だなぁ」

やってみよう。


$ import sys
$ sys.stdin.close()

どうなっただろうか。 おそらく例外が発生して/bin/shへfallbackされたのではないだろうか? コードを見ないルールなので、これは予測だが、 トレースバックを見るにどうやらstdinに対してxonshはポーリングを掛けており、 これが閉じられてしまうケースを想定していないようなのだ。 Oh...じゃあfork()しても、stdinをclose()した段階で、 プロセス落ちちゃうじゃん。

ところが、ここに光明が見える。


$ import sys
$ sys.stdout.close()

どうなっただろうか? 何も起こらない? ではこうして見てほしい。


$ import sys
$ sys.stdout

お分かりだろうか。 そう、このstdoutは_ioに属していないのだ。 stdoutが別のclassに差し替えられているということは、 同じことをstdinにも施してやればいい。 無害なインタフェースクラスを作り、stdoutとstdinを差し替えてやればオッ!

・・・

ところが、そうは行かなかったのである・・・。

解析中にトレースバックで見たのだが・・・


$ import os
$ os.close(1)

これ[*]をやって出てくる トレースバックに、 あろうことか


  File "/usr/lib/python3/dist-packages/xonsh/__amalgam__.py",
  line 15626, in settitle
      with open(1, 'wb', closefd=False) as f:

fuxx!!! オイオイオイオイ、その使い方は無いだろオイオイ!

stdoutをですね!ダイレクトにですね! 叩いてるコードがいるんですね!!!

俺の言えた義理じゃないが邪悪すぎやしませんかね?

ということで、いまだに俺はXonshを使ってfork()をうまく使うことが出来ない。 だれか助けてくれ。

最後に

新たなおもちゃを手に入れたと思って遊んでいたが、成果だけが出ていなかった。 皆さまはこのような悪い大人になってはいけませんよ?

それではまた。

About this document ...

Xonshを邪悪に使おうとしたが失敗した話から

This document was generated using the LaTeX2HTML translator Version 2017.2 (Released Jan 23, 2017)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.

The command line arguments were:
latex2html -split 0 xonsh.tex

The translation was initiated on 2017-12-20


Footnotes

... どう考えてもこうやりたい[*]
これ実は出来るよって情報求む
... 挫折した。[*]
これも出来るなら教えてほしいと思うが・・・いやこれは無理やろ
... ツイートするのが簡単になる。[*]
ほんとか?tweet()みたいな関数用意しとけばいいだけじゃね?
... Pythonには詳しい[*]
そんな気がする
... 実際、dir()を使った解放コードはある時まで[*]
バージョン控えておかなかったんだよなぁ
...調査のために、以下のように確認してみる。 [*]
Pythonにおいては未知のオブジェクトを調べる標準的な手法だ
... まだ、根幹の定義済み命令を置き換えると、xonshがハングアップしてしまう。[*]
なにやら新しいバージョンで試したら、今度はハングアップしなくなっているのかな?これは期待が持てる
... Cとの結合をしようとしたらGILが働く設計にしとけばよかったじゃんか[*]
愚痴
... 表示も二重に出て、有り体に言えばバグる。[*]
xonshのバグではないと思うけど
...これ[*]
stdoutを無理やり止めようとしたのだ

0 件のコメント:

自己紹介

自分の写真
NetRadioDJ ...since 2003, Programer ...since 1994