tkhrsskの日記

技術ネタなど

RestTemplateとApache HttpComponents HttpClient

RestTemplateとApache HttpComponents HttpClientの関係やバージョンの対応について調べてみた

Apache HttpComponents HttpClient 5系で、API変更が走っていることにより、いろいろと混乱をしている。 下手に使えるAPIググると、古いバージョンの情報がひっかかって素人には辛い。

特にタイムアウト系の設定値の関係性がカオスに感じる。 下記に書いた図は、APIドキュメントを追ってメモとしてまとめたもので、 厳密な関係を表しているわけではありません。

www.javadoc.io

github.com

クラスの関係図

node.jsで音声再生

駄文です

node.jsで音声再生する方法を模索

Mac環境だと play-sound 使って、おそらくaplayerが実行できていたソースコードWindowsで実行したら再生できなくなっていた。

エラー情報も 1 とだけ吐き出していて意味不明でしたが、 play-sound は音声プレイヤーをコマンドラインを読み出していだけのようで、 Windowsだと明示的にインストールが必要そうでした。

ChatGPTに相談しながら解決を模索していました。

まず試したのはmplayerのインストール。 PATHを通して、コマンドラインでも再生できることを確認し、一応明示的にplay-soundにもプレイヤーを指定。

const playSound = require('play-sound');
const opts = {
  players: ['mplayer']
};
const play = playSound(opts);

うまく再生できるときもあるが、呼び出し方によって無限ループで再生するようになった。 キュー使った処理で呼び出しているところが影響しているのか?とかよくわからず。 playerOptions を指定するも反映がされていなさそう。

child_process の execSync で直接コマンド実行するようにしても、現象を解消されず。 違う方法を模索。

次にmpg123をインストールしてみたが、こちらは音がでない現象発生し、断念。

次に試したのは、ライブラリを変えて、audioplayerライブラリを使用する方法。 ところが、実行時にエラー。どうもブラウザ上での実行限定っぽい内容。断念。

次は、ライブラリでspeakerとnode-wavを使用する方法。 するとspeakerのインストール時にエラーが発生。 Visual StudioC++のワークロードが必要ということで、わざわざこのために10G超えの環境をインストール。 再生できるようになったが、ノイズが入ることがあり断念。

const fs = require('fs');
const wav = require('wav');
const Speaker = require('speaker');

const file = fs.createReadStream('file.wav');
const reader = new wav.Reader();

// WAVファイルのフォーマットが解析されたら、そのフォーマットでスピーカーを設定
reader.on('format', function (format) {
  this.pipe(new Speaker(format));
});

file.pipe(reader);

また play-soundで経由で実行できるプレイヤーを調査。Windows Media Playerも使えるっぽい情報あったが、コマンドラインでうまく動かず。

次にVLCを利用。再生できるが、GUIが起動してしまう。オプション指定が必要。 下記のように設定してみるが、前回の時と同じように反映されず。

const playSound = require('play-sound');
const opts = {
  players: ['vlc'],
  playerOptions: {
    vlc: ['-I', 'dummy', '--verbose', '2', '--play-and-exit'] // VLCに渡すオプション(必要に応じて変更)
  }
};
const play = playSound(opts);

結局、execSync利用にする。

const { exec } = require('child_process');

execSync('vlc.exe -I dummy --play-and-exit ' + filename, (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.error(`stderr: ${stderr}`);
});

一旦、この方法で落ち着いた。 Macと環境差分でてしまっているので、ラッパー化しておきたいと思った。

プロパティやシートのヘッダやフッタを出力するマクロ

エクセルのプロパティやシートの情報を取得するマクロを ガッと書きなぐってみた。

ほとんどChatGPTにベースは書いてもらっているけど。

