Gradle, ProGuard, intellij-plugin周りの備忘録

intellij plugin開発時に難読化したbuildを生成しようとした時の備忘録。

流れ

  • intellij marketplaceドキュメントを見て、ProGuardがいいかなと適当に判断
  • ざっとググったらandroid関係の記事ばかりでよくわからなかったのでProGuardドキュメント
  • そもそもgradleの書き方わからないし、javaのcompileからbuild生成の流れもわからないと厳しそうだったので遠回り。
  • gradleのドキュメント でgradleの基本とjava pluginについての記事を読む
  • java pluginのタスクを順に実行してみてcompile - buildの流れを把握
  • proguardのタスクをどう組み込めばいいかを gradle-intellij-pluginのコードを見ながら検討
  • 一旦proguardタスクを単体で実行できるかtry
  • gradleのドキュメントをなんども見返しながら、なんとかproguard taskをbuildに組み込み

理解したことmemo

  • defaul scopeがproject
  • projectがビルドのベース。multi projectというのもあるらしい。
  • java pluginがprojectを拡張している
    • sourceCompatibility とかはjava pluginが拡張したもの
  • manualの見方

    • DSL Referene Home に主要なclassの説明がある
    • 落ち着いて時間をかけて読んで大体わかるようになった
  • configurations

    • implements fileList
      • content is dependency files(because configurations is dependency configuration)
      • Railsっぽい魔法感がある
      • ビルドツールとしてのeasyさを追求している感じ
      • 最初は訳がからないけど少しわかってきたら便利
  • configuration fase and execution fase

  • file path convention

  • life cycle task

    • Base Plugin が提供するtaskに色々紐付ける形になっている
    • build.buildintellij.buildPlugin どっちでも同じようにbuildできるのが不思議だった
  • task間のやりとり

    • dependeOnで指定したtaskから色々情報が得られる
      • taskの取得はdocumentのTaskContainerを見れば大体わかる
      • AbstractTaskのpropertyを使う
      • 必要であればsubTypeのcastして固有のpropertyを使う
    • configurations.compile.collect {} のように、configurationsのdependencyからとるようなケースもある
  • java compile flow outline

    • what is fat jar
    • compile java to class files
      • jdk
      • library jars
    • jar
      • class files
      • resources
    • build
  • proguard

    • ${javaHome}/libs/rt.jar
    • shrink, optimazeするため、dependenciesのjarを指定する必要があって、適切に指定しないとwarnが出る
    • obfuscateだけしたいなら dontwarn, dontshrink, dontoptimize を指定すれば、 jarを指定しなくてもok
      • depedency jarを省くとdependency内のclassをoverrideしたメソッド名も変更されてしまって壊れる(dependecy jarを指定して試してないからもしかして関係なく壊れる?)
        • keepを指定することで避けられる。例えば下記のような
keepclassmembers '!public class * { \
    public protected <methods>; \
}'

振り返り

  • Gradle魔法感があってとっつきにくそうと思ったけど、ドキュメントが丁寧に書かれていたので、ちゃんと読めばなんとか理解できた
  • 既存のbuildの流れにちょっとしたタスクを組み込みたいだけでも色々理解しなければならないことが多くて大変だったけど、結局理解していく過程は充実感があるから、これもプログラマの醍醐味?
  • 難読化も魔法じゃない

gomのトレース

https://github.com/mattn/gom

パッケージマネージメントツール プロジェクトの依存するpackage(revisionを含む)を管理できる。 go標準のパッケージマネージメントツールがない時からあって、結構な利用者がいそう。

概要

データとしては、プロジェクト毎の依存パッケージ情報を記載するGomfileと、依存パッケージデータを格納するvendorディレクトリがある。

buildやtestなどのgoコマンドをラップしているgomコマンドが使えるようになり、gom経由で実行すると各プロジェクトのvendorディレクトリも認識されるようになる。

