Javaソースコードへのよくある指摘 その1

僕の仕事の一環で他人のJavaソースを見ることがあるので、その時によく出る指摘をちょっと書き出してみた。

とーってもレベルの低い記事なのでJavaをまともにやっている皆さんは読む必要無しです。ただの愚痴エントリだよ。

不要な代入

String name = null; // ここの代入は不要
if (conditionA) {
  name = "foo";
} else {
  name = "bar"
}

これまじやめて。

C言語(C89)から来た人はローカル変数を一番上に宣言したり、初期値を代入しなきゃいけないものだと思ってて困る。 またそういうC言語特有のローカルルールをJavaの初心者に教え込んで嘘を広めていて困る。 (もうC言語から来る人はそうそう出てこないと思うが。)

だいたい、nameに代入しわすれるバグも引き起こすのでやめてほしい。

if分岐の都合上、ありえないけどnameに代入されない分岐パスが出てくるなら、例外でも投げればいい。 まさしく例外だから遠慮せずに投げればいい。

String name;
if (conditionA) {
  name = "foo";
} else if (conditionB) {
  name = "bar"
} else {
  throw new AssertionError("Unexpected condition!");
}

System.out.println(name);

ループで初期代入されるって?上手なかわし方を考えるか、諦めてください。

同じ式

String feces = kuso.getUnko().getShit().get("poo");
String urine = kuso.getUnko().getShit().get("piss");

こういうの本当に多い。似た式は変数かメソッドにまとめてください。そういう作業がいちいち面倒ならIDEの使い方覚えて下さい。

思考停止の StringBuilder

// ファッ!?
StringBuilder sb = new StringBuilder();
sb.append("The specified value = [");
sb.append(hoge);
sb.append("] is illegal.");
String result = sb.toString();

// これでいい。(あるいは String.format("[%s]", hoge) 使いましょう。 )
String result = "The specified value [" + hoge + "] is illegal."

見づらいのでやめていただきたい。

きっとどこかでStringの結合演算子(+)使うなっていうコーディングルール見て、その使うな、だけが脳みそに残ったか、単純にそのコーディングルールがイカれていたかなんでしょう。

ちなみにStringBuilderの多くの使いどころはループ処理で文字列結合する場合だと思いますが、Java8だと以下のようにListからStringに変換できたりするので、使う機会は減ると思います。

// Java7まで
StringBuilder sb = new StringBuilder();
for(Elem e : list){
  if(sb.length() > 0) sb.append(", ");
  sb.append(e.getVal());
}
String result = sb.toString();

// Java8から
String result = list.stream().map(Elem::getVal).collect(Collectors.joining(", "));

tryでcatchしなきゃいけないものだと思っている

try文にcatchは必須ではありません。 try {} finally {} だけでもいいのです。

// (そもそもtry-with-resources使えよっていうツッコミは無しで)

Connection c = null;
try {
  c = acquireConnection();
  transactionalProcessing(c);
} catch (HogeException e) {
  throw e; // 何もしないのにcatchして例外をもっかい投げるのは無駄
} finally {
  closeQuietly(c);
}

final配列を定数だと思っている

finalつけりゃいいってもんじゃないんだよ。時代はイミュータブルなんだよ。

// これ定数じゃない! CONSTANT_LIST[0] = "Unko" で更新できてしまう。
public static final String[] CONSTANT_LIST = {"One", "Two", "Three", "Four"};

// これが定数!
public static final List<String> CONSTANT_LIST = Collections.unmodifiableList(Arrays.asList("One", "Two", "Three", "Four"));

Collections.unmodifiableListArrays.asList の組み合わせはよく使うので、どっかにそれ用のユーティリティ用意しておくのが普通です。

nullありのObjectの比較

java.util.Objects クラス使いましょう。

// String lhs;
// String rhs;

if (Objects.equals(lhs, rhs)) {
  // lhs == null && rhs == null の場合もOK
}

番外編・メソッドを上手にEclipseで抽出する方法

メソッドのまとめ方ですが、Eclipseならこういうふうにできます。あんまりこういうやり方を解説している人が少ないので、一応解説しておきます。

以下のソースを見て下さい。

// Map<String, String> barMap, fooMap, bazMap;

if ( Objects.equals(barMap.get("hoge"), fooMap.get("hoge")) ) {
  bazMap.put("hoge", retrieveValue("hoge"));
}

if ( Objects.equals(barMap.get("fuga"), fooMap.get("fuga")) ) {
  bazMap.put("fuga", retrieveValue("fuga"));
}

これがこうなったらわかりやすそうです。

private void setRetrievedValueIfEq(String key, Map<String, String> barMap, Map<String, String> fooMap,  Map<String, String> bazMap) { ... }

setRetrievedValueIfEq("hoge", barMap, fooMap, bazMap);
setRetrievedValueIfEq("fuga", barMap, fooMap, bazMap);

上記のようなコードにさっさとEclipseで変更するには、まずは下記のように変数を抽出(Alt-Shift-l)し、

// Map<String, String> barMap, fooMap, bazMap;

String key = "hoge";
if ( Objects.equals(barMap.get(key), fooMap.get(key)) ) {
  bazMap.put(key, retrieveValue(key));
}

String key2 = "fuga";
if ( Objects.equals(barMap.get(key2), fooMap.get(key2)) ) {
  bazMap.set(key2, retrieveValue(key2));
}

