2019年6月23日 (日)

続:BSoDとの戦いに挑む

ネイティブモジュールのデバッグに挑む

先日のポストでToDoにしていた、デバッガのセットアップとかをやってみた。

いまどきのデバッガ

Debugging Tools for Windows というやつを使うんだが、こいつにはMicrosoft Store版があって、最新のWindows 10の場合そちらのほうがより近代的なんだそうな。

Windbg Preview in Microsoft Store

ストアアプリなので、導入自体はいたって簡単。ストアからアプリを入れたら、アクセスしやすくするためスタートメニューにピン止めしておく。

一方、ダンプサイズの拡張については、最近のWindowsはやらなくてもそれなりのサイズを確保してくれるらしい。設定自体は以前と同じシステムのプロパティの中の「起動と回復」の中にある。

Boot_and_recovery

デフォルト値が「自動メモリダンプ」になっていて、メモリを16GB搭載しているこのPCでは、だいたい1GB~1.5GB程度のダンプを確保してくれるようになっている。
昔のデフォルト値は「最小メモリダンプ(256KB)」だったので、ダンプから自力でどうにかしようと考えているような人にはいい時代になった、と言える(?)

実際にデバッガにかけてみる

実のところ、よく発生しているBSoD 2種が先日の記事を書いた後で運よく(?)発生したので、ダンプを確保して分析にかけてみることにした。
WinDbgを起動して、確保したダンプを読み込ませると、自動的に簡易分析が始まる。デバッグシンボルの取得は勝手にやってくれるようだ。

0xd1_simple_analysis

画面中の !analyze -v のリンクを押すと分析が始まる。
以下は DRIVER_IRQL_NOT_LESS_OR_EQUAL で BSoD になったやつを分析したときの出力だけど、長いので途中省略。 vmswitch というモジュールがトリガーになったようだ。

0: kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

 

DRIVER_IRQL_NOT_LESS_OR_EQUAL (d1)
An attempt was made to access a pageable (or completely invalid) address at an
interrupt request level (IRQL) that is too high.  This is usually
caused by drivers using improper addresses.
If kernel debugger is available get stack backtrace.
Arguments:
Arg1: 0000000000000548, memory referenced
Arg2: 0000000000000002, IRQL
Arg3: 0000000000000000, value 0 = read operation, 1 = write operation
Arg4: fffff808c961972b, address which referenced memory

 

Debugging Details:
------------------
  :
(中略)
  :
FOLLOWUP_IP: 
vmswitch!VmsQueueGroupIsRestrictedQueue+7
fffff808`c961972b 8b9048050000    mov     edx,dword ptr [rax+548h]

 

FAULT_INSTR_CODE:  548908b
SYMBOL_STACK_INDEX:  3
SYMBOL_NAME:  vmswitch!VmsQueueGroupIsRestrictedQueue+7
FOLLOWUP_NAME:  MachineOwner
MODULE_NAME: vmswitch
IMAGE_NAME:  vmswitch.sys
DEBUG_FLR_IMAGE_TIMESTAMP:  62acf328
STACK_COMMAND:  .thread ; .cxr ; kb
BUCKET_ID_FUNC_OFFSET:  7
FAILURE_BUCKET_ID:  AV_vmswitch!VmsQueueGroupIsRestrictedQueue
BUCKET_ID:  AV_vmswitch!VmsQueueGroupIsRestrictedQueue
PRIMARY_PROBLEM_CLASS:  AV_vmswitch!VmsQueueGroupIsRestrictedQueue
  :

そして、もう一つ。 KERNEL_SECURITY_CHECK_FAILURE で BSoD になったやつを分析してみる。

Loading Dump File [C:\work\memdumps\0x139_vmswitch-sys_20190622-03.DMP]
Kernel Bitmap Dump File: Kernel address space is available, User address space may not be available.

 

 

************* Path validation summary **************
Response                         Time (ms)     Location
Deferred                                       srv*
Symbol search path is: srv*
Executable search path is: 
Windows 10 Kernel Version 17134 MP (4 procs) Free x64
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 17134.1.amd64fre.rs4_release.180410-1804
Machine Name:
Kernel base = 0xfffff803`254b0000 PsLoadedModuleList = 0xfffff803`2585d170
Debug session time: Sat Jun 22 22:09:48.984 2019 (UTC + 9:00)
System Uptime: 0 days 5:39:39.842
Loading Kernel Symbols
.........................................Page 20008bef5 too large to be in the dump file.
Page 2000aadf4 too large to be in the dump file.
......................
................................................................
................................................................
...............................
Loading User Symbols

 

Loading unloaded module list
..............
For analysis of this file, run !analyze -v
nt!KeBugCheckEx:
fffff803`2565aab0 48894c2408      mov     qword ptr [rsp+8],rcx ss:0018:ffffda05`86612940=0000000000000139
2: kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

 

KERNEL_SECURITY_CHECK_FAILURE (139)
A kernel component has corrupted a critical data structure.  The corruption
could potentially allow a malicious user to gain control of this machine.
Arguments:
Arg1: 0000000000000003, A LIST_ENTRY has been corrupted (i.e. double remove).
Arg2: ffffda0586612c60, Address of the trap frame for the exception that caused the bugcheck
Arg3: ffffda0586612bb8, Address of the exception record for the exception that caused the bugcheck
Arg4: 0000000000000000, Reserved

 

Debugging Details:
------------------
  :
(中略)
  :
FOLLOWUP_IP: 
vmswitch!VmsQsUpdatePerProcessorPacketStats+2c3
fffff80b`e4a67be3 cd29            int     29h

 