Sub RecursiveGetExcelFileInfo(FolderPath As String, ByRef OutputRow As Long)
    Dim FileName As String
    Dim ExtName As String
    Dim wb As Workbook
    Dim ws As Worksheet
    Dim OutputCol As Long
    
    Debug.Print "[RecursiveGetExcelFileInfo][BEGIN]" & FolderPath
    
    ' FileSystemObject を作成
    Set fso = CreateObject("Scripting.FileSystemObject")
    Set folder = fso.GetFolder(FolderPath)
    
    ' フォルダ内の全てのファイルに対してループ
    For Each file In folder.Files
        FileName = fso.GetFileName(file.Name)
        ExtName = fso.GetExtensionName(file.Name)
        Debug.Print FileName
        If (FileName <> ".") And (FileName <> "..") And (FileName <> ThisWorkbook.Name) And Not (FileName Like "~$*") Then
            If LCase(ExtName) = "xls" Or _
               LCase(ExtName) = "xlsx" Or _
               LCase(ExtName) = "xlsm" Then
                
                Application.ScreenUpdating = False
                
                ' エクセルファイルの場合、処理
                Set wb = Workbooks.Open(FolderPath & FileName, , True)
                
                ' エクセルファイルのプロパティを出力
                Cells(OutputRow, 1).Value = FolderPath
                Cells(OutputRow, 2).Value = FileName
                Cells(OutputRow, 3).Value = wb.BuiltinDocumentProperties("Author")
                Cells(OutputRow, 4).Value = wb.BuiltinDocumentProperties("Last Author")
                Cells(OutputRow, 5).Value = wb.BuiltinDocumentProperties("Company")
                Cells(OutputRow, 6).Value = wb.ActiveSheet.Name
                
                ' 各シートに対してループ
                For Each ws In wb.Sheets
                    OutputCol = 7
                    ' ヘッダ・フッタ情報を出力
                    Cells(OutputRow, OutputCol).Value = ws.Name
                    Cells(OutputRow, OutputCol + 1).Value = CBool(ws.Visible)
                    ws.Activate
                    Cells(OutputRow, OutputCol + 2).Value = ActiveCell.AddressLocal(RowAbsolute:=False, ColumnAbsolute:=False, ReferenceStyle:=xlA1)
                    Cells(OutputRow, OutputCol + 3).Value = Selection.AddressLocal(RowAbsolute:=False, ColumnAbsolute:=False, ReferenceStyle:=xlA1)
                    
                    OutputCol = OutputCol + 4
                    Cells(OutputRow, OutputCol).Value = ws.PageSetup.LeftHeader
                    Cells(OutputRow, OutputCol + 1).Value = ws.PageSetup.CenterHeader
                    Cells(OutputRow, OutputCol + 2).Value = ws.PageSetup.RightHeader
                    Cells(OutputRow, OutputCol + 3).Value = ws.PageSetup.LeftFooter
                    Cells(OutputRow, OutputCol + 4).Value = ws.PageSetup.CenterFooter
                    Cells(OutputRow, OutputCol + 5).Value = ws.PageSetup.RightFooter
                    
                    ' 出力行を次に進める
                    OutputRow = OutputRow + 1
                Next ws
                
                ' エクセルファイルを閉じる
                wb.Close SaveChanges:=False
            
                Application.ScreenUpdating = True
                DoEvents
            End If
        End If
    Next file
    
    ' サブフォルダに対して再帰処理
    For Each folder In folder.SubFolders
        RecursiveGetExcelFileInfo folder.Path & "\", OutputRow
    Next folder
    
    Debug.Print "[RecursiveGetExcelFileInfo][END]" & FolderPath


End Sub

Sub ResetOutput()
    Dim OutputRow As Long
    Dim EndRow As Long
    OutputRow = 2
    EndRow = Rows.Count
    Range(Cells(OutputRow, 1), Cells(EndRow, 100)).Delete (xlShiftUp)
    Application.ScreenUpdating = True
End Sub

Sub StartRecursiveProcessing()
    Dim dlg As FileDialog
    Dim FolderPath As String
    Dim OutputRow As Long
    Dim OutputCol As Long
    Dim startTime As Double
    Dim endTime As Double
    Dim elapsedTime As Double
    
    ResetOutput
    
    ' ヘッダ出力
    OutputRow = 1
    Cells(OutputRow, 1).Value = "FolderPath"
    Cells(OutputRow, 2).Value = "FileName"
    Cells(OutputRow, 3).Value = "Author"
    Cells(OutputRow, 4).Value = "Last Author"
    Cells(OutputRow, 5).Value = "Company"
    Cells(OutputRow, 6).Value = "ActiveSheet"
    OutputCol = 7
    Cells(OutputRow, OutputCol).Value = "SheetName"
    Cells(OutputRow, OutputCol + 1).Value = "Visible"
    Cells(OutputRow, OutputCol + 2).Value = "ActiveCell"
    Cells(OutputRow, OutputCol + 3).Value = "Selection"
    
    OutputCol = OutputCol + 4
    Cells(OutputRow, OutputCol).Value = "LeftHeader"
    Cells(OutputRow, OutputCol + 1).Value = "CenterHeader"
    Cells(OutputRow, OutputCol + 2).Value = "RightHeader"
    Cells(OutputRow, OutputCol + 3).Value = "LeftFooter"
    Cells(OutputRow, OutputCol + 4).Value = "CenterFooter"
    Cells(OutputRow, OutputCol + 5).Value = "RightFooter"
    
    ' ヘッダ情報を出力する行を初期化
    OutputRow = 2
    
    ' フォルダ選択ダイアログを表示
    Set dlg = Application.FileDialog(msoFileDialogFolderPicker)
    
    If dlg.Show = -1 Then ' ダイアログでOKが選択された場合
        ' 処理開始時刻を記録
        startTime = Timer
        
        FolderPath = dlg.SelectedItems(1) & "\"
        ' 再帰的にフォルダ内のエクセルファイルを処理
        RecursiveGetExcelFileInfo FolderPath, OutputRow
        Application.ScreenUpdating = True
        
        ' 処理終了時刻を記録
        endTime = Timer
        
        ' 処理時間を計算
        elapsedTime = endTime - startTime
        
        Cells(OutputRow + 1, 1).Value = "[INFO] 処理が完了しました。 処理時間: " & Format(elapsedTime, "0.00") & " 秒"
        Debug.Print "処理が完了しました。 処理時間: " & Format(elapsedTime, "0.00") & " 秒"
        MsgBox "処理が完了しました。 処理時間: " & Format(elapsedTime, "0.00") & " 秒"
    
    Else
        Exit Sub ' キャンセルされた場合は終了
    End If