if部分を選択してメソッドを抽出すれば(Alt-Shift-m)下記のようになります。同じ構造しているものもメソッドに抽出されます。

要するにパラメータにしたいものをローカル変数として抽出してIDEが見つけられるようにしておくのがメソッド抽出のミソなんですね。

private void setRetrievedValueIfEq(String key, Map<String, String> barMap, Map<String, String> fooMap,  Map<String, String> bazMap) { ... }

String key = "hoge";
setRetrievedValueIfEq(key, barMap, fooMap, bazMap);
String key2 = "fuga";
setRetrievedValueIfEq(key2, barMap, fooMap, bazMap);

それぞれのパラメータになった変数は不要なのでインライン化(Alt-Shift-i)しましょう。

setRetrievedValueIfEq("hoge", barMap, fooMap, bazMap);
setRetrievedValueIfEq("fuga", barMap, fooMap, bazMap);

これらの操作をもうちょっとショートカットを駆使しながらほぼ無意識でできるようになるとJavaに対する見方がかわるでしょう。

(識別子をたぐる言語に見えてくるはず。)

総評

大概、知識が手習いで学んだJava1.4やってた頃でとまってる。

今日のところはこれぐらいにしておいてやるよ!

JenkinsのSSHスレーブが文字化けする場合

たとえば、DockerでテキトーにSSHスレーブを構成したりすると、スレーブでのビルドでUTF-8文字列が化けたりします。 ロケールがズレてるからですね。

スレーブの環境変数パネルに LC_ALL=en_US.UTF-8 を設定しても無駄だったりしますので、 スレーブ自体の java プロセスの環境変数LC_ALL=en_US.UTF-8 が入るようにしましょう。

Jenkinsのノードの設定値の画面で下記のように起動コマンドのPrefixとSuffixを設定してあげましょう。

# Prefix Start Slave Command
sh -c 'export LC_ALL="en_US.UTF-8";

# Suffix Start Slave Command (シングルクォートのみ書く)
'

参考

SSH Slaves plugin - Jenkins - Jenkins Wiki

JenkinsでVBAを動かす際のあれこれ

かれこれ1年以上、JenkinsでExcel VBAを動かしているので、そろそろたまっていることを出そうと思いました。

Excel VBAをサービス内で起動させる方法

Excel VBAは素直にJenkinsで動くようにできていません。Jenkinsで動くのは基本的には応答のいらないコマンドライン操作だけです。

Windowsでいえば、Jenkinsですんなり動くのは対話無しのバッチファイルかPowerShellスクリプトといったものということですね。

Excel VBAはこのように便利なコマンドラインとはかけ離れた性質を持っており、一般的に対話が必要(対話がいるマクロは死んで欲しい)だったり、実行するにもコマンドラインから素直には実行できなかったりします。

そもそも、一般的にはWindowsでJenkinsやJenkinsスレーブを動かすにあたって、それらをサービスとしてインストールすることが多いと思いますが、 Windowsのサービスで立ち上がるプロセスは普通のクライアントセッション(普通のデスクトップ環境)のプロセスとは違い、 Session 0 Isolationとも呼ばれる対話無しの環境で動作するため、通常のGUIアプリケーションが動かなかったり奇妙な動作をしはじめたりします。

Excelも例に漏れず無対策だと動きませんが、ここらへんは過去に書いたエントリに記載した通り、下記のフォルダを作ってやればちゃんと動くようになります。

C:\Windows\system32\config\systemprofile\Desktop

# (x64環境で32bit Officeを動かしている場合)
C:\Windows\SysWOW64\config\systemprofile\Desktop

(ちなみにログインしているデスクトップ環境のセッションからのプロセスでJenkinsスレーブを開くようにしても、この問題は回避可能です。 ここらへんのWindowsサービスから作成されたプロセス特有の問題はWindowsのブラウザやGUIのテストでもぶちあたると思います。)

Excel VBAコマンドラインから起動させる方法

Excel VBAコマンドラインから起動させる方法については、たぶん以下の方法が考えられます。

  • コンパイルのいる言語でコンソールアプリケーションを作って、そこからCOMを一生懸命呼び出す。
  • WSHで呼び出す。(cscript.exe とかで)
  • PowerShellで呼び出す。

僕はPowerShellで呼び出す、という選択肢を選びました。

Windows7以降は標準でバージョン ≧2.0 が使えますし、Jenkinsのビルドプロセス中、VBAの呼び出しといったタスク以外にも、 PowerShellを使った複雑な処理が多少は必要になる、あるいは複雑な処理とVBAが絡みあう可能性もあり、 PowerShellがCOMと.NETがあわさった手軽かつ強力なスクリプティング環境になっているからですね。

PowerShellExcelを呼び出すのは以下のコードだけでできます。

$excelApp = New-Object -com "Excel.Application"

そこからマクロを呼ぶなら、下記のコードでできます。macroProcedureというマクロを引数2つつけて呼んでいる例です。

$macroBook = $excelApp.Workbooks.Open(マクロブックへの絶対パス)
# ' でマクロブック名を囲む必要はありませんが、スペースなどがある場合に備えて囲んでおきましょう。
$excelApp.Run("'$($macroBook.name)'!macroProcedure", "arg1", "arg2")

こういうマクロを呼び出すPowerShellスクリプトをJenkinsから呼び出せば、 結果的にExcel VBAをJenkinsから呼び出していることになります。

Jenkins上でのPowerShellの呼び出しはPowerShell用のプラグインがJenkinsにあるのでそれを使いましょう。

