DigdagをWindows環境で使う際に発生するデコードエラーを解消する

# はじめに

前回に引き続きDigdagについてである。前回の最後の方で別の問題として、UnicodeDecodeErrorが発生すると書いた。今回の改修を施して自分の環境では正常化したため、忘備録として記事にまとめた。

# 発生した事象

digdag.env.paramsやraiseしたときのエラーメッセージにマルチバイト文字が含まれる場合、セットされた以降のタスクでは次のようなエラーが発生していた。

本来ユーザー側で作成したフローの実装では想定していない、システム的なエラーである。

```shell
2022-10-25 11:13:57 +0900 [INFO] (0020@[0:default]+FCM0022^error+error01): echo>: ***This job is error***
***This job is error***
2022-10-25 11:13:58 +0900 [INFO] (0020@[0:default]+FCM0022^error+sendmail): py>: SUBCOMMON.notifier.sendmail_error
Traceback (most recent call last):
  File "C:\hoge\digdagtest\.digdag\tmp\digdag-py-12-9218885547896916785\runner.py", line 14, in <module>
    in_data = json.load(f)
  File "C:\ProgramData\Anaconda3\envs\py310\lib\json\__init__.py", line 293, in load
    return loads(fp.read(),
UnicodeDecodeError: 'cp932' codec can't decode byte 0x86 in position 1422: illegal multibyte sequence
2022-10-25 11:13:58 +0900 [ERROR] (0020@[0:default]+FCM0022^error+sendmail): Task failed with unexpected error: Python command failed with code 1
java.lang.RuntimeException: Python command failed with code 1
        at io.digdag.standards.operator.PyOperatorFactory$PyOperator.runCode(PyOperatorFactory.java:175)
        at io.digdag.standards.operator.PyOperatorFactory$PyOperator.runTask(PyOperatorFactory.java:115)
```

Windows環境でPythonを使う際にしばしば発生する（やらかす）UnicodeDecodeErrorである。

## 直接の原因

ローカルでの実行で生成される `.digdag/tmp`の中の各フォルダ内にある runner.py。これでエラーが出ているので見てみる。

```python:runner.py
# 14行目
with open(in_file) as f:
    in_data = json.load(f)
    params = in_data['params']

```

エンコーディングが自動判定されて予期せぬ現象が起きるのも納得。だから次のように修正した。

```python:runner.py
# 14行目
with open(in_file, encoding="UTF-8") as f:
    in_data = json.load(f)
    params = in_data['params']

```

ここで開かれるファイルは runner.pyと同じ階層にある `input.json` なようだ。WindowsではCのUsersの奥深いTempフォルダ内に一時的に生成され、ローカルで起動がかかっている模様。システム的に作られるのでエンコーディングは固定でいい気がするので、UTF-8にした。

単独で実行し、正常にinput.jsonが読み込まれたので、いよいよdigdagの本物のソースを変更する。

|公式リポジトリ内の runner.py の場所|
| - |
| digdag/digdag-standards/src/main/resources/digdag/standards/py/runner.py |

これを修正する。
なお、rubyは今回対象外なので、もしrubyでも同じような問題が起きる場合は `rb/runner.rb` を編集すればいいはず。


# 改修する準備

## Javaのセットアップと公式手順

