もろもろ

もろもろ書いていきます。気軽にコメントください^_^

ASP.NET Core 3.1 + React.js + Docker で npm install や npm start が失敗する件

↓のページを参考に ASP.NET Core 3.1 + React.js + Docker を試していますが、Debug 構成での実行は (何故か) 出来るものの、Dockerfile のビルドを直接行ったり Release 構成での実行や発行を行うと失敗してしまいました。

docs.microsoft.com

原因は2つあって、一つは .NET Core SDK の Docker イメージに node.js が含まれていないことだと思われます。

1>Microsoft (R) Build Engine version 16.4.0+e901037fe for .NET Core
1>
1>Copyright (C) Microsoft Corporation. All rights reserved.
1>  Restore completed in 48.96 ms for /src/WebApplication1/WebApplication1.csproj.
1>  WebApplication1 -> /src/WebApplication1/bin/Release/netcoreapp3.1/WebApplication1.dll
1>  WebApplication1 -> /src/WebApplication1/bin/Release/netcoreapp3.1/WebApplication1.Views.dll
1>  /bin/sh: 2: /tmp/tmpa4ca4172ac674b57a69cab6fbb18beea.exec.cmd: npm: not found
1>/src/WebApplication1/WebApplication1.csproj(38,5): error MSB3073: The command "npm install" exited with code 127.
1>Removing intermediate container 965d653d9ca2
1>The command '/bin/sh -c dotnet publish "WebApplication1.csproj" -c Release -o /app/publish' returned a non-zero code: 1
1>D:\source\repos\WebApplication1\WebApplication1\dockerfile : error CTC1014: Docker コマンドが終了コード 1 で失敗しました。
1>D:\source\repos\WebApplication1\WebApplication1\dockerfile : error CTC1014: The command '/bin/sh -c dotnet publish "WebApplication1.csproj" -c Release -o /app/publish' returned a non-zero code: 1
1>プロジェクト "WebApplication1.csproj" のビルドが終了しました -- 失敗。
========== すべてリビルド: 0 正常終了、1 失敗、0 スキップ ==========

なので "base" ステージだけでなく "build" ステージでも node.js をインストールすることで解決します。
(関係無いですが、ついでに Node.js のバージョンは 12.x 系に上げてます。)

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
RUN curl -sL https://deb.nodesource.com/setup_12.x |  bash -
RUN apt-get install -y nodejs

FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
RUN curl -sL https://deb.nodesource.com/setup_12.x |  bash -
RUN apt-get install -y nodejs
COPY ["WebApplication1/WebApplication1.csproj", "WebApplication1/"]
RUN dotnet restore "WebApplication1/WebApplication1.csproj"
COPY . .
WORKDIR "/src/WebApplication1"
RUN dotnet build "WebApplication1.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "WebApplication1.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebApplication1.dll"]

もう一つは、React の開発用サーバー起動時に Node.js が package.json を見つけられないことです。

      An unhandled exception has occurred while executing the request.
System.AggregateException: One or more errors occurred. (One or more errors occurred. (The NPM script 'start' exited without indicating that the create-react-app server was listening for requests. The error output was: npm ERR! code ENOENT

npm ERR! syscall open
npm ERR! path /app/ClientApp/package.json
npm ERR! errno -2
npm ERR! enoent ENOENT: no such file or directory, open '/app/ClientApp/package.json'
npm ERR! enoent This is related to npm not being able to find a file.
npm ERR! enoent 


npm ERR! A complete log of this run can be found in:

))
 ---> System.AggregateException: One or more errors occurred. (The NPM script 'start' exited without indicating that the create-react-app server was listening for requests. The error output was: npm ERR! code ENOENT

npm ERR! syscall open
npm ERR! path /app/ClientApp/package.json
npm ERR! errno -2
npm ERR! enoent ENOENT: no such file or directory, open '/app/ClientApp/package.json'
npm ERR! enoent This is related to npm not being able to find a file.
npm ERR! enoent 


npm ERR! A complete log of this run can be found in:

)
 ---> System.InvalidOperationException: The NPM script 'start' exited without indicating that the create-react-app server was listening for requests. The error output was: npm ERR! code ENOENT

npm ERR! syscall open
npm ERR! path /app/ClientApp/package.json
npm ERR! errno -2
npm ERR! enoent ENOENT: no such file or directory, open '/app/ClientApp/package.json'
npm ERR! enoent This is related to npm not being able to find a file.
npm ERR! enoent 


