miauのブログ

はてなダイアリー「miauの避難所」をはてなブログに移行しました

GVedit(Windows 用 Graphviz フロントエンド)のバグをなおしてみた

最近の案件では納品用ドキュメントも TracRedmineWiki で書いていますが、ちょっと DFD 的な図を差し込みたくなりまして。Graphviz であれば Trac でも Redmine でもへの埋め込みが可能なので、とりあえずこれで書くことに。

で、いつからか WindowsGraphviz には GVedit というフロントエンドがついてきているようで。これを使えば テキスト変更→出力画像の確認 の流れがスムーズになるかな、とばかりに試してみて・・・バグだらけだったので修正してみた、というお話です。

GVedit のバグについて

に解説がありますが、nkf 等で事前処理を行うと、テンポラリファイル末尾のゴミが付加されてしまいます。

これはこれでひとつのバグなんですが、それ以外にも以下の問題がありました。

  • テンポラリファイルの扱いが根本的におかしい
    • ソースを見ると、%TEMP% に作ったり GVedit.exe と同階層に作ったりで、動くわけがないような状態でした。
  • 現在配布されているバージョンでは、Preprocessor Settings の情報が反映されない
    • .ini ファイル経由で情報をやりとりしているんですが、こちらもパスの不整合がありました・・・。

ちなみに「現在配布されているバージョン」と書きましたが、今回動作を確認したバージョンは

  • GVedit バージョン: タイトルの表記は 1.01、バージョンダイアログでは 0.99 beta
  • Graphviz バージョン: 2.26.3、2.27.20110301.0545 の両方で確認

です。

完成品

経緯を書いていると長くなるので、先に完成品&使い方を。

こちらが完成品です。Graphvizインストーラ等で導入後、C:\Program Files\Graphviz2.26.3\bin 等にある Gvedit.exe を差し替えてください。

上記 3 つのバグを修正しただけなので、このままで日本語が扱えるわけではありません。まず Windows 用の nkf をどこかで入手しておいて、GVedit の「Graphviz」メニュー→「Preprocessor Settings」から

  • Path: C:\bin
    • nkf.exe の格納先
  • CommandLine=nkf.exe -S -w

のように指定して、GraphvizUTF-8 でテキストを与える必要があります。

※冒頭の参考 URL では -w ではなく -w8 が指定されていましたが、オプションの解説を見ると

-w -w80     UTF8コードを出力する。(BOM無し)
-w8    UTF8コードを出力する。

とのことなので、個人的な好みで BOM なしにしています。(UTF-8 は基本的に BOM が要らないエンコーディングのはずですし。)

また、グラフでは

digraph {
    graph[fontname="MS GOTHIC"];
    edge[fontname="MS GOTHIC"];
    node[fontname="MS GOTHIC"];
    :
}

のようにフォント指定しないと、日本語は正しく表示されません。ここは以下の URL を参考にしました。

毎回フォント指定をするのが面倒なら、こちらの方と同じようにスクリプトで変換をかけたほうがいいかもしれませんね。

問題点

しばらくは快適に使ってたんですが、何かのタイミングで %TEMP%\__temp.dot にロックがかかりっぱなしになって、うまく更新できなくなってしまうことがあるようで・・・。(PC の調子が悪いだけかもしれませんけど。)
なんかもう HTA + WinGraphviz で適当なフロントエンドでも作っちゃおうと考えてます。もう 5 年以上 WinGraphviz のバージョンが上がってないから、あまり使いたくなかったんですが・・・。できあがったらまたブログに書きます。


さて、ここから先は長い蛇足で、どうしてバグを修正するに至ったかという経緯とか、ビルド方法についてです。

発生した問題(1)・・・Preprocessor Settings が効かない

上のほうに書いたように、配布されているバージョンでは Preprocessor Setting がうまく設定されません。設定後「OK」で閉じる→またダイアログを開くと空白になっていて、設定が効いているのか調べるだけでも煩雑だったんですが、それはさておき。