技術的要素

  • Gomfileの生成

    • プロジェクトをscanして、 build.Import で依存packageのパスを取得できる
    • build.Package.Goroot(bool)build.IsLocalImport でvendor packageかどうかを判別できる
    • 上記のパスを元に、GOPATH以下のpackage dirに移動して、 git rev-parse HEAD でrevisionを取得できる
  • Gomfileをparseするときの正規表現

    • 各最小単位の要素ごとに正しく定義して、それを組み合わせることで、一般的に読みにくい、理解しにくい正規表現をなるべくメンテナブルに。
    • gom ('repository name') や optionsの :(option name) => (option list or option name)
    • 行の種別ごとの正規表現を正しく定義すれば、最低限の正規表現の標準メソッドを使うだけで、シンプルにparserが実装できそう
    • スペースやカンマとかの区切りとか、正規表現で正しく表現するのはややこしそうだけど、いくつかのパターンのテストを用意しておけば、数回の試行錯誤で割と網羅的な表現が書けそうな気がする
  • install

    • GOPATHに ./vendor 設定した上で、 go get package することで、 ./vendor にpackageを持ってきている

memo

  • goの例外処理でひたらすerrorを返していくやり方は解り易くて良さそう
  • 各種変数や関数の定義が色々なファイルに散らばってしまっている感があるけど、これぐらいの規模なら問題ない?(普通にIDEを使えば実際読み解きずらいとは感じなかった)
  • コンソール出力の色付け github.com/daviddengcn/go-colortext 便利そう

main.go

  • flagArgsをsubArgsに切り出したり、flagArgs(0) あたりの処理、最初の引数がコマンドで、それ以降がコマンドごとの引数だということが解りやすい
  • コマンドのswitch文が読みやすい
  • genコマンドのswitchの入れ子、2階層ぐらいなら全然問題ない

exec_test.go

  • run経由で go envを実行して取得したgopathの中に、tmp pkgのvendor dirが含まれているかをチェック。
  • gomはgo ** 系のコマンドをラップしていて、PATHやGOPATHをvendorが読み込まれるようにしている。
    • それがちゃんと指定されているかをテストしている。
  • defer func(){...}() という書き方
  • ioutil.Temp**系の関数を使うと面倒そうなpkg dirや標準出力のparse系のテストが割と簡単に書ける?
  • cmdに渡すstdoutを、直接os.Stdoutを使うのではなく、変数のstdoutを経由させることでテストが可能になっている。

genGomfile

  • build.importsを再帰処理して、カジュアルに外部パッケージの一覧が取得できる
  • vcsのrevision番号の読み取りを正規表現のmaskとして実装しているのは直感的でわかりやすい
  • vcsのcommandをsliceで表現している。goはsliceの切り取りが直感的だからexec.Commandの受け渡しもシンプルに書ける
  • 簡易的な struct vcsで、vcs毎の差異を隠している

gopwtのトレース

概要

power assertのgolang版のgopwtのトレース

github.com

どうやったか

使い方と全体ディレクトリ構造とコードをざっくり見た後に、インクリメンタルに動作させつつコードを書き写していった。

gopwtの使い方

main_test.go

func TestMain(m *testing.M) {
    flag.Parse()
    gopwt.Empower()
    os.Exit(m.Run())
}

func TestHoge(t *testing.T) {
    assert.OK(t, 2 == inc(2), "yeah!")
}

func inc(a int) int {
    return a + 1
}

上記を実行すると

--- FAIL: TestHoge (0.00s)
    assert.go:85: FAIL trace_gopwt_test.go:18
        assert.OK(t, 2 == inc(2), "yeah!")
                       |  |
                       |  3
                       false
        
        Assertion messages:
                - yeah!
        
        --- [int] inc(2)
        +++ [int] 2
        @@ -1,1 +1,1@@
        -3
        +2

のような形で出力される。

empowerをしないと下記のようになる

--- FAIL: TestHoge (0.00s)
--- FAIL: TestHoge (0.00s)
    assert.go:37: [FAIL Assertion] assert.OK(t, 2 == inc(2), "yeah!")
        
        Please call gopwt.Empower() in your TestMain(t * testing.M).It give you power.
        If you need more information, see http://github.com/ToQoz/gopwt
        
        AssertionMessage:
                - yeah!

どのように実現させているか?

empower内でやっていること

tmp dirにpackageを複製、調整し、 tmp dir上で exec.Command("go", "test") を実行している。 (package複製時に empower() は取り除いているので無限ループしない(取り除かないと無限ループするようなやり方で実現させているということ))

package複製時にやっていること

assert.OK を powered表示用のメソッド translatedassert.OK に書き換える。

その時に、translatedassertで使う色々な細かい情報を作っている。

translatedassert.OK のメソッドの定義

func OK(t *testing.T, e bool, messages []string, header, filename string, line int, origexpr string, termw int, expectedPos, gotPos int, pvPairs ...posValuePair)