npm ERR! A complete log of this run can be found in:


 ---> System.IO.EndOfStreamException: Attempted to read past the end of the stream.
   at Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer.ReactDevelopmentServerMiddleware.StartCreateReactAppServerAsync(String sourcePath, String npmScriptName, ILogger logger)
   --- End of inner exception stack trace ---
   at Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer.ReactDevelopmentServerMiddleware.StartCreateReactAppServerAsync(String sourcePath, String npmScriptName, ILogger logger)
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at System.Threading.Tasks.Task`1.get_Result()
   at Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer.ReactDevelopmentServerMiddleware.<>c.<Attach>b__2_0(Task`1 task)
   at System.Threading.Tasks.ContinuationResultTaskFromResultTask`2.InnerInvoke()
   at System.Threading.Tasks.Task.<>c.<.cctor>b__274_0(Object obj)
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at System.Threading.Tasks.Task`1.get_Result()
   at Microsoft.AspNetCore.SpaServices.Extensions.Util.TaskTimeoutExtensions.WithTimeout[T](Task`1 task, TimeSpan timeoutDelay, String message)
   at Microsoft.AspNetCore.SpaServices.Extensions.Proxy.SpaProxy.PerformProxyRequest(HttpContext context, HttpClient httpClient, Task`1 baseUriTask, CancellationToken applicationStoppingToken, Boolean proxy404s)
   at Microsoft.AspNetCore.Builder.SpaProxyingExtensions.<>c__DisplayClass2_0.<<UseProxyToSpaDevelopmentServer>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

こちらは今のところ直接的な解決方法はわかっていませんが、React の開発用サーバーを使わないよう Startup.cs を変更することで回避は可能なようです。

app.UseSpa(spa =>
{
    spa.Options.SourcePath = "ClientApp";
    //if (env.IsDevelopment())
    //{
    //    spa.UseReactDevelopmentServer(npmScript: "start");
    //}
});

ペアプロ・モブプロのすゝめ

少し前に社内でペアプロ・モブプロを導入するために作成したプレゼン資料です。
社外でも使用できるようあえてプライベートで作成したものなので、この場でも公開しておきます。

ちなみに、今回導入したのはとある Working Group に対してだったのですが、WG の活動そのものに対しては僕はやや特殊な立ち位置で、あまり直接的には関われていません。 なので話で聞く程度ですが、どうやらペアプロ・モブプロの効果を最大限に発揮するまでは至っていないようです。
ただ、これはある意味当然で、もろもろの事情からふりかえりをまだ導入できていないことが大きいと思われます。
ペアプロ・モブプロに限らず、アジャイルラクティスのほとんどはふりかえりによってチームに溶け込んでいきます。

ペアプロ・モブプロのふりかえり方としては、この資料でいうところの "進め方""有能なペアプログラマの7つの習慣"指標として掲げ、たまにふりかえりの最初に読み上げるのが良いと思います。あとさいごに書いてある通り、"楽しく取り組めているか" も重要です。
手法としては KPTA (KPT+A) を採用するのが良いと思います。 ふりかえりのふりかえりとして Plus/Delta も併せて導入すると尚良いです。

ラズパイに Windows 10 ARM64 をインストール

WOA Deployer for Raspberry Pi を使って Micro SD Card に Windows 10 ARM64 を展開するまでの手順です。
WOA Deployer for Raspberry Pi のサイト内に入手方法の丁寧な解説がありますが、一応日本語で書き直しておきます。
一連の手順は、全て Windows 10 上で行ってください。

Windows 10 ARM64 (WOA)

  1. 上記のサイトを開く。
  2. [Browse a full list of known builds] をクリック。
    f:id:YokoKen:20190901233314p:plain
  3. インストールしたい ARM64 用ビルドを選択。通常は最新の Insider ビルドで OK。 (AMD64 用と間違えないように!)
    f:id:YokoKen:20190901233635p:plain
  4. 言語とエディションを選択 (通常は Japanese / Windows 10 Professional) し、[Download using aria2 and convert] をクリックしてダウンロード。
    f:id:YokoKen:20190901234049p:plain
    f:id:YokoKen:20190901234236p:plain
    f:id:YokoKen:20190901234421p:plain
  5. ダウンロードした zip ファイルを、"C:\temp\W10IsoScripts" などのパスにスペースを含まない空フォルダに展開する。
  6. 展開されたフォルダ内の "aria2_download_windows.cmd" ファイルを実行し、完了するまで待つ。(1時間前後)
  7. 完了したら 0 を入力して閉じる。
    f:id:YokoKen:20190901235220p:plain
  8. "aria2_download_windows.cmd" と同じ場所に iso ファイルが作成されるので、ダブルクリックしてマウントする。
    f:id:YokoKen:20190901235547p:plain
  9. マウントされたドライブの sources フォルダに install.wim ファイルが含まれていることを確認する。
    f:id:YokoKen:20190902001445p:plain
    f:id:YokoKen:20190902001523p:plain