FAULT_INSTR_CODE:  8d4829cd
SYMBOL_STACK_INDEX:  4
SYMBOL_NAME:  vmswitch!VmsQsUpdatePerProcessorPacketStats+2c3
FOLLOWUP_NAME:  MachineOwner
MODULE_NAME: vmswitch
IMAGE_NAME:  vmswitch.sys
DEBUG_FLR_IMAGE_TIMESTAMP:  62acf328
STACK_COMMAND:  .thread ; .cxr ; kb
BUCKET_ID_FUNC_OFFSET:  2c3
FAILURE_BUCKET_ID:  0x139_3_CORRUPT_LIST_ENTRY_vmswitch!VmsQsUpdatePerProcessorPacketStats
BUCKET_ID:  0x139_3_CORRUPT_LIST_ENTRY_vmswitch!VmsQsUpdatePerProcessorPacketStats
PRIMARY_PROBLEM_CLASS:  0x139_3_CORRUPT_LIST_ENTRY_vmswitch!VmsQsUpdatePerProcessorPacketStats

おや? こちらも vmswitch がトリガーになっているようだ。

問題になっているモジュールが特定できたので、次はワークアラウンドの検索かな....。

| | コメント (0)

2019年6月21日 (金)

BSoDとの戦いに挑む

ここ最近ひどい

僕が普段使いしている Panasonic CF-B11 というノート PC なんだけど、ここ最近不定期に BSoD を出してくる。
どうにかしなきゃね、ということで、調べてみることに。

イベントログを読む

Windows という OS はぎりぎりまできっちりイベントログを収集していて、それは BSoD による強制再起動も例外ではない。ということは割と知られていることなので、とりあえず直近の2か月ほどを抜いてみる。

BSoD イベントは、 System イベントログ内に BugCheck というソース名で記録されるので、 Windows PowerShell でとりあえずヘッドラインだけでも出してみる。

PS > Get-EventLog -LogName System -After "2019/05/01" | Where-Object {$_.Source -eq "BugCheck"}

   Index Time          EntryType   Source                 InstanceID Message                                                                            
   ----- ----          ---------   ------                 ---------- -------                                                                            
  157766 6 20 06:47    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  156383 6 17 21:48    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  151168 6 08 16:26    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  148570 6 08 15:11    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  146691 6 05 20:38    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  145460 6 03 22:35    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  144162 6 02 20:54    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  137197 6 01 22:18    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  136141 6 01 07:49    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  133441 5 26 19:51    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  133303 5 26 13:27    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  132988 5 25 21:32    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  132631 5 25 19:19    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  131648 5 24 23:02    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  130682 5 22 22:24    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  129735 5 20 21:04    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  129301 5 19 15:18    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  128834 5 19 11:44    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  128416 5 18 22:42    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  126350 5 13 22:47    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  125399 5 12 10:24    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  125194 5 11 23:39    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  123956 5 08 13:52    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  121016 5 02 23:33    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  120450 5 02 15:58    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...
  119397 5 01 19:09    Error       BugCheck               1073742825 ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DL...

あー、このアウトプットではだめだ、ということで、 Format-List に通してみたところ、大半がこんな感じ。

Index              : 120450
EntryType          : Error
InstanceId         : 1073742825
Message            : ソース 'BugCheck' のイベント ID '1073742825' の説明が見つかりません。必要なレジストリ情報またはメッセージを表示するメッセージ DLL ファイルがローカル コンピューターに存在しない可能性があります。または、これらのデータへのアクセス
                     許可がユーザーに与えられていない可能性があります。次の情報はイベントの一部です:'0x00000139 (0x0000000000000003, 0xfffff801f3072950, 0xfffff801f30728a8, 0x0000000000000000
                     )', 'C:\WINDOWS\MEMORY.DMP', 'b537a1cf-30ea-4911-be63-13624e70de94'
Category           : (0)
CategoryNumber     : 0
ReplacementStrings : {0x00000139 (0x0000000000000003, 0xfffff801f3072950, 0xfffff801f30728a8, 0x0000000000000000), C:\WINDOWS\MEMORY.DMP, b537a1cf-30ea-
                     4911-be63-13624e70de94}
Source             : BugCheck
TimeGenerated      : 2019/05/02 15:58:00
TimeWritten        : 2019/05/02 15:58:00
UserName           : 

ReplacementStrings 行をみると、バグチェックコードが 0x139 だとわかった。先頭の16進数がそれだ。
MSDN の Bug Check Code Reference によれば、

カーネルが重要なデータ構造の破損を検知した
んだそうな。[出展]

加えて、先頭から2番目の16進数は「破損のタイプ」だそう。前述の MSDN の記事では、

A LIST_ENTRY was corrupted (for example, a double remove). For more information, see the following Cause section.

LIST_ENTRY というのは、リスト構造のデータを扱う構造体のことのようだ。