End Sub

Windows(WSL2)でdockerを動かすまで

ポイント

  • WSL2にアップデートする

ステップ

  • WSL2にする
  • WSL上にUbuntuをいれる
  • Docker公式サイトをもとにaptリポジトリ追加とインストール

毎回WSL2にするのを忘れてはまる。

手順

あとで書く

WSL インストール

管理者としてPowerShellを起動して下記でWSLをインストール

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

Ubuntuのダウンロード

curl.exe -L https://aka.ms/wslubuntu2204 -o ~\Downloads\Ubuntu.appx

パッケージにUbuntu追加

Add-AppxPackage ~\Downloads\Ubuntu.appx
wsl --list --online
wsl --install -d ディストロ名

WSL2 移行

Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform

もしかしたら下記も必要かも

dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

下記 Linux kernel update package のダウンロード&インストール https://learn.microsoft.com/en-us/windows/wsl/install-manual#step-4---download-the-linux-kernel-update-package

ディストロ名の確認

wsl -l -v

WSL2化

wsl --set-version ディストロ名 2

カーネルアップデート

wsl --shutdown
wsl --update
Docker インストール
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
  "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo docker run hello-world

Dockerデーモン起動していなかったら

sudo service docker start
sudo docker run hello-world

ユーザ権限でも実行したい場合

sudo usermod -aG docker `whoami`

シェル再起動

Proxy関連

環境変数 (~/.bashrc にいれておく)

export HTTP_PROXY=http://user:pass@proxyserver:8080
export HTTPS_PROXY=http://user:pass@proxyserver:8080

/etc/apt/apt.conf

Acquire::http::Proxy "http://user:pass@proxyserver:8080";
Acquire::https::Proxy "http://user:pass@proxyserver:8080";

Dockerデーモン /etc/default/docker

export no_proxy='export no_proxy=127.0.0.1,localhost,xxx.xxx.xxx.xxx'
export NO_PROXY="${no_proxy}"
export http_proxy='http://user:pass@proxyserver:8080/'
export HTTP_PROXY="${http_proxy}"
export https_proxy='http://user:pass@proxyserver:8080/'
export HTTPS_PROXY="${https_proxy}"

Dockerクライアント ~/.docker/config.json

{
 "proxies":
 {
   "default":
   {
     "httpProxy": "http://user:pass@proxyserver:8080",
     "httpsProxy": "http://user:pass@proxyserver:8080",
     "noProxy": "127.0.0.0/8,172.16.0.0/16"
   }
 }
}

参考

Node.jsでCORS回避する中継Webサーバを建てる

ローカルのhtmlでAPIを叩いて結果を見れるツールを作りたいときがたまにある。 できればjavascriptでブラウザで見れるようにしたい。

Node.jsの簡素なパッケージだけで実現してみた。

サンプルなので自分自身にスタブ用意しているけど、リクエスト先をCORS設定されているような公開APIに変更すればいい。

やりたいことはシンプルなのに、つまずきポイントがちょいちょい出てくるので残しておくことにする。

特にCORSの回避周りだったり、bodyの非同期受信の処理とか。

ググると使えない古い情報で溢れていたりするので、 できればnodejsとjqueryの非推奨呼び出しとなった歴史も整理していきたい。

web.js

let http = require("http");
let https = require("https");
let server = http.createServer();