WOA Deployer for Raspberry Pi

  1. 上記のサイトを開く。
  2. ページ中段の Download It のリンク先から最新版の WOA.Deployer.zip ファイルをダウンロードする。
  3. ダウンロードした zip ファイルを任意の場所に展開する。
  4. 展開されたフォルダ内の "WoaDeployer.exe" ファイルを実行する。
    f:id:YokoKen:20190902003818p:plain
  5. WOA の展開先となるドライブ (Micro SD Card)、WOA Image 内の install.wim の場所、WOA のエディション (Windows 10 Pro) を指定。
    f:id:YokoKen:20190902002514p:plain
  6. [Deploy] をクリックし、Micro SD Card 以外のドライブを選択していないか再度確認した上で 展開を実行する。 (1時間前後)
    f:id:YokoKen:20190902002553p:plain
    f:id:YokoKen:20190902020007p:plain

できあがり

以上で、WOA が展開された Micro SD Card が完成しました。
なお、ここから先はまだ試していないためうまく起動するかはわかりません。 → 起動できました。

VSCode の Task を PowerShell スクリプトで実装する

VSCode の Task を PowerShell スクリプトで実装する方法です。
tasks.json に直接 PowerShell コードを記述することも出来るのですが、セミコロン区切り、多重エスケープなど、中々面倒です。 それよりも、tasks.ps1 というように PowerShell スクリプト化して tasks.json から呼び出す形の方が圧倒的に楽です。

ちなみに僕のところだと、リモートにソースコードやテストスクリプトを同期するために "SFTP" 拡張機能を使っているのですが、この設定ファイルを簡単に構成するためのタスクを PowerShell スクリプト化して用意しています。("Remote Development" 拡張機能Linux 32bit が非サポートな関係で使用を断念)
ここではこれを簡略化したサンプルコードを使って要点を押さえていきましょう。

では、早速サンプルコードです。

.vscode/tasks.json

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "type": "shell",
    "options": {
        "shell": {
            "executable": "powershell.exe"
        }
    },
    "presentation": {
        "echo": false,
        "reveal": "always",
        "focus": true,
        "panel": "shared",
        "showReuseMessage": true,
        "clear": true
    },
    "problemMatcher": [],
    "inputs": [
        {
            "id": "hostAddress",
            "type": "promptString",
            "description": "Host Name or IP Address",
        }
    ],
    "tasks": [
        {
            "label": "Configure SFTP",
            "command": [
                "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process;",
                ". .vscode/tasks.ps1;",
                "ConfigureSFTP ${input:hostAddress};"
            ],
        },
    ],
}

.vscode/tasks.ps1

$ErrorActionPreference = "Stop"