むぅ、構造体そのものではなくて、そいつを操作している処理を特定しないといけないようだ。ここから先の追跡は、デバッガ使ってダンプを眺めてみるしかなさそうだけど、デバッガって Visual Studio のどのコンポーネントに入ってるんだろう?

次の ToDo

  1. デバッガのセットアップ
  2. ダンプの取得サイズを1段階上げる設定に変える

くらいかな....?

| | コメント (0)

2019年4月21日 (日)

MySQL素人がWindows版で3rd partyユーティリティからの接続を使えるようにした話

終わってみればどうということはないけれど

 

正解に行きつくまで、正直なところ相当苦労した。いや、ぼくの MySQL 力がしょぼいせいだけど。

 

何が問題だったのか

 

かいつまんで書くとこんなところ。

 

  • 開発にも使っている自分の Windows PC に、 MySQL Community Edition 8.0 をインストール
  • 起動モードは Windows Service に設定 (Window なので)
  • 同時にインストールできる MySQL Workbench から接続に行くと普通に使える(まぁ、当たり前)
  • 普段は JetBrains DataGrip を使っているので、 DataGrip からローカルホストの MySQL に接続しようとすると「タイムゾーンがおかしい」といって接続できない

 

やったことの備忘録

 

大きくわけて二つ。

 

  1. タイムゾーン設定のインポート
  2. 起動時に参照されるタイムゾーン設定の作成

 

1. タイムゾーン設定のインポート

 

「そう言えば、タイムゾーンって my.cnf とかに書くやつじゃなかったっけ?」というわけで、そのあたりをググってみたところ、

 

Windowsの場合はOSにタイムゾーンの設定が無いため、タイムゾーンのファイルをダウンロードする必要があります。

 

などという記述を発見。
「ええ?そうなん?」と思いつつも、公式の MySQL :: Time zone description tables から、 MySQL 5.7以上むけの POSIX 標準 SQL ファイルをダウンロードして実行。

 

これでタイムゾーンの管理情報は作成できたので、起動してみるがダメ。

 

2. 起動時に参照されるタイムゾーン設定の作成

 

ここでどんなタイムゾーン設定が有効になっているのかを調べてみる。

 

mysql> show variables like '%time_zone%';
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| system_time_zone | |
| time_zone | SYSTEM |
+------------------+--------+
2 rows in set (0.01 sec)

 

これはどういうことなのか? と再度調べてみたところ、

 

 

変数名 説明
system_time_zone システムのタイムゾーンで、起動時に TZ 環境変数を参照して決定したあとは、変更されることはない
time_zone サーバのカレントタイムゾーンで、権限があれば後から変更できる。
初期値は SYSTEM で、 system_time_zone と同じであることを表す

 