実際に書き換えられたコード

translatedassert.OK(
    t, 
    2 == translatedassert.FRVInterface(translatedassert.MFCall("{path to tmp dir}}/trace_gopwt_test.go", 24, 912, translatedassert.RVOf(inc), translatedassert.RVOf(2))),
    []string{"yeah!"}, 
    `FAIL`,
    `trace_gopwt_test.go`, 
    24, 
    `assert.OK(t, 2 == inc(2), "yeah!")`, 
    236, 
    19, 
    14, 
    translatedassert.NewPosValuePair(14, 2, false, `2`), 
    translatedassert.NewPosValuePair(16, 2 == translatedassert.FRVInterface(translatedassert.MFCall("{path to tmp dir}}/trace_gopwt_test.go", 24, 912, translatedassert.RVOf(inc), translatedassert.RVOf(2))), true, `2 == inc(2)`),
    translatedassert.NewPosValuePair(19, translatedassert.FRVInterface(translatedassert.MFCall("{path to tmp dir}}/trace_gopwt_test.go", 24, 912, translatedassert.RVOf(inc), translatedassert.RVOf(2))), true, `inc(2)`), 
    translatedassert.NewPosValuePair(23, 2, false, `2`)
)

下記の4つがテスト用の式を分割した形で表示させるための大事な情報。 表示調整用のpositionのint値や、元の式のstring、式を評価した値などが作られている。

設計メモ

プロジェクトを分割すると、

  • テストをassert.OKの形式で書けるようにするところ/assert
  • power assert形式で、FAILした情報を見やすく表示してくれるところ /translatedassert
  • 元のテストファイルを解析し、必要な情報とともに assert.OKtranslatedassert.OK にpackageを書き換えるところ /translator

やってみてメモ

  • テストの出力部分をごにょごにょ調整しているだけだと思ったらとんでもなかった
  • goでgoのプログラムをメタ的に扱う機能が充実してそう

    • go fileをparseして一部書き換えるとかが割とカジュアルにできそう
  • ast nodeの再帰的な処理(子ノードや、ノードの種類に適応させた処理)が結構あって参考になりそう

  • typeのassertionやswitchでの処理がかなりあってgo的な書き方の参考になりそう
  • translatedassert向けのpkgはかなり大仕事でコード量が多いけど、 translator/internal 内に閉じ込めているので、プロジェクト全体の見通しが良い

    • /main.goからは translator.Translate()を呼び出して、作成したpkg上でexec.Command("go", "test") を実行するだけ
  • pkgを複製する処理の絡みでpathを色々調整する処理が結構複雑で理解が進みずらいところがあった

    • PackageContextにわかりやすい名前でメソッドを定義したら解りやすくなりそう?
  • PackageContextでのpkgを扱う処理を CopyPackage, ReadPackage, TypecheckPackage, RewritePackage に分割していて解りやすかった

    • まとめてやろうとしたらかなりカオスになってしまいそう
    • 複雑な大きな処理をどういった視点で分割するかの判断は難しそう
    • dependency pkgのcache周りの処理は理解しきれていない

fuelphpのvalidationについて

filedsetに対して定義できるものと、modelに対して定義できるものがある。

Fieldsetのvalidation

Fieldsetと1対1で紐付いたvalidationインスタンスのrunメソッド(return bool)を呼び出す。 $fieldset->validation()->run()

ruleの設定

各fieldに対して個別にadd_ruleで設定可能。

デフォルトで用意されているルールは下記サイトの下部を参照

http://fuelphp.jp/docs/1.9/classes/validation/methods.html

メッセージの設定

下記のいずれかの方法で可能

  • app/lang/ja/validation.php に書く
  • Validationインスタンスに対して設定する $fieldse->validation()->set_message(‘ルール名’, ‘メッセージ’)
  • 各fieldに設定する $fieldset->field(‘field名’)->set_error_message(‘ルール名’, ‘メッセージ’)

カスタムルール

validationインスタンス_find_ruleメソッド内で、 $thi->callablesに設定されているオブジェクトの prefixが _validationのメソッドを探しいく。

デフォルトではvalidationインスタンス自身が設定されているので、Fuel\Core\Validation 内のprefixが_validationのメソッドが適用できるようになってい る。

callablesはvalidationインスタンスのadd_callableで追加できるので、_validation_*** が定義されているクラスであれば何でもok(Fieldset自身とか、専用の Validation_Callable_Hoge のようなクラスを作ったりとか)