何がおかしいのか調べるべくソースコードを読んでみたんですが・・・このダイアログを開くときの処理が、

void __fastcall TfrmPre::FormShow(TObject *Sender)
{
        TIniFile *ini;
        AnsiString FileName=getIniFile() ;
        if(FileExists(FileName))
        {
                ini = new TIniFile(FileName);
                :

こうで、「OK」を押したときの処理が、

void __fastcall TfrmPre::Button3Click(TObject *Sender)
{
        TIniFile *ini;
        AnsiString FileName=ExtractFilePath( Application->ExeName)+"Settings.ini" ;
        if(FileExists(FileName))
        {
                ini = new TIniFile(FileName);
                :
}

こう。

  • 開くときは %TEMP%\Settings.ini の内容を見に行く
  • 閉じるときは %USERPROFILE%\Local Settings\Application Data\Settings.ini に保存しようとする(ファイルがない場合は保存処理はスキップされる)

ということで、見事に食い違ってます。なんでこんなソースコードになってるんだか・・・。

この段階ではソースコードを変更するつもりはなかったので、%USERPROFILE%\Local Settings\Application Data\Settings.ini (%APPDATA% じゃなくて Local Settings\Application Data のほうなので注意)に、

PrePath=C:\bat
PreCmd=nkf.exe -S -w

と追加して、ようやく Preprocessor Settings を有効にできました。

発生した問題(2)・・・Preprocessor Settings を設定すると画像変換に失敗する

「Temporary file is missing!」とか言われてコケます。こちらもソースを確認すると、__temp.dot は %TEMP% にあることが想定されている感じなのに、GVedit と同階層の __temp.dot を探しに行ってたり・・・orz

仕方ないので GVedit.exe と props.txt(これがないとエラーになったりした)を %TEMP% にコピーしてパスを一致させてやる。すると dot.exe を発見できなくなるようなので、Settings.ini で

binPath=C:\Program Files\Graphviz2.26.3\bin\

みたいに正しいパスを指定してやる、と。

これで一応動作はするんですが、どうやら上のほうでも挙げた

の問題に引っかかるようで、Syntax Error だとか。%TEMP%\__temp.dot だか %TEMP%\__temp2.dot だかを見ると、

digraph finite_state_machine {
	node [fontname="MS GOTHIC"]
        :(中略)
    }       LR_8 -> LR_6 [ label = "S(b)" ];
        LR_8 -> LR_5 [ label = "S(a)" ];
    }

のように末尾のところが二回繰り返されるような感じになっていました。

じゃああまり気が進まないけどバイナリパッチをあてるかー・・・と、バイナリエディタで開いてみると、

ちなみに "__temp2.dot" と書かれた部分は三箇所あって, その真ん中でした (当然, バージョンアップに伴い三箇所が四箇所, 五箇所になる可能性もありますので, これも参考程度です).

の情報とは違い、__temp2.dot の出現箇所は一か所だけでした。もう真面目に対策するしかない(?)みたいです。

ソースコードをちゃんと見てみる

Graphviz は依存ライブラリ多そうだしビルド面倒だろうなー、とうんざりしてたんですが・・・ここまでの動作で考えると、GVedit はただ dot を呼び出すだけのプログラムのような気が。Graphviz が依存してるいろいろなライブラリを集めなくてもビルドできるんじゃ?

と、ちゃんとソースを確認すると、.cpp と一緒に .bpr ファイルがあって。C++ Builder 6 で作られたただのスタンドアロンアプリケーションみたいで。これなら Turbo C++ でビルドできるぞと、Turbo C++ の環境を準備。

ちなみに今回知ったんですが、無償公開版の Turbo C++ Explorer(日本語版)って 2009-08-26 に日頒布終了してたんですね・・・。

当時これをダウンロードしてユーザ登録してる人じゃないと使えないってのが残念ですけど、一応ビルド方法とかについても書いておきます。

ビルド方法

.bpr ファイルの編集

普通にビルドすると .obj ファイルが見つからなくてエラーになってしまいますが、これは Turbo C++ ではビルドディレクトリとして Debug_Build や Release_Build が設定されて .obj がこのディレクトリに吐かれる一方で、プロジェクト設定でカレントディレクトリの .obj が使われるよう設定されているためです。

そもそも .obj をプロジェクトに追加する必要はないはずなので、あらかじめ GraphX.bpr 68 行目付近から始まる

      <FILE FILENAME="USettings.obj" FORMNAME="" UNITNAME="USettings.obj" CONTAINERID="OBJTool" DESIGNCLASS="" LOCALCOMMAND=""/>
      <FILE FILENAME="GraphX.obj" FORMNAME="" UNITNAME="GraphX.obj" CONTAINERID="OBJTool" DESIGNCLASS="" LOCALCOMMAND=""/>
      <FILE FILENAME="UAbout.obj" FORMNAME="" UNITNAME="UAbout.obj" CONTAINERID="OBJTool" DESIGNCLASS="" LOCALCOMMAND=""/>
      <FILE FILENAME="UEditor.obj" FORMNAME="" UNITNAME="UEditor.obj" CONTAINERID="OBJTool" DESIGNCLASS="" LOCALCOMMAND=""/>
      <FILE FILENAME="Umain.obj" FORMNAME="" UNITNAME="Umain.obj" CONTAINERID="OBJTool" DESIGNCLASS="" LOCALCOMMAND=""/>
      <FILE FILENAME="UPreview.obj" FORMNAME="" UNITNAME="UPreview.obj" CONTAINERID="OBJTool" DESIGNCLASS="" LOCALCOMMAND=""/>

を削除しておきましょう。(プロジェクトマネージャで「プロジェクトから削除」を選んでも同じことが行えるはずなんですが、なぜか GraphX.cpp が破損してしまうようでなので。)

アイコンの設定

GraphX.res がバージョン管理対象になっていないので、作成した .exe は Turbo C++ のデフォルトのものになってしまいます。

もし配布用の .exe をビルドしたい場合は、ResouceHacker 等で GVedit.exe のアイコンを抽出しておいて、プロジェクト→オプション→アプリケーション→アイコンの読み込みでそのアイコンを指定する必要があります。

この手順で作った GraphX.res を置いておきますので、面倒でしたらこれを GraphX.bpr と同階層に置いてください。

Turbo C++ で .bpr ファイルを開く

プロジェクトの変換が行われ、GraphX.bdsproj 等が作られます。上記手順を踏んでおけば、とりあえずビルドは通るようになっているはずです。

ビルドすると GraphX.exe という名前で実行ファイルが作られます。必要であれば GVedit.exe にリネームして利用する感じで。

パッチの適用

上記のバグ修正を行いました。とりあえず 2.27.20110301.0545 ベースでパッチを書いたのでどうぞ。

パッチ適用の際、Windows の patch だと

patch -p1 -i fix_preprocessor.patch --binary

みたいに --binary が必要でした。

修正点は以下のような感じです。

  • Preprocessor Settings の設定が正しく反映されるよう修正
  • テンポラリファイルの扱いを見直し
    • すべて %TEMP%\__temp.dot を使う形に変更しました(Preprocessor 実行時は標準入出力経由でデータをやりとりしていたので __temp2.dot は不要でした)
  • Preprocessor 呼び出し後にテンポラリファイル末尾にゴミが残ってしまう問題を修正
    • パイプから \0 で終端されていない char* で読み込んでそれを StringList 末尾に追加していたので、メモリ上の余計な文字列がくっついていたみたいです
    • SetLength() で余計な文字をカットすることで対応しましたが、StringList.Text に対して実行しても効果がなかったので、一時的に AnsiString に格納する形にしました

全体的に「いかにもプログラミング初心者が書きました」という感じのソースで、いろいろ手を入れたい部分はあるんですが・・・なるべく変更点が少なくなるように対応しています。大文字/小文字が異なるだけの同名の変数があったので、そこは変数名変えちゃいましたけど。