ただし、Jenkinsはテンポラリの*.ps1スクリプトをその場で生成してそれを動かそうとするので、 そのテンポラリのスクリプトを動かせるよう、 Set-ExecutionPolicyでポリシーを変更しておきましょう。 (システム管理者権限が必要。)

Set-ExecutionPolicy RemoteSigned
# または
Set-ExecutionPolicy Unrestricted

PowerShellの処理について、bash-eフラグを立てた時のように、 処理中に何らかのエラーがあった場合に即時終了してほしい場合は下記のようにしておけば即時終了します。

$ErrorActionPreference="Stop"

自動化したいマクロは対話を避けよう。自動化に備えた作りにしよう。

Jenkins上のVBA内でメッセージがポップアップしたり、ダイアログが開いてしまうと、そこで処理が停止してしまいます。

自動化させたいマクロにこういう対話要素を組み込むのはやめましょう。 (ちなみにマクロでランタイムエラー等が出ても誰にも応答できません。)

個人的な好みを言わせてもらえば、対人間であってもマクロでダイアログは開いてほしくないものです。

開発者向けに作ったマクロなんかは、ダイアログはほとんどの場合邪魔になっているケースが多いと思います。

特に、ディレクトリを選ぶパスもペーストできない、あのダイアログをわざわざWin32API使って呼ぶ奴はクレイジーと断言していいでしょう。 あのディレクトリツリーを一日数十回以上ポチポチさせられる人の気持ちを想像してほしいです。

マクロの設定値が必要ならセルに書かせりゃいいんです。セルから値を拾うのが面倒なら、値の部分を名前付き範囲にすればいいんです。 (ここらへんはいつかエントリにします。)

マクロの構成としては実際、対人間用と対Jenkins用にマクロの入り口のプロシージャを下記のように分けるのが現実的でしょう。

  • 対人間用入り口 → 共通メイン処理
  • 対Jenkins用入り口 → 共通メイン処理

対Jenkins用入り口で対話的要素のフラグをOFFるための初期処理などさせればいいです。

ジョブを実行するたびに excel.exe が増えていく件

PowerShell内で起動したExcelは基本的には自前で終了させるまで起動しっぱなしです。 PowerShellが終了したからといって自動的に強制終了させられたりはしません。(そうだったらどんなに楽だったか)

メモリの肥やしを消す意味でも、また、excel.exeが掴んでいるファイルロックを解除させるためにも、 マクロ実行後のExcelの後始末はちゃんとしましょう。

少なくとも下記の条件がすべて満たされていれば、後始末完了とみなして良いようです。

  • 生成したExcel.Applicationおよび内部への参照をPowerShellスクリプトが保持していないこと。 (ただ、PowerShellスクリプトからの参照についてはPowerShellスクリプトが終了すれば全て解放されるので、あまり気にする必要はないかも)
  • $excelApp.Visible = $true にしないこと。 可視化されたExcelは解放されません。
  • 開いたブックを全て閉じていること。
  • 最後に $excelApp.Quit() が発行されていること。

後始末が完了していれば、適当なタイミングでexcel.exeが閉じられるようです。 適当なタイミングというのは、次にExcel.Applicationを作った時とか、そういう本当に適当なタイミングのようです…。

(もし中途半端に残留するexcel.exeを許せない場合、Excelのアプリから$excelApp.HWNDでハンドラをとりWin32APIでPIDを取得して Stop-Process で強制終了するといった方法がオススメです)

Excelを後始末するには、下記のコードのようにすればよいでしょう。

try {
  $excelApp = New-Object -com "Excel.Application"
  // $excelApp を使った処理
} finally {
  # 後始末
  if ($excelApp) {
    # 抵抗されると面倒なのでイベント・警告OFF、VisibleはOFFになっていると思うがOFF。
    $excelApp.EnableEvents = $false
    $excelApp.DisplayAlerts = $false
    $excelApp.Visible = $false

    # 全ブック保存なしで閉じる。保存など必要な操作はここにくるまでにされているはず。
    $excelApp.Workbooks | % { $_.Close($false) }

    # アプリケーションが終了済みとしてマーク
    $excelApp.Quit()
    # Excelに関する参照を外す(どうせこのスクリプトを終了したら外れるので、書かなくてもいい)
    $excelApp = $null
  }
}

ただ、このように後処理したとして、何かマクロでエラーが残ってしまうなどのことが起これば、プロセスはいともたやすく残留してしまうので、 1日1回程度Officeアプリケーションだのを全部殺すプロセスクリーニングジョブを流すこともおすすめしておきます。 VBAのビルドをしているマシンを毎日再起動できるなら、そうしてもいいでしょう。

また、あるワークスペース上にてExcelのプロセスが中途残留したことにより、たとえばワークスペース内のExcelファイルにファイルロックが残りっぱなしになってしまうなどの事象については、 基本的に読み取り専用でワークスペース内のファイルおよびマクロブックを開くように組んでおくことをおすすめします。 (ワークスペースのファイルの属性を読み取り専用に変更してしまってもいいでしょう)

JenkinsからPowerShellスクリプトをアボート(停止)させたらどうなるの?

Jenkinsでは走行中のビルド処理を×ボタンを押すことによりビルドを中断(強制終了)させることができます。

ただその場合、大変残念ですが、PowerShellはそこで即座に強制終了させられ、内部で起動していたexcel.exeは死にません。走り続けてしまいます。