またadd_callableで設定しておかなくても、validationインスタンスのrunメソッドの第三引数に$temp_callableを渡せる。

modelのvalidationでカスタムルールをモデル内に定義できるのもこれで実現させてる。

https://github.com/fuel/orm/blob/1.9/develop/classes/observer/validation.php#L232

modelのvalidation

ミニマムのサンプル

<?php

class Model_Post extends \Orm\Model
{
    protected static $_properties = array(
        'id',
        'title' =>  [
            'validation' => [
                'required',
                'max_length' => [1],
                'contains_hoge'
            ]
        ],
    );

    protected static $_observers = array(
        'Orm\Observer_Validation' => ['events' => ['before_save']]
    );

    protected static $_table_name = 'posts';

    public function _validation_contains_hoge($val)
    {
        return (bool)preg_match('/hoge/', $val);
    }
}
  • $_propertiesvalidationに設定できる
  • _validation_*** という命名規則でmodelにカスタムバリデーションを定義できる
  • 上記の指定だと before_save 時にバリデーションが実行される
  • fail時は Orm/ValidationFailed がthrowされる
  • errorメッセージは app/lang/validation.php でruleごとに定義できる(model内では設定できないぽい)
  • 全体の表示の用のメッセージは $e->message で取得(フォーマットはConfigのvalidationで設定可能)
  • エラーの個別の取得は $e->get_fieldset() でmodelから生成したFieldsetが取得できるので、Fieldsetのインターフェース経由で行える。
    • $e->get_fieldset()→error('title')

仕組み

modelのpropertiesの設定から動的にFieldsetを生成し、Fieldsetのvalidationの機能を利用して実現させてる。

bootstrapのモーダルを複数開いたときの問題と対処法

bootstrapのモーダルは複数重ねて開いたときにいくつか細かい問題が発生する。 versionは3系。

2つめのモーダルの背景(.modal-backdrop)より1つめのモーダルが上に表示されてしまう

標準の挙動

背景(.modal-backdrop)はmodalごとに、bodyにappendされる(z-indexは.modalのz-index設定値 - 10)

z-indexのスタック文脈について詳しい記事

z-index再入門 - z-indexの仕組み | CodeGrid

問題点

モーダルの数、重なりに関係なく .modal.modal-backgroupより上に表示される。

対処法

.modal.modal-backdropのz-indexをstackされている要素を考慮するようにhackする

$(document).on('show.bs.modal', '.modal', function () {
    var zIndex = 1040 + (10 * $('.modal:visible').length);
    $(this).css('z-index', zIndex);
    setTimeout(function() {
        $('.modal-backdrop').not('.modal-stack').css('z-index', zIndex - 1).addClass('modal-stack');
    }, 0);
});

bodyスクロールの調整

標準の挙動

body.modal-openoverflow:hidden を指定することで、モーダルが開いているときの、body要素のscrollを無効化している

問題点

.modal-open はbodyに対して設定しているので、モーダルが複数ある場合は考慮されていない。 なので、2つめのモーダルを閉じたときに body.modal-open が外れてしまい、まだ開いているモーダルがあるのにbodyのスクロールが有効になってしまう。

対処法

モーダルを閉じた後のeventで、まだ開かれているモーダルがあるときは、再度 body.modal-openをつける

$(document).on('hidden.bs.modal', '.modal', function () {
    $('.modal:visible').length && $(document.body).addClass('modal-open');
});

right paddingの調整

標準の挙動

モーダルを開く際に、bodyに縦スクロールが発生している場合、.modal.modal-dialogではない)に対してスクロールバーの幅分paddingRightを指定する。 結果モーダルはスクロールバーの幅分を除いた表示領域内でセンタリングされる。

問題点

2つめのモーダルを開く際はbodyに overflow: hidden が指定されているため(body.moda-openによって)スクロールバーが発生していない判定になり、paddingRightが設定されない。 結果、2つめのモーダルが若干右にずれる形になる。

また、1つめのモーダルで縦スクロールが発生している場合、1つめのモーダルはスクロールバー幅分を除いた領域でセンタリングされるが、2つめのモーダルはスクロール幅分を考慮しない領域でセンタリングされるので、2つめが若干右にずれる。

対処法

うーむ。。

参考

stackoverflow.com

app.codegrid.net