....とのことのようだ。(出展 : http://download.nust.na/pub6/mysql/doc/refman/5.1/ja/time-zone-support.html)

 

なるほど、 my.cnf を編集しなくても環境変数定義で大丈夫なのか。ということで、オフセット値を設定してみることに。
管理者権限のあるDOS窓を起動して、以下を投入。 MySQL はサービス稼働なので、システム環境変数として登録した。

 

> setx /m TZ +09:00

 

この状態で MySQL を再起動して、再びタイムゾーン設定を見てみると、

 

mysql> show variables like '%time_zone%';
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| system_time_zone | +09 |
| time_zone | SYSTEM |
+------------------+--------+
2 rows in set (0.01 sec)

 

....ということで、無事タイムゾーンが日本に設定されたようだ。

 

この状態で、再び GataGrip から接続を試みると、無事接続できた。

| | コメント (0)

2018年8月 8日 (水)

古いプロジェクトを最新のASで開こうとした話

1. ことのはじまり


先日 Google Play から「2018年11月1日までに公開しているすべてのアプリの compileSdkVersion を API 26以上にしてちょうだい」とのメールが。詳しくは今後の Google Play でのアプリのセキュリティおよびパフォーマンスの改善についてというGoogle Developersのブログ記事を参照。


Oh、これ、対応しないと Ban されるやつだわ....。

ということで、GitHubにあげているソースをcloneして改修に入ることに。

2. 最新のAndroid Studioで読めるようにするには


「読めるようにするには」というよりもむしろ、「Gradleビルドが正常終了するようにするには」というほうが正しい。

いまのAndroidはgradleプロジェクトだけど、 build.gradle の設定はざっくりこんな感じ。

minSdkVersion 15
compileSdkVersion 19
targetSdkVersion 19
Gradle Wrapper 1.10
Android Gradle plugin 0.9
buildToolsVersion 19.0.3


注目すべきは Android Gradle plugin のバージョンで、どうやらこのアプリ、Android Studio 1.0がリリースされる前から開発していたようだw

2-1. Gradle Wrapper をアップグレードする


ここから先はASを使わず、エディタとコンソールで対応する。


Android の Gradle プロジェクトは、そのプラグインと Gradle そのもののバージョンに縛りがあり、これは公式で公開されている。


Android Studio 3.x には Gradle 4.4がすでにバンドルされているので、4.4以上にするのだが、たまたまシステムに Gradle 4.6 をセットアップしていたのでこっちの wrapper タスクを利用することにした。


また、 Gradle を 4.6 にするので、 Android Gradle plugin は 3.0.0 以降にする必要があるのだが、公式のAndroid Plugin for Gradle 3.0.0 への移行にあるとおり Google の Maven リポジトリを使う必要がある。

なので、ルートの build.gradle 中の buildscript に Google の Maven リポジトリを追加して Gradle プラグイン 3.1.x を使う設定を書く。

buildscript {
  repositories {
    google()  // 追加、Google Maven Repository を使う設定
    jcenter()
  }
  dependencies {
    classpath 'com.android.tools.build:gradle:3.1.4'
  }
}


また、appcompat-v7なども同様に Google Maven リポジトリからの取得になるので、dependencies ブロックにも設定を入れる。

Maven リポジトリの参照先をモジュールごとに変える理由はないので、ルートの build.gradleallprojects ブロックを定義する。

allprojects {
  repositories {
    google()
    jcenter()
    mavenCentral()  // jcenterがあれば普通は大丈夫だが、状況に応じて入れる
  }
}


ここまでできたら wrapper タスクを実行する。

$ gradle wrapper


正常終了したら、 Gradle Wrapper のバージョンは4.6になっているはず。

2-2. Android アプリモジュールの調整


今度は app/build.gradle をいじる。

buildToolsVersion を 27.0.3 以降にすればいいのだが、メジャーバージョンが targetSdkVersion の値と一致していないと警告の対象となるので、 targetSdkVersion は自然と同じ値になる。

また、これに引きずられて compileSdkVersionappcompat-v7 のバージョンも27以降にする必要がある。


今回は API 28 にするので、以下のとおりに変更。

android {
  compileSdkVersion 28
  buildToolsVersion '28.0.2'
  defaultConfig {
    targetSdkVersion 28
  }
}
dependencies {
  implementation 'com.android.support:appcompat-v7:28.0.0-rc01'
}


また、プラグインの仕様変更で、最初の行に書く apply plugin も書き換える必要がある。

apply plugin: 'com.android.application'


この状態で、 assembleDebug タスクが正常終了すれば、ASにインポートしてもビルドが通るはずである。


| | コメント (0) | トラックバック (0)

2018年7月23日 (月)

ぼくのかんがえたさいきょうのドキュメントCIかんきょう

1. 前説:こんな書かれ方をするといやなのだ


みなさん、ソフトウェアの設計書ってどうやって書いてます?

....やっぱExcelですよね、この業界。


僕の周囲でもこれはそうなんだけど、個人的には

  • 自分一人で設計を書き、
  • その設計をもとに自分一人で実装し、
  • 実装内容とは無関係な体裁の修正に、十分な時間がかけられ、
  • 版の管理は気にしなくてよい


場合になら、たぶんExcelを普通に使うと思う。


ただ、実際の現場だとそんな悠長なことを言っていられるわけでもなく、人が書いた設計書をもとに実装、なんてことは普通で、場合によってはこんな設計書()に出くわすこともありうる。

Revision_of_mislieading_design


この場合の正解は、商品情報.商品カテゴリが104のとき、商品情報.個数を加算する。なんだけど、不要な情報が一緒に目に飛び込んでくるのは、正直勘弁してほしいと思う。個人的には。


それに、設計書がMicrosoft Office(xlsだと特に)だとこんなことが起きうる。

  1. 共有ディスクに設計書がアップされる
  2. 共有ディスクの設計書ファイルをそのまま開くと、反応が悪い上に場合によってはファイルを破壊する可能性もあるので、作業効率を優先してローカルディスクにダウンロード
  3. 実装作業中の設計書がいつの間にか更新される(アナウンスはなし)
  4. 更新を知らないままローカルファイルで実装を終える
  5. 「設計と挙動が違う」とバグ表を切られる


ほかにもいろいろあるが、設計書がMicrosoft Officeだとこんな弊害が起きる、と個人的には考えている。

  • 版の管理にはOneDrive for Businessとかが必要で、ふつうのVCSでやるのは悪夢でしかない
  • (上に関連するが)リビジョン間で「どこがどう変わったか」を把握するのは、容易なことではないので、一つの最終版ドキュメントにすべての情報を押し込めようとする
  • 集中管理されるべき設計書がカジュアルコピーとして拡散するので、どれが本物かわからなくなる
  • 設計とは本質的に関係のない「体裁の修正」に、少なくはない労力を割かなければならない


ここまでで終わらせたらただの文句でしかないので、どうすればましになるかを考える。

2. 設計書作成ツールとしてのMicrosoft Office


オフィススイートであるMicrosoft Officeでもっとも設計書作成で使われるのは、表計算ソフトであるExcelじゃないだろうか。

表計算をほとんど必要としない文書なのに、なぜExcelを使うのか、僕なりに考察してみた。

  • 日本には昔から「方眼紙文化」があるが、Wordだと方眼紙的なインデントが面倒だが、Excelだとセルを狭めることで方眼紙的なレイアウトを容易に再現できる
  • 複数の独立した文書を、「ワークシート」という単位で一つのファイルにまとめることができる
  • 日本のオフィスPCには、原則Microsoft Officeが入っているので、環境を選ばない。これは、設計書を「納品」しても、それを相手先でも読むことができることを意味する


Excelを使うときの利点を考えたら、僕ではこれくらいしか思いつかない。読者諸兄で、これ以外の利点がご存知ならご教示いただきたい。


個人的には、これらは何もExcelでなくても達成できるものだと思う。

そう考える理由は以下のとおり。

  • 方眼紙的なインデントが必要なのは、「一見して文書の意味付けを把握できるようにしたい」という要求の現れなので、要件に合うような文書構造にすればよい
  • 独立した文書を一つのファイルにまとめたいなら、HTMLでもできる
  • 環境を選ばないファイル形式には、Adobe PDFというものがある
  • 版の管理とリビジョン間の更新差分の管理がMicrosoft Officeだと難しいのは、ファイルフォーマットがバイナリが基本なのが原因なので、原稿をプレーンテキストで記述できれば差分管理はVCSが良しなにしてくれる
  • 単一の原稿から複数のフォーマットの成果物を生成できれば、「開発時は開発サーバ上のHTMLを参照」し、「納品にはPDFを使う(必要であれば印刷してもよい)」という方式がとれる
  • ドキュメントビルダを経由すれば、規定のマークアップ仕様のもとで、体裁は統一された仕様として構造化された文書に紐づくので、「文書体裁の修正」と「ソフトウェアの設計」を明示的に分離できる


前置きが長くなったが、これらを一度に解決する(と僕が考える)ソフトウェアがある。Sphinxである。

3. 本題:ぼくのかんがえたさいきょうのドキュメントCIかんきょう


まずは目標とすることを挙げてみる。

  • 文書の意味付けは標準テンプレートでまかなう
  • 版の管理をVCSに分離する
  • VCSにコミットされた原稿からのビルドは、人手を介することなく自動的に行われるようにする
  • 開発中の設計書が拡散しないよう、Webサーバで参照できるようにする
  • 納品用成果物となる設計書は、PDFにして管理する


いろいろ挙げたが、概念的にはこんな図になる。

Document_ci_overview


というわけで、ひとつひとつ書いていく。

3-1. プロジェクトとVCSの設定


プロジェクトについては、プロジェクトを作るの記事に従って sphinx-quickstart を実行して作るだけなので、特に難しいことはないはず。

VCSも、現状Git以上にベターな選択肢はないので、Gitを選択し、外部のホスティングサービスにおいている。ここでは、プライベート/パブリックの切り替えが簡単なAtlassian BitBucketを使っている。


プロジェクトを作ったら、UTF-8を扱えるエディタでreStructuredTextのマークアップ仕様に基づいて設計書を書いていこう。入門記事もある。

3-2. 面倒なことは有能な執事に任せる


「体裁の修正」や「可読性の高い文書ファイルの生成と配置」といった、本質的に設計と関係ないところで労力を割きたくないので、このテの作業はすべてJenkinsに任せる。

BitBucketがプッシュを受け付けたことをトリガーにしてビルドを始めるのが、反映タイミングとしては最速なのだが、これを実現するためにBitBucketにWebHookを設定する。


まず、Jenkins側にBitBucketからのWebHookを受け付けるためのプラグインを導入する。これにはBitBucket pluginを使った。導入自体は、Jenkinsのプラグインマネージャーから行える。


導入したら、ビルドプロジェクトをつくる。


ソースコード管理はGitにする。BitBucketの仕様に従っていれば特に問題はないはず。

01_source_code_control


次のビルドトリガは Build when a change is pushed to BitBucket を選択する。

02_build_trigger


最後に実際のジョブだが、 make の成果物をディレクトリにコピーするシェルスクリプトを実行する。

Ubuntu の DocumentRoot である /var/www/html 以下に反映する。

#!/bin/bash
HTML_ROOTDIR=/var/www/html/designdoc
UID_TOMCAT=tomcat
UID_WWW=www-data

LANG_CD=ja

HTML_DIST=$HTML_ROOTDIR/$LANG_CD
EPUB_DIST=$HTML_ROOTDIR/epub/$LANG_CD
PDF_DIST=$HTML_ROOTDIR/pdf/$LANG_CD

make clean html epub latexpdf

PS4='+ [${BASH_SOURCE}:${LINENO}] ${FUNCNAME:+$FUNCNAME(): }'
set -vx

if [ ! -e $HTML_ROOTDIR ]; then
mkdir $HTML_ROOTDIR
fi

sudo chown -R $UID_TOMCAT:$UID_TOMCAT $HTML_ROOTDIR

if [ ! -e $HTML_DIST ]; then
mkdir -p $HTML_DIST
fi
rsync -rlDH --delete _build/html $HTML_DIST

if [ ! -e $EPUB_DIST ]; then
mkdir -p $EPUB_DIST
fi
cp -f _build/epub/MySkill.epub $EPUB_DIST

if [ ! -e $PDF_DIST ]; then
mkdir -p $PDF_DIST
fi
cp -f _build/latex/MySkill.pdf $PDF_DIST

sudo chown -R $UID_WWW $HTML_ROOTDIR

set +vx


ただ、 Tomcat の実行ユーザーと Apache の実行ユーザーが違うので、 /etc/sudoers をいじって chown を認証なしで実行できるようにしている。

tomcat	ALL=(ALL:ALL)	NOPASSWD:	/bin/chown


このあたり、もっとましなやりようがないものか。我ながらかなりの力技だ(汗)。


Jenkins側の受け入れ態勢ができたところで、BitBucketのプロジェクトにWebHookを送る設定を書く。

[送信先URL]/bitbucket-hook/を宛先にする。

Webhook_from_bitbucket


ここまでやれば、手元で原稿を書き、それをBitBucketへプッシュするだけで、実装者にも納品担当者にもうれしい設計文書が作られているはずである。


| | コメント (0) | トラックバック (0)

2018年7月20日 (金)

Sphinxの環境を整える

この記事の目標

  • Sphinxの編集環境を作る
  • こぎれいなPDFを出力できる環境をWindowsで作る

Sphinx is 何


SphinxはPythonで書かれたドキュメントビルダで、reStructuredTextというマークアップ言語で書かれたソースをこぎれいなHTMLなんかに変換してくれる優れもの。

Sphinxのインストール


詳しくは公式のSphinxのインストール参照。

以下、かいつまんで書く。

  1. Pythonをインストール、デフォルトでよいが、公式ではPython 3.xを推奨している模様
  2. PIPをインストール
  3. pip install -U Sphinxを実行


編集には、Visual Studio CodeとreStructuredText拡張を使っているが、UTF-8が扱えれば使い慣れたエディタで大丈夫。

こぎれいなPDFをWindowsで作る


実はここがこの記事のメイン。


とりあえず、自分で試したWindowsとLinux(Ubuntu)で必要だったパッケージについて、ざっくりまとめた。

ビルダ html epub latex latexpdf
Windows
要TeX Live

要TeX Live, GNU Make, sh
Linux
要TeX Live

要TeX Live


この表のとおり、SphinxでこぎれいなPDFを作るにはTeXの力を借りる必要があるんだが、これにはTeX Liveというパッケージを使うのが一番簡単だ。


インストーラはInstalling TeX Live over the Internetで取得できる。

Windowsの場合、ネットインストール型のインストーラを使うのがお手軽だが、時間はとてもかかる(2時間くらい?)ことを覚悟したほうがいい。

とはいえ、待っているだけで基本終了するので、特に難しいところはないはず。


また、Sphinxのlatexpdfビルダは、WindowsのDOS窓では処理ができない動きをするので、UNIX由来のツールであるGNU Makeとshを入れておく必要がある。

これにはMSYS2を使うことにした。


QiitaにMSYS2 による gcc 開発環境の構築という記事を投稿している方がいるので、そちらを参考にmakeだけを入れる。

入れたら、C:\msys64\usr\binをパスに追加しておくこと。


ここまでできれば、Sphinxのドキュメントプロジェクトのあるディレクトリでmake.exe latexpdfとやればこぎれいなPDFが出力できていることだろう。


なお、「Sphinxのlatexpdfビルダは、WindowsのDOS窓では処理ができない動きをする」件、一応Issueにも挙がっているようで、日本のメーリングリストでも話題になっているんだけど、なかなかfixしないよなぁ....。


| | コメント (0) | トラックバック (0)

2018年1月31日 (水)

買いました報告:Android アプリ設計パターン入門

なんとなーくアプリを作っている身としては、こういう本が出てくるのはありがたい。

Android アプリ設計パターン入門

Android アプリ設計パターン入門

  • 著者:日高 正博,小西裕介,藤原聖,吉岡 毅,今井 智章,
  • 製本版,電子版
  • PEAKSで購入する

| | コメント (0) | トラックバック (0)

2017年12月 6日 (水)

Redmineを入れてみる on CloudGarage

CloudGarageというNHNテコラスさんが展開している定額制パブリッククラウドサービスがあって、こちらのご厚意で開発者ライセンスを発行してもらえるようになった。

で、Ubuntu Serverのインスタンス一つ立てて、前から欲しかった「自分専用BTS」をセットアップしてみることに。自分自身はRedmineが使い慣れているので、がんばってセットアップに挑戦してみた。

手順自体はRedmine.JPさんのブログの手順のとおりで、特にこれといって特別なことはしていないんだが、途中のPassengerのビルドがメモリ不足で止まる。

むぅ、1GBじゃきびしいか、と思っていたんだが、ある日ふと「スワップ拡大すれば何とかならないか?」と考えてスワップを切りなおすことに。

とりあえず現状。

# swapon -s
Filename                                Type            Size    Used    Priority

え? スワップきいてない?

では、ってことでスワップを追加してみることに。さすがにスライス切りなおすわけにもいかないので、Windowsみたいにファイルをスワップにしてみる。伝統的に物理メモリの2倍にしてみる。

# mkdir /swap
# fallocate -l 2G /swap/swapfile
# chmod 600 /swap/swapfile
# mkswap /swap/swapfile
# swapon /swap/swapfile

ここまで出来たら再度確認。

# swapon -s
Filename                                Type            Size    Used    Priority
/swap/swapfile                          file            2097148 360544  -1

うむ、有効になってる。これで作業再開してみると、こんどはちゃんとビルドが通る。これでめでたくRedmineが使えるように。

CloudGarageさんのインスタンスはディスクがSSDなので、スワップアウトしてもさほど気にならない。いいですな。

| | コメント (0) | トラックバック (0)

2017年9月17日 (日)

ログイン失敗時のメッセージを増やしたい on Spring Boot

0. ことのはじまり

先日、ログイン/ログアウト前後でクエリパラメータを保つ on Spring Bootと題して、ログイン/ログアウト時にクエリパラメータを保持する方法について書いたんだけど、こいつの参照実装を後輩君に教えてやったところ、「ログイン失敗時のメッセージが出なくなったんすけど?」とかいうリプライが。

どうやら、槙 俊明さんのはじめての Spring Boot[改訂版]をもとに(というかほぼそのまんまの模様)Spring Securityの設定をしていたようだが、「ログインに失敗したときに、画面上どうふるまうか」を理解せぬまま、僕が示した参照実装のコードを丸写ししていたようで、あんなリプライを出してきた模様。

このあたり、「自分で考えろ」と突き放してもよかったんだが、後学のために「どうすればいいか」を少々考えてみることにする。

1. 実際どうなっているのか

氏の書籍には、だいたいこんな感じでエラーメッセージ周りの処理が紹介されている。

Viewのhtml(抜粋)



<div th:if="${param.error}>ユーザー名、またはパスワードが違います。</div>

SecurityConfig(抜粋)



protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginProcessingUrl("/login")
.loginPage("/loginForm")
.failureUrl("/loginForm?error")
.defaultSuccessUrl("/", true)
.usernameParameter("username")
.passwordParameter("password")
.permitAll();
}