問題部分の改修をする前に Digdagの [公式リポジトリ](https://github.com/treasure-data/digdag/) からソース一式をダウンロードしておく。
それから上記の修正をし、下記手順に進む。

なお、Windowsの素の環境では諸々面倒なので、WSLでUbuntu を入れてその中で環境のセットアップするとよい。

```shell
#途中でパスを書き換えることになるので公式手順を少し進めておく
$ curl -L git.io/nodebrew | perl - setup
$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bashrc
$ source ~/.bashrc
$ nodebrew install-binary v12.x
$ nodebrew use v12.x
# ^^^公式手順ここまで
# Javaのセットアップ（ビルドには Java 8が必要なので）
$ sudo apt update
$ sudo apt install openjdk-8-jre
$ sudo apt install openjdk-8-jdk
$ java --version
$ javac -version
$ sudo update-alternatives --config java
$ vi ~/.bashrc
```

```bash:.bashrc
#最後の方に次を記述
export JDK_HOME="/usr/lib/jvm/java-8-openjdk-amd64/"

export PATH=$HOME/.nodebrew/current/bin:$JDK_HOME:$PATH
```

```shell:シェルの続き
$ cd [digdagリポジトリをクローンしたディレクトリ]
$ chmod ugo+w gradlew
# 途中でこれをしろ！と一度怒られたので事前にこれを実行しておく
$ npx browserslist@latest --update-db
# ビルド実行
$ ./gradlew cli
$ cd digdag-cli/build/libs
```

ここまですると、最後に移動したディレクトリに `digdag-cli-0.10.4-all.jar` などというファイルが生成される。これができていればOK。

## Windows向けの追加設定

公式で配布されている `digdag.bat`は、jarファイルに複数行のバッチスクリプトが埋め込まれている。これをバイナリエディタ等で埋め込んでも公式プログラムを再現できなかったので、そのバッチスクリプトとJarファイルを完全に分けることにした。

個別に用意するバッチファイルは次の通り。（ほぼ全て埋め込まれているバッチスクリプト通り）
今回向けに書き換えた箇所には★でコメントを記した。

```bat:digdag-custom.bat

: <<BAT
@echo off

setlocal

rem this=%~f0
rem ★これに書き換え
set this=%~dp0
set thisjar=digdag-cli-0.10.4-all.jar
rem ★ここまで
set java_args=
set default_optimize=
set overwrite_optimize=
set status=
set error=
set args=

rem In jar file, cannot goto ahread for some reason.

for %%a in ( %* ) do (
    call :check_arg %%a
)

if "%error%" == "true" exit /b 1

set optimize=false
if "%overwrite_optimize%" == "true" (
    set optimize=true
) else (
    if "%default_optimize%" == "true" (
        if not "%overwrite_optimize%" == "false" (
            set optimize=true
        )
    )
)

if "%optimize%" == "true" (
    set java_args=-XX:+AggressiveOpts -XX:+UseConcMarkSweepGC -Djdk.attach.allowAttachSelf=true %java_args%
) else (
    set java_args=-XX:+AggressiveOpts -XX:TieredStopAtLevel=1 -Xverify:none -Djdk.attach.allowAttachSelf=true %java_args%
)

rem java -Dio.digdag.cli.launcher=selfrun %java_args% -jar "%this%" %args%
rem ★次に書き換え
java -Dio.digdag.cli.launcher=selfrun %java_args% -jar "%this%\%thisjar%" %args%


endlocal

exit /b

:check_arg
set arg=%*

rem Remove double quotations
set p1=%arg:~0,1%
set p1=%p1:"=%
set p2=%arg:~-1,1%
set p2=%p2:"=%
set arg=%p1%%arg:~1,-1%%p2%

if "%status%" == "rest" (
    set args=%args% "%arg%"

) else if "%status%" == "read" (
    call :read_file %arg%

) else if "%arg%" == "-J+O" (
    set overwrite_optimize=true
    set status=rest

) else if "%arg%" == "-J-O" (
    set overwrite_optimize=false
    set status=rest

) else if "%arg:~0,2%" == "-J" (
    if not "%arg:~2%" == "" (
        set java_args=%java_args% %arg:~2%
    ) else (
        set status=read
    )

) else (
    set args=%args% %arg%
    set status=rest
)
exit /b

:read_file
if not exist "%~1" (
    echo "failed to load java argument file."
    set error=true
) else (
    for /f "delims=" %%i in (%~1) do set java_args=%java_args% %%i
)
set status=
exit /b

BAT

java_args=""
default_optimize=""
overwrite_optimize=""

while true; do
    case "$1" in
        "-J+O")
            overwrite_optimize="true"
            shift
            break;
            ;;
        "-J-O")
            overwrite_optimize="false"
            shift
            break;
            ;;
        -J*)
            v="${1#-J}"
            if test "$v"; then
                java_args="$java_args $v"
            else
                shift
                file_args=`cat "$1"`
                if test $? -ne 0; then
                    echo "Failed to load java argument file."
                    exit 1
                fi
                java_args="$java_args $file_args"
            fi
            shift
            ;;
        *)
            break
            ;;
    esac
done

if test "$overwrite_optimize" = "true" -o "$default_optimize" -a "$overwrite_optimize" != "false"; then
    java_args="-XX:+AggressiveOpts -XX:+UseConcMarkSweepGC -Djdk.attach.allowAttachSelf=true $java_args"
else
    java_args="-XX:+AggressiveOpts -XX:TieredStopAtLevel=1 -Xverify:none -Djdk.attach.allowAttachSelf=true $java_args"
fi

exec java -Dio.digdag.cli.launcher=selfrun $java_args -jar "$0" "$@"
exit 127
```

このバッチではあくまでも .bat と .jarファイルが同じフォルダにあるという前提で記述している。
実際に運用で異なる場合は %this% の値を別のフォルダに置き換えてやればよい。

`thisjar`の値は実際にビルドして生成されたjarファイルの名前に書き換える必要があるが、それ以外はこのままでOK。


ここまでおこなって、どこかパスの通ったフォルダに次の２つを置く。

* digdag-custom.bat
* digdag-cli-0.10.4-all.jar

そしてあとはオリジナルの `digdag.bat` に代わって `digdag-custom` を使ってrunしたりserverして使う。



# 改修版で実行した例

UnicodeDecodeErrorが発生したワークフローをもう一度流してみる。

```shell
2022-10-25 14:22:37 +0900 [INFO] (0020@[0:default]+test02^error+msg): echo>: **This flow is error.**
**This flow is error.**
2022-10-25 14:22:37 +0900 [INFO] (0020@[0:default]+test02^error+sendmail): py>: notifier.sendmail_error
warning_not_data= True
errmsg= py: [C:\hoge\digdagtest\test01\data\ret2.csv] not found.ほげらん煽るんabc+3$92
C:\foo\maildata\errmail01.yaml
error:
  * +test02+step4+check01^sub:
    JOB [judgetest] don't find ret2csv !!

Task state is saved at C:\hoge\digdagtest\.digdag\status\20221025T000000+0900 directory.
  * Use --session <daily | hourly | "yyyy-MM-dd[ HH:mm:ss]"> to not reuse the last session time.
  * Use --rerun, --start +NAME, or --goal +NAME argument to rerun skipped tasks.
```

改修前は _error:パラメータ内で起きていた余計なエラーが起きていない。想定した本来の処理のエラーメッセージのみが出力された。

独自ビルドしたjarでも動作には問題なさそうだ。

なお、 digdag-custom serverをして同じ余計なエラーが起きていたワークフローを実行し直したところ、server状態でも問題が解決した。


# おわりに

自分の作った.digや Pythonファイルでデコードエラーが発生したなら自分のせいだし直しようがあるが、公式プログラムに埋め込まれたものでこうしたエラーが起きると、ただ使うだけのユーザー側では対処が大変だ。

幸いエラーメッセージに runner.pyの問題箇所が記されていたため改修が容易だったし、公式リポジトリの説明どおりに操作すればビルドまでできたので良しとしよう。