server.on("request", function (req, res) {
  console.log(
    "HTTP",
    req.httpVersion,
    req.method,
    req.url,
    req.headers["user-agent"]
  );
  console.log("header: ", req.headers);

  // CORS全許可
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, HEAD, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "*");

  // ignore url
  if (req.url.indexOf("/favicon.ico") === 0) {
    let response = {};
    res.writeHead(404, { "Content-Type": "application/json" });
    res.write(JSON.stringify(response));
    res.end();
    return;
  }

  // OPTIONSメソッドは無条件で正常応答(エラー応答時のCORSエラーを防ぐため)
  if (req.method === "OPTIONS") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end();
    return;
  }

  body = "";
  req.on("data", (chunk) => {
    body += chunk;
  });
  req.on("end", () => {
    console.log("body: ", body);

    // スタブ応答
    if (req.url.indexOf("/api") === 0) {
      console.log("[API] Called ", req.url);
      let response = {};
      res.writeHead(200, { "Content-Type": "application/json" });
      res.write(JSON.stringify(response));
      res.end();
      return;
    }
    host = "127.0.0.1";
    port = 3000;
    method = req.method;
    path = "/api/";
    content = "json";
    protocol = port == 443 ? https : http;

    let options = {
      host: host,
      port: port,
      method: method,
      path: path,
    };

    let payload = body ? body : null;
    options.headers = {
      "Content-Type": "application/json",
    };

    options.headers["user-agent"] = "API Test Client";
    if (req.headers["authorization"]) {
      options.headers["authorization"] = req.headers["authorization"];
    }

    const reqApi = protocol.request(options, function (resApi) {
      var body = "";
      resApi.on("data", (chunk) => {
        body += chunk;
      });
      resApi.on("end", () => {
        console.log("StatusCode: ", resApi.statusCode);
        console.log("Response Headers: ", resApi.headers);
        console.log("Response Body: ", body);
        res.writeHead(resApi.statusCode, resApi.headers);
        res.write(body);
        res.end();
      });
    });
    reqApi.on("error", function (e) {
      console.log("[ERROR][API]" + e.message);
      let response = {};
      res.writeHead(500, { "Content-Type": "application/json" });
      res.write(JSON.stringify(response));
      res.end();
    });
    if (payload) {
      reqApi.write(payload);
    }
    reqApi.end();
  });
});

console.log("http://127.0.0.1:3000/");
server.listen(3000, "127.0.0.1");

index.html

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>index</title>
    <style>
        #result {
            white-space: pre-wrap;
        }
    </style>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"
        integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
</head>

<body>
    <h1>API Request</h1>
    <div>
        <h2>Request</h2>
        Url:<br>
        <select id="requestUrl">
            <option>http://127.0.0.1:3000/</option>
            <option>http://127.0.0.1:3000/api/</option>
            <option>https://www.githubstatus.com/api/v2/summary.json</option>
            <!-- <option>https://www.jma.go.jp/bosai/forecast/data/forecast/140000.json</option> -->
        </select><br>
        Bearer Token:<br>
        <input id="token" type="text" size="64" placeholder="token" value=""><br>
        Payload:<br>
        <textarea id="payload" cols="64" rows="5" placeholder="payload"></textarea></br>
        <button id="run">RUN</button>
        <h2>Response</h2>
        <pre id="status"></pre>
        <pre id="result"></pre>
        <pre id="responsetime"></pre>
    </div>
    <script>
        $('#run').on('click', function () {
            var token = $('#token').val();
            var requestUrl = $('#requestUrl').val();
            var payload = $('#payload').val();
            console.log(payload);
            console.log(token);
            var options = {
                url: requestUrl,
                type: 'get',
                dataType: 'json',
                contentType: 'application/json',
            };
            if (token) {
                options.headers = {
                    'Authorization': 'Bearer ' + token
                };
            }
            if (payload) {
                options.data = payload;
            }
            console.log({ requestUrl, token });
            $('#responsetime').text("");
            $('#status').text("");
            $('#result').text("");
            const startTime = Date.now(); // 開始時間
            $.ajax(options)
                .done(function (data, textStatus, jqXHR ) {
                    const endTime = Date.now(); // 終了時間
                    console.log(JSON.stringify(data));
                    $('#status').text("status: " + jqXHR.status);
                    $('#result').text(JSON.stringify(data));
                    $('#responsetime').text("ResponseTime: " + (endTime - startTime) + " ms");
                })
                .fail(function (jqXHR, textStatus, errorThrown) {
                    const endTime = Date.now(); // 終了時間
                    console.log("fail", jqXHR, textStatus, errorThrown);
                    $('#responsetime').text("ResponseTime: " + (endTime - startTime) + " ms");
                    $('#status').text("status: " + jqXHR.status);
                    $('#result').text("fail");
                    console.log(jqXHR.responseText);
                    $('#result').text(jqXHR.responseText);
                });
        });
    </script>
</body>

</html>