勘のいい方はお分かりと思うが、


  • 認証に成功したら、「/」にリダイレクト

  • 認証に失敗したら、「/loginForm?error」に移動

と、 URLだけで制御している ことがわかると思う。

それが証拠に、認証に失敗しなくても、「/loginForm?error」をブラウザのアドレスバーに直接入力すると、エラーメッセージが出るはずである。

これ自体は、話を簡単にするための処置なのだろう、と考えているのだが、今回の一件では、認証の成功、失敗で適切にクエリパラメータを加工して流す考慮が必要になるので、アプローチとしては以下の2つがあると考える。


  1. 認証ハンドラで、クエリパラメータを適切に追加、削除する

  2. 失敗時の認証ハンドラで、エラーメッセージをセッションに置く


2. URLだけで制御する場合

Viewの実装をそのままにしたい場合、いままでどおりURLでコントロールするよう、クエリパラメータを適切に追加、削除する処理を書けばよい、ということになる。

たとえば、こんな感じ。

認証成功時(抜粋)



// add whole query parameters to url
String queryParams = request.getQueryString() == null ? "" : "?" + request.getQueryString();

// remove error parameter if present.
if (queryParams.contains("error&")) {
queryParams.replaceAll("error&", "");
} else if (queryParams.contains("&error")) {
queryParams.replaceAll("&error", "");
}