https://wiki.jenkins-ci.org/display/JENKINS/Aborting+a+build に書いてあるように、アボート時は子孫プロセスも殺すということで、 PowerShell内で起動したExcelなども死にそうですが、ExcelPowerShellの子孫として起動しません。

じゃあfinally句あたりでExcelを殺しにいけばいいじゃないかというのも、 JenkinsのWindows版のプロセスキラーの実装(https://github.com/kohsuke/winp/blob/master/native/winp.cpp)が TerminateProcesshttps://msdn.microsoft.com/ja-jp/library/cc429376.aspx)を呼んでPowerShellを強制抹殺しにいくので不可能です。

try {
  // 処理…
} finally {
  ' 強制終了させられたらfinallyもクソもなくPowerShellは終了。動作しているExcelのお掃除はできずに終了である。
}

このタイプの強制終了のトラップ(捕捉)はWindows上では不可能です。何もできません。

これで問題になるのは、たとえば、ブックやドキュメントを処理中に読み取り専用で開いたりしていなかった場合、アボートしたジョブのプロセスがファイルにロックを持ったままになってしまうことですね。 残ったままのプロセスをどうにかして殺さない限りロックが解除されることはありません。次のジョブが同じワークスペースで動いた時、問題になるでしょう。

そもそもロックなんか気にしなくて済むように、読み取り専用で開いて済むものはなるべくそれで処理するのが一番ですが、何かいい方法ないんですかねー。

これに限らずWindows上でのJenkinsというのは、だいたいファイルのロックに悩まされるのが定番です。 最悪の場合はプラグインか何かのせいで、Jenkinsのプロセス自体がワークスペース内のファイルにロックを持ってしまうこともあります。 特定のフォルダ以下のロックの強制解除方法探したほうがはやいかもしれません。

そういう事故に備えて、前回起動したExcelのPIDをワークスペースのどこかにおいておき、次にジョブがそのワークスペースで起動した時にそのPIDを強制終了しにいけばいいんじゃないの?

確かにそうなのですが、残念ながらWindowsは以前使ったPIDを再利用します。OS起動後、プロセスごとに必ずユニークなPIDが振られると思ったら大間違い。

正常終了した後でもそのPIDが前回残留したものという保証はありません。ということで、一筋縄ではうまくいかないでしょう。

ここらへんの確実にプロセスを殺す手法についてはいろいろ考え中です。子孫関係のないプロセス、たとえばスケジューラとかにPowerShellが死んだらExcelを殺すように依頼するのが手っ取り早そうですね。 https://wiki.jenkins-ci.org/display/JENKINS/Spawning+processes+from+build も参考になるかも。

マクロ呼び出しに相対パスは使えません

PowerShellから起動されたExcelインスタンスはシェルの相対パスなど理解してくれないので、PowerShellから与えるパス関連の情報は全て絶対パスで与える必要があります。

以下はパスに関する処理の、駄目な例と良い例。

# 駄目な例 (相対パスは使えない)
$macroBook = $excelApp.Workbooks.Open("macroBook.xlsm")
$excelApp.Run("$($macroBook.name)!macroProcedure", ".\targetFolder", ".\some\destionation\output.xlsx")


# 良い例 (一度相対パスで資材の場所を変数に格納しておき、絶対パスにあとで変換する)
function Make-Absolute(){
  param($relativePath)

  Join-Path (pwd).path $relativePath
}
$macroBookPath = "macroBook.xlsm"
$targetFolder = ".\target"
$destination = ".\some\destination\output.xlsx"

$macroBook = $excelApp.Workbooks.Open((Make-Absolute $macroBookPath))
$excelApp.Run("'$($macroBook.name)'!macroProcedure", (Make-Absolute $targetFolder), (Make-Absolute $destination))

ちなみにUnixなどのシェルスクリプトでもそうですが、作業中広域にわたってcdしてカレントディレクトリを切り替えるのは混乱のもとになるので、やめましょう。

少なくともJenkinsでのスクリプティングで、カレントディレクトリがワークスペースフォルダ直下以外になることは原則ないというように処理したほうがいいでしょう。

Excel VBAから処理進捗のコンソールログを出力したい

PowerShellからExcel VBAを起動できるのはわかった。

でも、無言のままジョブが走っていても今何が処理されているのかわからない。もしかしたら止まっているのかも?今何をしているのかVBA側からコンソールに表示したい。

これは当然の要求ですね。

これをさせるためには、PowerShellのコンソールに出力するメソッドを持つオブジェクトをVBAに渡してあげれば、VBA側からPowerShellのコンソールに出力させることができます。 (そのPowerShellスクリプトがJenkins上で動いていれば、VBAからの出力がJenkinsのコンソール画面で見られることになります。)

' VBA (Book1.xlsmというブックにマクロモジュールとして下記プロシージャを中身にしたものを宣言)

Private consoleObj As Object

' PowerShell側から最初に呼んで conosleObj の中身をもらう関数
Public Sub RegisterConsole(ByVal envConsoleObj As Object)
  Set consoleObj = envConsoleObj
End Sub

' VBA内でDebug.Printのかわりに使う関数
Public Sub DebugPrint(ByVal logMessage$)
  If Not consoleObj Is Nothing Then
    ' ちなみに親のPowerShellだけ上記に書いたアボートなどで一方的に死ぬとここで失敗こきます (TypeName(consoleObj) は "Object"を返す)
    consoleObj.WriteLine logMessage$
  End If
  Debug.Print logMessage$  
End Sub


Public Sub SomeProcessing()

  DebugPrint "処理開始しました!"

  ' :
  ' :
  ' :

  DebugPrint "処理終了しました!"  

End Sub
# PowerShell

# AlternativeConsole というクラスをPowerShellで動的生成。
Add-Type -TypeDefinition @"
using System;

public class AlternativeConsole
{
    public void WriteLine(string line)     { Console.WriteLine(line); }
    public void Write(string line)         { Console.Write(line); }
    public void ErroWriteLine(string line) { Console.Error.WriteLine(line); }
    public void ErroWrite(string line)     { Console.Error.Write(line); }
}
"@

try {
  $excelApp = New-Object -com "Excel.Application"

  # 末尾の後ろ2つの引数は: UpdateLinks := $false, ReadOnly = $true
  $wb = $excelApp.Open((Make-Absolute "Book1.xlsm"), $false, $true)

  # AlternativeConsole インスタンスをインジェクト
  $excelApp.Run("'$($wb.name)'!RegisterConsole", (New-Object AlternativeConsole))

  $excelApp.Run("'$($wb.name)'!SomeProcessing")
  # ここで下記のようにPowerShell側のコンソールに出力される:
  #   処理開始しました!
  #   処理終了しました!

} finally {
  if ($excelApp) {
    $excelApp.EnableEvents = $false
    $excelApp.DisplayAlerts = $false
    $excelApp.Visible = $false

    $excelApp.Workbooks | % { $_.Close($false) }
    $excelApp.Quit()
  }
}

VBA内で Debug.Print を呼んでいる箇所を自前の DebugPrint 関数に置き換えるといい具合にコンソールログが出ると思います。

まとめ

要するに下記課題に対応できれば、JenkinsでVBAをスムーズに起動できるでしょう。

  • Excel VBA自体をサービスプロセス上で動かせるようにする or そもそもサービスプロセス上で起動させない
  • マクロをコンソール仕様にあわせる。
    • マクロに応答要素を入れない。
    • マクロからコンソールに処理状況を出力させる。
  • 基本的にマクロとのやり取りは絶対パス
  • ファイルのロックやプロセスの残滓が残らないようにする。

もし同じことをもっとうまくやっている人がいたら、是非どうやっているか私に教えてほしいです…。

Jenkinsの公式Dockerイメージ使ってみた

以前のエントリ( http://knjname.hateblo.jp/entry/2014/05/03/190842 )で自分でJenkinsのDockerイメージを作成したりしてみましたが、 Jenkins公式でDockerイメージを配布するようになったので、それを使用したほうがいいと思います。

普通に使うだけなら、下記のようにすればいいだけですが、

docker run -p 8080:8080 jenkins

これだと何も細かいことを設定できていないので、いくつか補足。

Dockerイメージ内のJenkinsのバージョンについて

基本的にJenkinsのサイトのトップ( http://jenkins-ci.org/ )に載っているようなマジもんの最新版は不安定です。

ただ、DockerイメージのlatestはLTS版(安定版)の最新がデフォルトで指定されているようです。 ちゃんと運用するのであれば明示的なバージョン指定をオススメしますが、特に気にしなくても問題なさそうです。

# 今(2015-02-10)のところ下記3つは同じ
docker run -p 8080:8080 jenkins
docker run -p 8080:8080 jenkins:latest
docker run -p 8080:8080 jenkins:1.580.2

# (実はLTS版は1.580.3がリリースされていたりするがDockerHubにはない模様…)

今のところ、使用されるJavaはOpenJDK7のようです。気に入らなければ自分でイメージを作って別のJDKを使わせるようにしたほうがいいでしょう。

永続化ディレクトリ(JENKINS_HOME)について

永続化が必要なデータはコンテナ内の /var/jenkins_home に作成されるので、ホストディレクトリをその位置にマウントしましょう。

docker run -p 8080:8080 -v /hostdir/jenkins_home:/var/jenkins_home jenkins

ただし、root権限でそのまんま作成したディレクトリ(普通にdocker runが勝手に作ったものがこれに該当)はコンテナ側のJenkinsユーザ(jenkins(UID=1000) )で書くことができません。 以下のいずれかで対処しましょう。

  • オススメ: chown 1000 /hostdir/jenkins_home する。(※UIDはDockerfile内で明示的に指定されているので変わる可能性は低いでしょう)
  • chmod 777 /hostdir/jenkins_home する。
  • 独自のJenkinsイメージを作成してJenkinsの実行ユーザをrootにする。
  • 一度マウントしないで起動させたコンテナ内から/var/jenkins_homeをコピーしたものをあらためてマウントする。

JenkinsのJVMのオプションについて

JavaのメモリetcのためのJVMオプションはJAVA_OPTSという名前で環境変数に設定すればいいです。

docker run -p 8080:8080 -e JAVA_OPTS="-Xmx1g" jenkins

Jenkinsのオプションについて

たとえば、 http://hostname/jenkins のようにURLプレフィックスを設定するには、docker run jenkinsの後ろに引数追加すればいいです。あるいはJENKINS_OPTSに定義すればいいです。

docker run -p 8080:8080 jenkins --prefix=/jenkins
docker run -p 8080:8080 -e JENKINS_OPTS="--prefix=/jenkins" jenkins

Jenkins用の引数一覧は https://wiki.jenkins-ci.org/display/JENKINS/Starting+and+Accessing+Jenkins で一部リスト化されています。

ログファイルの出力先などもオプションでの指定できるはずなのですが、記載がないですね。https://github.com/jenkinsci/jenkins/blob/master/rpm/SOURCES/jenkins.init.in とかを参考に追加してみるといいかもしれません。

docker run -p 8080:8080 jenkins --logfile=/var/jenkins_home/jenkins.log

Jenkins初期化Groovyスクリプト

ほか、設定ファイルに永続化されておりパラメータでは初期指定できない情報や、 初期プラグインを勝手にインストールさせるなどを行わせるGroovyスクリプトをロードさせることができます。

(Groovyでの自動化の詳細は https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+Script+Console を見て下さい。)

ためしに、下記のスクリプト(my-jenkins-init.groovy)が入ったディレクトリを /var/jenkins_home/init.groovy.d/ にマウントしてみましょう。

// my-jenkins-init.groovy
import hudson.model.*;
import jenkins.model.*;

def inst = Jenkins.instance

def uCenter = inst.updateCenter

// Set the proxy for retrieving plugins. (Comment out and modify it if you need.)
// inst.proxy = new hudson.ProxyConfiguration("192.168.10.10", 3128, "proxyUser", "proxyPassword", "localhost, 192.*")

// Ensure the Update Center is up-to-date.
uCenter.updateAllSites()

def checkIfPluginInstalled = { pluginid ->
  for(p in inst.pluginManager.plugins) {
    if(p.shortName == pluginid){
      return true
    }
  }
  return false
}

// Installs the following plugins if not installed.
["checkstyle", "copyartifact", "build-timeout", "emotional-jenkins-plugin"].findAll({! checkIfPluginInstalled(it)}).each {
  uCenter.getPlugin(it).deploy()
}

// Set the number of executors (in master-node) to 0.
inst.setNumExecutors(0)

inst.reload()

自動でいくつかプラグインがインストールされており、マスターノードのエグゼキュータ数が0の設定が行われているはずです。

初期のイメージでは、スレーブ用のJNLPポートの自動設定スクリプトが入っているようです。

// tcp-slave-angent-port.groovy
import jenkins.model.*;

Thread.start {
      sleep 10000
      println "--> setting agent port for jnlp"
      Jenkins.instance.setSlaveAgentPort(50000)
}
Feb 09, 2015 5:08:41 PM jenkins.util.groovy.GroovyHookScript execute
INFO: Executing /var/jenkins_home/init.groovy.d/tcp-slave-angent-port.groovy
Feb 09, 2015 5:08:43 PM org.jenkinsci.main.modules.sshd.SSHD start

(2015/09/09) 今のJenkinsのイメージだとコンテナ起動時に指定された環境変数 JENKINS_SLAVE_AGENT_PORT の番号にJNLPのポートを変更するようになっています。良い改善だと思います。

ということで、

だいたい上記でやりたいことはできます。

yumapt-getで入れたパッケージ版のJenkinsも結局は、 上記で説明しているようなパラメータの設定を設定ファイルにバラしているだけなので、 パッケージ版に機能で遅れを取るといったことは一切心配不要です。

Jenkinsのマスターはマスター!

以前のエントリ( http://knjname.hateblo.jp/entry/2014/05/03/190842 )でも書いたとおり、Jenkinsのマスターノードにビルドを直接させる必要は一切ありません。

大量にいろんな種類のジョブをこなすようになると、スレーブが増えていき、 そんな中でマスターがビルドしているのが不自然かつ、マスター依存のジョブがあった日にはスケールが面倒になってきます。

特に貧弱な環境じゃない限り私はマスターノードにビルドをさせていません。マスターのジョブキューの太さは0にしてます。

今回のエントリでふれているのは、Jenkinsマスターノードのセットアップであり、スレーブノードの話ではありません。 Jenkinsのマスターノードは、WebUIを提供して、設定を保存して、スレーブノードとつながって、 スレーブが収めてくるジョブの履歴などをディレクトリに貯めていればそれでいいんです。(というのが私の考え。)

Jenkinsが活躍している開発現場でマスターノードが死ぬと開発者への影響が大ですが、 スレーブに関していえばホストOSごとおっ死んでも大して影響はないでしょうし、代替のスレーブがいればスレーブの1台2台ダウンしようが 平気なところも多いはずです。

実際ビルドを行わさせるスレーブについては、Jenkinsのビルド用のDockerコンテナを作成してSSHでマスタから繋くとかの平易な構成でいいと思います。 (もちろんDockerコンテナ使い捨てとか頑張るのもアリ)

ちなみにスレーブ内にビルド用のJDKやAntを含めてもいいですが、実際には含めず、Jenkinsの設定画面で JDKやAntのtar.gzやzipをビルド実行時にネットからインストール or どっかのURLから解凍できるように構成したほうが楽です。 (とはいえ、スレーブプロセスはJVMで動くので、スレーブ自身が動くためのJREは必要です。)

現状の公式Dockerイメージへの個人的な不満点

  • OpenJDK7を使っている。OpenJDK8じゃ駄目なのかな。個人的にはヒープはともかく、いろいろやると広がるパーマネント領域を意識したくない。 → 最新のイメージはOpenJDK8使っているようです (2015/08/05)
  • /var/jenkins_homeにroot権限で作成した外部ディレクトリをマウントさせると起動に失敗する件。…まあこれはDocker自体の問題に近いですが。 → docker run 時に -u root オプション付ければうまくいくことも多いでしょう(2015/09/09)
  • Jenkinsのログファイルの出力場所やログレベルの指定など細かい起動オプションが分かりづらい。

VBAで長いパスが扱えないと思ったら

Windowsでは長いパス(260文字前後)を使うと呪いに遭います

Windowsでは長いパスを使うと呪いに遭います。

(個人的には英語の長ったらしいディレクトリ名とファイルパスをついたものをJenkinsでこれまた長ったらしいワークスペースフォルダにチェックアウトした時に遭遇している)

こんなディレクトリとファイルがあったとしましょう。

C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\
    01234567.xlsx
    a.xlsx
    b.xlsx

上記中の 01234567.xlsxWindowsで許されている最長のファイルパスを持つエントリです。

ただ、ファイルパスが長過ぎるためか、、、

  • ExcelVBAのWorkbooks.Open()で開くことができません。(アプリケーション側から開くのも不可能です。)
  • FSOが妙な挙動を示すようになります。

このFSOの妙な挙動というのは、詳しくいうと、下記のようにファイルの列挙に失敗します。

Sub listFiles()

  Dim fso As New FileSystemObject

  Dim i&

  ' 正しいファイル数 3 が表示される
  Debug.Print fso.GetFolder("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789").Files.Count

  ' 長いファイルパスのFileオブジェクトの作成にFor Each文で失敗し、2が表示される
  For Each f In fso.GetFolder("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789").Files
    i& = i& + 1
  Next
  Debug.Print i

End Sub

ロングパス対策

では、どうやってこれを克服すればいいかというと、解決方法は簡単で、長いパス部分についてショートパスを使ってあげればOKです。

Sub listFiles2()

  Dim fso As New FileSystemObject
  Dim i&

  Debug.Print fso.GetFolder("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789").Files.Count
  For Each f In fso.GetFolder("C:\LONGPA~1\012345~2").Files
    Debug.Print f
    i& = i& + 1
  Next
  Debug.Print i

End Sub

同様に、長過ぎるパスのブックをExcelは開けないので、かわりにショートパスを与えてあげれば問題なく開くことができます。

Dim wb As Workbook

' 失敗する
Set wb = Workbooks.Open("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\01234567.xlsx")

' これならOK
Set wb = Workbooks.Open("C:\LONGPA~1\012345~2\012345~1.XLS")

ちなみに上記のようなショートパスでファイルを開いた場合、ショートパスのファイルとしてVBA内では扱われます。

Debug.Print wb.Name ' 01234567.xlsx
Debug.Print wb.Path ' C:\LONGPA~1\012345~2
Debug.Print wb.FullName ' C:\LONGPA~1\012345~2\01234567.xlsx

ショートパス⇔ロングパス変換

たとえばファイルパスの一覧を出すといったマクロの場合、長過ぎるパスを処理する場合に、ショートパスが欲しかったり、逆にショートパスで処理しているが故にレポート出力時ではちゃんとロングパスで出力したい場合があると思います。

ロングパス → ショートパス変換はFSOを使って行うこともできますが、

Dim fso As New FileSystemObject

Dim shortPath As String
shortPath = fso.GetFile("えらい長いパス").ShortPath

そもそも長すぎてGetFileする時点で失敗するかもしれないし、相互の変換については、専用のWindows APIがあるので、それを使いましょう。

以下がWindows API(の宣言とその応用のためのVBAプロシージャ)です。64bit Officeでしか試していませんし素人なので、なんか間違ってるかも。(いつか調べます)

' LongPath -> ShortPath
#If VBA7 Then
Private Declare PtrSafe Function Win32APIGetShortPathName Lib "kernel32" Alias "GetShortPathNameA" _
  (ByVal lpszLongPath As String, ByVal lpszShortPath As String, ByVal cchBuffer As LongLong) As LongLong
#Else
Private Declare Function Win32APIGetShortPathName Lib "kernel32" Alias "GetShortPathNameA" _
  (ByVal lpszLongPath As String, ByVal lpszShortPath As String, ByVal cchBuffer As Long) As Long
#End If

Function GetShortPathName(ByVal longPath As String) As String
  Const PATH_LENGTH = 260

  Dim pathBuffer As String
  pathBuffer = String$(PATH_LENGTH + 1, vbNull)

  #If VBA7 Then
    Dim pathLength As LongLong
  #Else
    Dim pathLength As Long
  #End If
  pathLength = Win32APIGetShortPathName(longPath, pathBuffer, PATH_LENGTH)

  GetShortPathName = Left(pathBuffer, CLng(pathLength))
End Function


' ShortPath -> LongPath
#If VBA7 Then
Private Declare PtrSafe Function Win32APIGetLongPathName Lib "kernel32" Alias "GetLongPathNameA" _
  (ByVal lpszShortPath As String, ByVal lpszLongPath As String, ByVal cchBuffer As LongLong) As LongLong
#Else
Private Declare Function Win32APIGetLongPathName Lib "kernel32" Alias "GetLongPathNameA" _
  (ByVal lpszShortPath As String, ByVal lpszLongPath As String, ByVal cchBuffer As Long) As Long
#End If

Function GetLongPathName(ByVal shortPath As String) As String
  Const PATH_LENGTH = 260

  Dim pathBuffer As String
  pathBuffer = String$(PATH_LENGTH + 1, vbNull)

  #If VBA7 Then
    Dim pathLength As LongLong
  #Else
    Dim pathLength As Long
  #End If
  pathLength = Win32APIGetLongPathName(shortPath, pathBuffer, PATH_LENGTH)

  GetLongPathName = Left(pathBuffer, CLng(pathLength))

End Function

上記のソースで使っているWinAPIはこちら。

https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx https://msdn.microsoft.com/en-us/library/windows/desktop/aa364980(v=vs.85).aspx

このVBAソースはGistにもあげてあります。

https://gist.github.com/knjname/ba3b7d433655d5930a08

PowerShellのダイナミックスコープについて

PowerShellのスコープが好きになれないし、罠にわりとハマっているので、個人的に動きを確認してみた。

ある程度以上の規模のスクリプトを書くにあたって、PowerShellは特に、スコープの意識が絶対必要です。

基本はダイナミックスコープ

PowerShellのスコープはダイナミックスコープ(スタック上、最後の変数宣言を見る)。

普通の言語みたいにレキシカルスコープ(上に辿って一番近くの変数宣言を見る)ではない。

$foo = "FOO"
"foo is $foo" # foo is FOO

Function Update-foo(){
  $foo = "BAR"
  "foo is $foo" # foo is BAR
}
Update-foo

"foo is $foo" # foo is FOO

まあこれが基本。

ただ、ダイナミックスコープって一言でいうけど、どういうシチュエーションでどう変わるか、確信持てますか?

ダイナミックスコープ七変化

とりあえずどういうスコープで変数が束縛されているのか知るためにいろいろコードのパターン並べてみました。

ちゃんと脳内動作と一致しているか確認してみよう!たぶん一致してないと思うよ!

(僕もいくつかの動作はよく理解できませんでした。いつか学習したい…)

思ったこと

PowerShellがシンプルに見える人は病院いったほうがいいと思う。

パクり元のPerlみたいにmy $lexicalVar; local $dynamicVar;させてほしい…。

Jenkinsのジョブの進化

Jenkinsのジョブの進化をみていくよ〜。

はじまりはシンプルだった

プロジェクトの成果物をチェックするために、チェッカを動かすジョブ check-project-artifacts を作る。

check_aというチェッカを動かす必要があり、そのチェッカを動かした後のワークスペースの成果物はこんな感じ:

check-project-artifacts:
  ./target/check_a-result

一般の開発者は check-project-artifacts のJenkins成果物をブラウザで開いて利用する。

チェッカが増えた

チェッカの種類が増え、もう1つのチェック check_b を実施することになった。

check-project-artifacts のジョブの最後にチェッカの追加をそのまま追加することにした。

check-project-artifacts:
  ./target/check_a-result
  ./target/check_b-result

さらにチェッカが増えた

更にチェッカの種類が増えた。(check_c)

このままチェッカを一つのジョブに追加しつづけると埒があかず、しかもチェッカを直列に実行していると遅いので、チェッカごとにジョブを分割することにした。

  • check-project-artifacts (もともとあったやつ)
    • check-project-artifacts_a (分割後)
    • check-project-artifacts_b (分割後)
    • check-project-artifacts_c (分割後)

それぞれ子供側のチェッカのジョブが終了したら、自分のチェック結果を成果物として保存し、 check-project-artifacts を後続ジョブとして呼ぶ。

check-project-artifacts ではそれぞれ分割したジョブから成果物をコピーし、そのまま自分のジョブ成果物として保存する。

check-project-artifacts:
  ./target/check_a-result
  ./target/check_b-result
  ./target/check_c-result

check-project-artifacts_a:
  ./target/check_a-result

check-project-artifacts_b:
  ./target/check_b-result

check-project-artifacts_c:
  ./target/check_c-result

一部のチェッカについて、実行時間が長いので並列化を検討する

check_b が長時間かかっているようなので、チェック対象となるプロジェクト成果物をある程度の単位(ここでは dirA,dirB,dicC)に切り分け、ジョブをさらに細かく分割する。

  • check-project-artifacts
    • check-project-artifacts_check_a
    • check-project-artifacts_check_b
      • check-project-artifacts_check_b_dirA (分割後)
      • check-project-artifacts_check_b_dirB (分割後)
      • check-project-artifacts_check_b_dirC (分割後)
    • check-project-artifacts_check_c

check-project-artifacts_check_bcheck-project-artifacts を分割した時と同様、分割したジョブが完了したら分割前の親ジョブを呼び、親ジョブで子ジョブの成果物を回収する。

ただし、分割された子ジョブの成果物はそのままでは分割されていて見づらいため、親ジョブで分割された成果物を結合する処理を実施する。

check-project-artifacts_check_b_dirA:
  ./target/dirA_check_b-result

check-project-artifacts_check_b_dirB:
  ./target/dirB_check_b-result

check-project-artifacts_check_b_dirC:
  ./target/dirC_check_b-result

check-project-artifacts_check_b:
  ./target/dirA_check_b-result (結合用)
  ./target/dirB_check_b-result (結合用)
  ./target/dirC_check_b-result (結合用)
  ./target/check_b-result (上記3ファイルを結合したもの)

(ほかのプロジェクトは省略)

で、何が言いたいの?

Jenkinsのジョブがこうやって成長と複雑化を遂げることを言いたかった。

また、上記のようなジョブの分割といった場合は前のエントリで紹介しているように、Job DSL Pluginを使うと簡単に好きなだけコントロール可能な方法で分割できます。