デザインパターン入門【マルチスレッド編】備忘録

android開発を少しすることになってマルチスレッド周りの知識が残念な感じだったので、良い本だと噂の

Amazon CAPTCHA

を読んだので備忘録。

メモ

  • synchronizedがあったら何か守るべきものがあると考える
  • InterruptedException
    • throws InterruptedExceptionがついているメソッドは以下のように認識すると良い
      • 時間がかかる
      • キャンセルできる
    • Thread.interruptメソッドは直接InterruptedExceptionを投げるわけではなく、Threadをinterrupt状態にするだけ
    • Threadがwait,sleep,joinなどの待ち状態のときにinterrupt状態を確認していて、interrupt状態の場合にInterruptedExceptionを自ら投げる
    • 通常の処理を行っているときに急にInterruptedExceptionが発生することはない

パターン図

いくつかパターンをピックアップして、PlauntUMLの練習がてら図で表してみる。

続きを読む

vuexでのデータの扱いについて少し困ったので整理

目的

vuexを使っているある程度複雑なvueコンポーネント上で、定数的なものをどう扱っていいか解らなかったので、一旦考えを整理しておきたい。

現状の整理

なぜvuex?

コンポーネントの階層が4つまであって、propsだけで実現しようとすると、rootコンポーネントのpropsを最下層まで渡していくのが面倒だし、挙動や設計を少し変えようとした場合の変更箇所も増えるので大変。

vuexを使うと、storeを共有できるので、最下層のコンポーネントからも直接storeからstateを参照、更新(mutationの実行)ができるので楽。

vuexのデメリット?

コンポーネントが依存する先が増えるので、挙動を理解するのが少し大変になるのと、再利用性は低くなる。

props, data, storeそれぞれの役割を理解して使わないとカオスになる(なった。経験値が必要) 勿論ちゃんと整理して使えばかなり便利。

props, data, store

何に困ったか?

今回作っていたのはがっつりSPAという感じではなく、phpで動いているwebサービスのある一画面をSPA的に実装する形。

なので、

  • php側からjsに各値を渡す
  • 渡された値をstoreのデフォルト値として設定

という感じ。

php → js に渡したいデータとして、state的なもの以外にphp側では定数として定義されているデータも渡す必要があって、割と大事なデータで色々なコンポーネントから参照される。

window.constants のように定義すればいけるけど、グローバル変数に依存するのは何か違くない?と思い及び腰に。

storeに突っ込んでおけば、 mapStateでどのコンポーネントからでも必要があれば簡単に参照できるし、依存先もstoreだけになるかあらまあいいか。。と思いそうしたけどなんか違うよな。。と。(じっくり考える余裕がなかった反省)

考察

storeに突っ込むのは何がいけない?

storeはアプリ全体でリアクティブなフローを実現するための仕組みなので、state的なデータを保持するためのものなのでそもそも使い方が間違っている。

storeにconstants的なデータも入れてしまうと、stateとconstantsが入り乱れてしまうので、何が動的に変わる想定のデータで、何がimmutableな定数的なデータとして想定されているものか解らなくなってしまい、コードを読み解くのが困難になってしまう。 (読み解くの難しいコードはどんな状況であれ、避けるべきで良くないもの)

どうすべきだったか?

  • constants用のclassを定義する
    • 名前をちゃんと付けることで、windowに直接入れ込むより用途が明確になる
    • 各プロパティのgetterを定義することで、他にどういったデータがあるか解りやすくなる
  • php側から渡されるjsonをloadする関数 loadJson をexportして、loadしたらfreezeされる
    • phpから渡されるデータをload後にimmutableになる」という設計の意図が解りやすくなる
class HogeConstants
{
    constructor(json)
    {
        // json parse ..
    }

    get a() {
        return this.a
    }

    get b() {
        return this.b
    }
}

let instance

export function loadJson(json)
{
    instance = new HogeConstants(json)
    Object.freeze(instance) // singleton pattern https://www.sitepoint.com/javascript-design-patterns-singleton/
}

export function getHogeConstants()
{
    return instance
}

phpとの連携部分のjs部分で、 loadJson を呼び出しあげて、 あとはこのデータを使いたいコンポーネント側でimportすれば参照できるようになる。

アプリの規模によってはwindowの直接設定しても問題にならないこともある気がする。

あんまりvuexどうたらは関係なく、jsもある程度の規模になったらある程度設計を考えないと、という話?