認証失敗時(抜粋)



// add whole query parameters to url
String queryParams = request.getQueryString() == null ? "" : "?" + request.getQueryString();

// add error parameter if not present
if (!queryParams.contains("error")) {
if (queryParams.length() == 0) {
queryParams = queryParams + "?error";
} else {
queryParams = queryParams + "&error";
}
}

汚い実装で恐縮だが、雰囲気は伝わっただろうか。

3. セッションを使う場合

認証失敗時に呼ばれる AuthenticationFailurehandler#onAuthenticationFailure(HttpServletRequest, HttpServletResponse, AuthenticationException) では、認証失敗に至った理由が AuthenticationException のオブジェクトとして受け取ることができるようになっている。(参考

「パスワードが違う」「アカウントが有効期限切れ」「アカウント凍結中」など、詳細に調べることができるので、これを使って流すエラーメッセージを選別すればいい。

たとえば、こんな感じ。

認証失敗時(抜粋)



// Analyze the cause of the error
String errReason = null;
if (exception instanceof BadCredentialsException) {
errReason = "Invalid user name or password.";
} else if (exception instanceof AccountExpiredException) {
errReason = "This account is expired. Please contact administrator.";
} else if (exception instanceof CredentialsExpiredException) {
errReason = "Your password is expired. Please contact administrator.";
} else if (exception instanceof DisabledException) {
errReason = "Your password is disabled. Please contact administrator.";
} else if (exception instanceof LockedException) {
errReason = "Your accouunt is locked. Please contact administrator.";
} else {
errReason = "Unknown problem occured. Please contact administrator.";
}