function ConfigureSFTP {
    param(
        [string]$hostAddress
    )
    
    $Host.UI.WriteLine("[Configure SFTP]")
    $config = [PSCustomObject]@{
        name         = "My Server";
        host         = $hostAddress;
        protocol     = "sftp";
        port         = 22;
        username     = ${env:username};
        remotePath   = "/";
        uploadOnSave = $true;
        ignore       = @(".git");
    }
    $json = (ConvertTo-Json $config).Replace("`r`n", "`n")
    [IO.File]::WriteAllText(".vscode/sftp.json", $json)
    $Host.UI.WriteLine("Configured: .vscode/sftp.json")
    
    $Host.UI.WriteLine("Completed.")
    $Host.UI.WriteLine()
    $Host.UI.WriteLine("Please apply the config with the following steps.")
    $Host.UI.WriteLine("1. Execute `"sftp: config`" on the command palette.")
    $Host.UI.WriteLine("2. Re-save the opened `"sftp.config`" file.")
}

tasks.json について

トップレベルの設定

タスクを主に PowerShell で実装していくつもりなら、このサンプルのように "type" や "options" を tasks.json のトップレベルで宣言してしまいましょう。各タスクの中で設定をオーバーライドすることもできるので、PowerShell 以外で実装するタスクを共存させたい場合も問題ありません。

"presentation" の詳細はここではあまり触れません。詳細はこちらをご参照ください。
僕は実行コマンドのエコーや過去の出力履歴を邪魔に感じたので、"echo" を false、"clear" を true に設定しています。あと、タスク内から ssh コマンドや scp コマンドを実行すると初回接続時に入力を求められたりするので、"focus" も予め true にしています。

タスクの入力パラメータ

"inputs" はタスクの入力パラメータ宣言です。ここでは hostAddress という名前の入力パラメータを宣言しています。タスク内で ${input:hostAddress} というように使用することができ、そのタスクの実行時にユーザー入力が求められるようになります。
詳細はこちらをご参照ください。

PowerShell スクリプトの呼び出し

ここからが肝です。
タスク実行時に tasks.ps1 の ConfigureSFTP 関数が実行されるよう、"command" に PowerShell コードを記述していきます。
"command" は文字列もしくは文字列の配列で指定できるのですが、配列で指定した場合でも全て一行に繋げられて実行されてしまうため、文末にセミコロンが必要になることに気を付けてください。

まず、タスク実行時にのみスクリプト実行を自動で一時的に許可するよう Add-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process; を実行します。
PowerShell はデフォルトだとスクリプトの実行が許可されていません。この設定は管理者権限で Set-ExecutionPolicy コマンドを実行することで恒久的に変更することができますが、開発メンバー全員に予め設定変更しておいてもらうというのはいくつかの理由から (特にセキュリティの観点で) 面倒です。
この方法であれば、事前設定や管理者権限、そしてセキュリティの考慮も不要です。タスク実行時にのみ反映され、他には一切影響を与えません。

続いて、tasks.ps1 を実行します。タスク実行時のカレントディレクトリは Workspace フォルダ、スクリプト内の関数のロードが目的なのでドットソースで実行、となりますので . .vscode/tasks.ps1; という形で実行します。

最期に、ロードされた PowerShell 関数を実行します。ここでは ConfigureSFTP 関数を、第一引数にタスクの入力パラメータ "hostAddress" を指定して実行する必要がありますので、ConfigureSFTP ${input:hostAddress}; という形で実行します。

tasks.ps1 について

$ErrorActionPreference

関数実行中に例外が発生した場合には処理をエラー終了させたいので、$ErrorActionPreference に "Stop" を設定しておきます。

関数

PowerShell スクリプトファイルをタスク毎に用意するのは嫌なので、関数化して tasks.ps1 に全てのタスクを集約します。各タスクは tasks.ps1 をドットスコープでロードして、対応する関数を個別に実行します。

$Host.UI

$Host.UI を使用することで VSCode のターミナルに出力することができます。WriteLine()、WriteWarningLine()、WriteErrorLine() を使い分けましょう。(WriteWarningLine() だけは、行頭に "警告: " とか "WARNING: " を勝手に付加するのでご注意ください。)
入力を受け付けることもできますが、基本的にはタスクの入力パラメータで充分なはずです。

ファイル選択ダイアログ

サンプルコードには含んでいませんが、下記のサンプルコードのように OpenFileDialog クラスを使ってファイル選択ダイアログを利用することも可能です。

[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
$dialog = New-Object System.Windows.Forms.OpenFileDialog
$dialog.Filter = "json files (*.json)|*.json"
if ($dialog.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) {
    $Host.UI.WriteErrorLine("Cancelled.")
    return
}
$contents = Get-Content $dialog.FileName
$Host.UI.WriteLine($contents)

さいごに

これで皆さんも PowerShellVSCode タスクを作成することができるようになったと思います。
機会がありましたら是非ご活用ください。

技術とかもろもろ

久しぶりにブログを始めてみました。
大分前は↓でブログやってました。

C#と諸々

一応永遠のC#erを自称していますが、残念ながら組み込み系の業務に専任になってからC#はほとんど触っていません。 以来C++をメイン、Pythonを少し使っていますが、オフショア開発なのでたまにプロトタイプコードを書く程度、そもそも愛着が無いのでブログではあまり扱わないかも。

余所はよく知りませんが、うちはオフショア先がグループ内の会社ということもあり割と色々口出し出来るので、最近はアジャイル開発の導入推進活動を細々とやっています。
当面はその周辺のネタを書ければと思います。
アジャイルマスターとかでは全然ないです。

技術以外のネタももろもろ書いてくつもりですので、よろしくお願いします。