🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

MicroBatchFrameworkとSeleniumでCLIツールを作った話(その1)

ASP.NET 2.0 時代(!)に作られた社内Webサービスを自動操作するCLIアプリをMicroBatchFramework + Seleniumで作ったのでその備忘録です。

背景

対象の社内Web サービスはその日作業した作業の製番や、工数を入力するWebサービスです。いかにも業務アプリっぽい見た目のUIで、毎日使う割にはちょっと入力が大変でした。

幸いにもExcelファイルをアップロードして工数情報を一括入力する機能があるので、アプリから自動でExcelファイルをアップロードするCLIアプリを作りました。

言語とライブラリ

  • .NET Core 3.0
  • MicroBatchFramework
  • Selenium
  • SeriLog

Webの操作はSeleniumを使用します。自動操作中の画面は表示させたくないのでドライバはChromeDriverを使用します。
Chromeには画面非表示で自動操作する--headlessモードがあります

MicroBatchFrameworkはC#大統一論でお馴染みの@neueccさんが作成されたCommandLineParserです。Generic Hostな書き心地でCLIが書けるため、C#大統一論信者の私にはぴったりです。

MicroBatchFrameworkのリポジトリはこちらです。

github.com

Project の作成

プロジェクトを作って、必要なパッケージを追加します。

dotnet new console -o HogeBot
cd HogeBot
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Options.ConfigurationExtensions
dotnet add package MicroBatchFramework
dotnet add package Selenium.Support
dotnet add package Selenium.WebDriver
dotnet add package Selenium.WebDriver.ChromeDriver
dotnet add package Serilog.Extensions.Logging
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File

csporj の変更

下記仕様に沿い.csprojを変更します

  • 配布するファイルは少な目にしたい
  • サイズは軽くしたい
  • 起動は早くしたい
  • 対象OSはWindows10 x64に絞る
  • appsettings.jsonを同封する
  • 発行時にChromeDriverを同封する
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
+   <PublishSingleFile>true</PublishSingleFile>
+   <RuntimeIdentifier>win10-x64</RuntimeIdentifier>
+   <PublishReadyToRun>true</PublishReadyToRun>
+   <PublishTrimmed>true</PublishTrimmed>
+   <WebDriverPlatform>win32</WebDriverPlatform>
+   <PublishChromeDriver>true</PublishChromeDriver>
+   <AssemblyVersion>1.0.0.0</AssemblyVersion>
+   <FileVersion>1.0.0.0</FileVersion>
+   <Version>1.0.0.0</Version>
  </PropertyGroup>

  ...

+ <ItemGroup>
+   <None Update="appsettings.json">
+     <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+   </None>
+ </ItemGroup>

</Project>

ChromeDriverはWebDriverPlatformPublishChromeDriverを指定することで発行時にbinフォルダからpublishフォルダにコピーしてくれるようです。

上記挙動は@jsakamotoさんのnupkg-selenium-webdriver-chromedriverによるもののようです。 頭が上がりませんね。

コーディング

あとはちょいちょいとコーディングしていきます。

Program.cs

  • appsettings.jsonはExeと同じパスに同盟のファイルが存在する場合はそちらのファイルを優先するよう、以下の感じで実装しています。
  var currentDir = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
  //途中省略
  if (File.Exists(Path.Combine(currentDir, APP_CONFIG_FILE))) config.SetBasePath(currentDir);
  • Logファイル出力先はExeと同じパスに出力したいので、Logのパスの指定はProgram.cs内で指定します。 Consoleに対するLoggingFilterの設定はappsettings.jsonから読み込むようにしています。
using System;
using System.Diagnostics;
using System.IO;
using Serilog;
using MicroBatchFramework;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using HogeBot.Models;
using HogeBot.Interfaces;
using HogeBot.Services;

namespace HogeBot
{
    class Program
    {
        static readonly string APP_CONFIG_FILE = "appsettings.json";
        static readonly string APPLICATION_SETTING_SECTION = "ApplicationSetting";
        static readonly string LOG_FILE = "Log.log";

        static async System.Threading.Tasks.Task Main(string[] args)
        {
            // Exeのあるパスを取得
            var currentDir = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
            var host = BatchHost.CreateDefaultBuilder()
                                .ConfigureHostConfiguration(config =>
                                {
                                    // Exeと同じ場所にconfigがある場合はそちらを優先する
                                    if (File.Exists(Path.Combine(currentDir, APP_CONFIG_FILE))) config.SetBasePath(currentDir);
                                    config.AddJsonFile(APP_CONFIG_FILE);
                                })
                                .ConfigureServices((hostContext, services) =>
                                {
                                    services.Configure<AppSettingModel>(hostContext.Configuration.GetSection(APPLICATION_SETTING_SECTION));
                                    services.AddTransient<IAutoOperationable, ChromeBrowserAutoOperator>();
                                })
                                .ConfigureLogging((hostContext, logging) =>
                                {
                                    logging.ClearProviders();
                                    logging.AddSerilog();

                                    // Serilogの設定
                                    Log.Logger = new LoggerConfiguration()
                                                .Enrich.FromLogContext()
                                                .WriteTo.File(Path.Combine(currentDir, LOG_FILE))
                                                .ReadFrom.Configuration(hostContext.Configuration)
                                                .CreateLogger();
                                });

            await host.RunBatchEngineAsync<HogeBotBatchService>(args);
            Log.CloseAndFlush();
        }
    }
}

BatchService.cs

using System;
using System.Reflection;
using HogeBot.Interfaces;
using HogeBot.Models;
using MicroBatchFramework;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace HogeBot.Services
{
    public class HogeBotBatchService : BatchBase
    {
        private readonly AppSettingModel _config;
        private readonly ILogger<HogeBotBatchService> _logger;
        private readonly IAutoOperationable _operator;

        public HogeBotBatchService(IOptions<AppSettingModel> config, ILogger<HogeBotBatchService> logger, IAutoOperationable operator)
        {
            this._config = config.Value;
            this._logger = logger;
            this._operator = operator;
        }

        [Command("version", "Version情報を表示します")]
        public void ShowVersion()
        {
            var version = Assembly.GetExecutingAssembly()
                                    .GetCustomAttribute<AssemblyFileVersionAttribute>()
                                    .Version;
            Console.WriteLine($"Version:{version}");
        }

        [Command(nameof(Import), "Excelファイルの工数情報をWebサービスへインポートします。")]
        public void Import([Option("f", "Excelファイルのパスを指定.")]string filePath)
        {
            try
            {
                _operator.AutoOperate(_config.URI, filePath);
            }
            catch (Exception)
            {
                _logger.LogError("Import Failed.");
                throw;
            }
        }
    }
}

おわり

MicroBatchFrameworkはversion 1.5でExitCodeも対応されたこともあって
個人的には小規模なCLIならMicroBatchFramework一択って感じです。

Seleniumの話はまた別の記事で触れたいと思います。