if (errReason != null && errReason.length() > 0) {
HttpSession session = request.getSession();
session.setAttribute("errReason", errReason);
}

一方Thymeleafには、セッションオブジェクトを扱うための「session」というそのものずばりな予約変数がある。

利用方法は「Spring MVC and Thymeleaf: how to access data from templates - Thymeleaf」という公式ドキュメントに詳しく書かれているので、ひとまずそちらを参照していただくとして、セッションにエラーメッセージを置くなら、セッションから値を取り出して張る処理をViewテンプレートに書く。

たとえば、こんな感じ。

Viewのテンプレート(抜粋)



<div th:unless="${session == null}" th:text="${session.errReason}">Hoge</div>

この方法のメリットは、クエリパラメータをこねくり回す必要がなくなる点で、おそらくこういう方法のほうがフレームワークの設計思想に沿ったものなんじゃないだろうか、と個人的に考えている。

4. とりあえず動く参照実装

URLだけでコントロールしたい場合こちらを、セッションを使う場合こちらを、それぞれご参照ください。

| | コメント (0) | トラックバック (0)

2017年9月10日 (日)

ログイン/ログアウト前後でクエリパラメータを保つ on Spring Boot

0. ことのはじまり

僕は現在、都合により外に出されているんだけど、先日22:00くらいに自宅で自社のグループウェアを開いてみると、弊社内にいる後輩君から「助けてくださ~い(´;ω;`)」とかいうメールが。

いわく、「Spring Bootを使っているアプリがあり、認証にSpring Securityを使っているが、ログイン/ログアウトの際にクエリパラメータ(※ページのアドレスの後ろについている「?」以降のアレ)を保つ必要があり、どうやったらいいのか自分ではわからない」とのことらしい。

まぁ、自分で調べる際の検索キーワードの選定がまずいだけ、のような気もしないでもないのだが、とりあえず参照実装作成までを目標にして、どうやったらいいかを調べてみた。

ということなので、どういう想定でいくかの整理をしてみる。

  • Spring Bootの最新安定版(現時点では1.5.6.RELEASE)を使用
  • 依存ライブラリは、Thymeleaf、Spring Securityと、JS処理用にjQuery 2.1.4を追加
  • 認証には、データアクセスではなくインメモリ認証を使う
  • クエリパラメータはpだけを認識する

というわけなので、やってみる。

1. やること

いろいろググってみたところ、やらなければならないことは以下の3つだということが判明。

  1. 認証ハンドラを実装したクラスを用意し、その中でクエリパラメータを右から左にリレーする処理を書く。
  2. WebSecurityConfigurerAdapter の継承クラスで、1.で作成した認証ハンドラを使う設定を書く。
  3. Viewからのアクセスは、単純なsubmitやaタグのhrefで処理するのではなく、JSでaction属性やhref属性を直接操作する。

2. 実際には

2-1. カスタム認証ハンドラの実装

必要なカスタム認証ハンドラは、以下のとおり。

処理されるタイミング 実装するinterface
ログイン成功時 org.springframework.security.web.authentication.AuthenticationSuccessHandler
ログイン失敗時 org.springframework.security.web.authentication.AuthenticationFailureHandler
ログアウト時 org.springframework.security.web.authentication.logout.LogoutHandler

とりあえずログイン成功時だけ乗せるけど、HttpSevletRequestからクエリパラメータの文字列をとって、リダイレクトURLにぶら下げなおす、というのが基本。

public class AuthSuccess implements AuthenticationSuccessHandler {

  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws IOException, ServletException {
    
    // add whole query parameters to url 
    String queryParams = request.getQueryString() == null ? "" : "?" + request.getQueryString();

    RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    redirectStrategy.sendRedirect(request, response, "/menu" + queryParams );
  }

}

2-2. WebSecurityConfigurerAdapterに認証ハンドラをセット

Spring Securityを入れていると、その構成を行うJavaConfigがあるはずだが、そこをこんな感じに書き換える。

http.formLogin()
    .loginProcessingUrl("/login")
    .loginPage("/login")
    .successHandler(new AuthSuccess())  // ログイン成功時のカスタムハンドラ
    .failureHandler(new AuthFailure())  // ログイン失敗時のカスタムハンドラ
    .permitAll();

http.logout()
    .logoutRequestMatcher(new AntPathRequestMatcher("/logout**"))
    .addLogoutHandler(new LogoutPostProcess())  // ログアウト時のカスタムハンドラ
    .deleteCookies("JSESSIONID")
    .invalidateHttpSession(true);

それぞれカスタム認証ハンドラを設定するためのメソッドが用意されているので、そこにオブジェクトを放り込めばOK。

2-3. JSでURLをたたく

フロント系中心にやってる人にはどうということはないんだけど、通常 <input type="submit"> とか <a href="hogehoge"> とかで書いている処理を、JavaScriptでクエリパラメータを取得してURLをたたくようにすればOK。

/**
 * URLについているクエリパラメータを取得して、Formのaction属性を書き換えてsubmitする処理。
 */
function sendReq() {
  var arg = getQueryParam();
  
  var url = $('#loginForm').attr('action');
  if (arg.p != null) {
    url += '?p=' + arg.p;
  }

  $('#loginForm').attr('action', url);
  $('#loginForm').submit();
}

/**
 * URLについているクエリパラメータを取得する処理。
 */
function getQueryParam() {
  var arg = new Object;
  var pair = location.search.substring(1).split('&');
  for (var i = 0; pair[i]; i++) {
    var kv = pair[i].split('=');
    arg[kv[0]] = kv[1];
  }
  
  return arg;
}

/**
 * URLについているクエリパラメータを取得して特定ページへ遷移する処理。
 */
function doLogout() {
  var proto = location.protocol;
  var host = location.host;

  var url = proto + '//' + host + '/logout';
  
  var arg = getQueryParam();
  if (arg.p != null) {
    url += '?p=' + arg.p;
  }
  
  location.href = url;
}

3. とりあえず動かせる参照実装

GitHubに上げました。ご参照あれ。
f97one/AddingQueryParamsDemo

| | コメント (0) | トラックバック (0)

«液晶割れ