みなさんExcelマクロでツールを作るとき、ユーザ入力を受け取るために意味もなくダイアログ開いてませんか?取り扱い説明書を別に作っていませんか?
ちょっと力んでいますね。そのマクロツールがプログラマ向けであるなら、力む必要はないのです。
力を抜いて、このスタイリングに従えば幸せになれます。
このスタイリング方法は、私がいつもやっている方法で、今のところ不満を聞いたことがありません。
僕の仕事の一環で他人の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 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 {} finally {}
だけでもいいのです。
// (そもそもtry-with-resources使えよっていうツッコミは無しで) Connection c = null; try { c = acquireConnection(); transactionalProcessing(c); } catch (HogeException e) { throw e; // 何もしないのにcatchして例外をもっかい投げるのは無駄 } finally { closeQuietly(c); }
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.unmodifiableList
・ Arrays.asList
の組み合わせはよく使うので、どっかにそれ用のユーティリティ用意しておくのが普通です。
java.util.Objects
クラス使いましょう。
// String lhs; // String rhs; if (Objects.equals(lhs, rhs)) { // lhs == null && rhs == null の場合もOK }
メソッドのまとめ方ですが、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やってた頃でとまってる。
今日のところはこれぐらいにしておいてやるよ!
たとえば、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 (シングルクォートのみ書く) '
#Jenkins のSSH型スレーブのスレーブ自体のjavaプロセスの環境変数を設定したい場合は
prefix: sh -c 'export LC_ALL=en_US.utf-8;
suffix: '
ってやるといいです。
スレーブで文字化けに困ってる場合など。
— (call me #'knjname) (@knjname) 2015, 2月 26
@knjname こうなるのだ pic.twitter.com/cUCnZ7WUUd
— (call me #'knjname) (@knjname) 2015, 2月 26
かれこれ1年以上、Jenkinsで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をコマンドラインから起動させる方法については、たぶん以下の方法が考えられます。
cscript.exe
とかで)僕はPowerShellで呼び出す、という選択肢を選びました。
Windows7以降は標準でバージョン ≧2.0 が使えますし、Jenkinsのビルドプロセス中、VBAの呼び出しといったタスク以外にも、 PowerShellを使った複雑な処理が多少は必要になる、あるいは複雑な処理とVBAが絡みあう可能性もあり、 PowerShellがCOMと.NETがあわさった手軽かつ強力なスクリプティング環境になっているからですね。
PowerShellでExcelを呼び出すのは以下のコードだけでできます。
$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用入り口で対話的要素のフラグをOFFるための初期処理などさせればいいです。
PowerShell内で起動したExcelは基本的には自前で終了させるまで起動しっぱなしです。 PowerShellが終了したからといって自動的に強制終了させられたりはしません。(そうだったらどんなに楽だったか)
メモリの肥やしを消す意味でも、また、excel.exeが掴んでいるファイルロックを解除させるためにも、 マクロ実行後のExcelの後始末はちゃんとしましょう。
少なくとも下記の条件がすべて満たされていれば、後始末完了とみなして良いようです。
$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はそこで即座に強制終了させられ、内部で起動していたexcel.exeは死にません。走り続けてしまいます。
https://wiki.jenkins-ci.org/display/JENKINS/Aborting+a+build に書いてあるように、アボート時は子孫プロセスも殺すということで、 PowerShell内で起動したExcelなども死にそうですが、ExcelはPowerShellの子孫として起動しません。
じゃあfinally句あたりでExcelを殺しにいけばいいじゃないかというのも、
JenkinsのWindows版のプロセスキラーの実装(https://github.com/kohsuke/winp/blob/master/native/winp.cpp)が TerminateProcess
(https://msdn.microsoft.com/ja-jp/library/cc429376.aspx)を呼んでPowerShellを強制抹殺しにいくので不可能です。
try { // 処理… } finally { ' 強制終了させられたらfinallyもクソもなくPowerShellは終了。動作しているExcelのお掃除はできずに終了である。 }
このタイプの強制終了のトラップ(捕捉)はWindows上では不可能です。何もできません。
これで問題になるのは、たとえば、ブックやドキュメントを処理中に読み取り専用で開いたりしていなかった場合、アボートしたジョブのプロセスがファイルにロックを持ったままになってしまうことですね。 残ったままのプロセスをどうにかして殺さない限りロックが解除されることはありません。次のジョブが同じワークスペースで動いた時、問題になるでしょう。
そもそもロックなんか気にしなくて済むように、読み取り専用で開いて済むものはなるべくそれで処理するのが一番ですが、何かいい方法ないんですかねー。
これに限らずWindows上でのJenkinsというのは、だいたいファイルのロックに悩まされるのが定番です。 最悪の場合はプラグインか何かのせいで、Jenkinsのプロセス自体がワークスペース内のファイルにロックを持ってしまうこともあります。 特定のフォルダ以下のロックの強制解除方法探したほうがはやいかもしれません。
確かにそうなのですが、残念ながら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でのスクリプティングで、カレントディレクトリがワークスペースフォルダ直下以外になることは原則ないというように処理したほうがいいでしょう。
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をスムーズに起動できるでしょう。
もし同じことをもっとうまくやっている人がいたら、是非どうやっているか私に教えてほしいです…。
以前のエントリ( http://knjname.hateblo.jp/entry/2014/05/03/190842 )で自分でJenkinsのDockerイメージを作成したりしてみましたが、 Jenkins公式でDockerイメージを配布するようになったので、それを使用したほうがいいと思います。
普通に使うだけなら、下記のようにすればいいだけですが、
docker run -p 8080:8080 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を使わせるようにしたほうがいいでしょう。
永続化が必要なデータはコンテナ内の /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
する。/var/jenkins_home
をコピーしたものをあらためてマウントする。JavaのメモリetcのためのJVMオプションはJAVA_OPTS
という名前で環境変数に設定すればいいです。
docker run -p 8080:8080 -e JAVA_OPTS="-Xmx1g" 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
ほか、設定ファイルに永続化されておりパラメータでは初期指定できない情報や、 初期プラグインを勝手にインストールさせるなどを行わせる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のポートを変更するようになっています。良い改善だと思います。
だいたい上記でやりたいことはできます。
yum
やapt-get
で入れたパッケージ版の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は必要です。)
/var/jenkins_home
にroot権限で作成した外部ディレクトリをマウントさせると起動に失敗する件。…まあこれはDocker自体の問題に近いですが。
→ docker run
時に -u root
オプション付ければうまくいくことも多いでしょう(2015/09/09)Windowsでは長いパスを使うと呪いに遭います。
(個人的には英語の長ったらしいディレクトリ名とファイルパスをついたものをJenkinsでこれまた長ったらしいワークスペースフォルダにチェックアウトした時に遭遇している)
こんなディレクトリとファイルがあったとしましょう。
C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\ 01234567.xlsx a.xlsx b.xlsx
上記中の 01234567.xlsx
はWindowsで許されている最長のファイルパスを持つエントリです。
ただ、ファイルパスが長過ぎるためか、、、
Workbooks.Open()
で開くことができません。(アプリケーション側から開くのも不可能です。)この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にもあげてあります。
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;
させてほしい…。