変奏現実

パソコンやMMORPGのことなどを思いつくまま・・・記載されている会社名・製品名・システム名などは、各社の商標、または登録商標です。

この画面は、簡易表示です

Linux

[AlmaLinux10] J1900: 非推奨,非推奨,非推奨

ログに

Warning: Deprecated Hardware is detected: x86_64-v2:GenuineIntel:Intel(R) Celeron(R) CPU  J1900  @ 1.99GHz will not be maintained in a future major release and may be disabled

という悲しいお知らせが載っていた。

x86_64だけど

# uname -a
Linux ******.******.****** 5.14.0-570.21.1.el9_6.x86_64 #1 SMP PREEMPT_DYNAMIC Wed Jun 11 07:22:35 EDT 2025 x86_64 x86_64 x86_64 GNU/Linux

x86_64-v2のハズなんだが?

ググってみるとRedHat10では、AVX2命令が無いatom系やceleron系は未対応の様だ。

lscpuで調べると確かにavx2の文字が無かった。

過去には子猫(開発)版は対応してたらしいが今の開発版のleapp preupgradeでも同じだった。

ま、それはそれとして・・・

ELevateでAlmaLionux9から10へのアップデートを試みると

==================================================================
レポートの概要
=======================================================================

以下の問題によりアップグレードが中止されました。
1. 現在の x86-64 マイクロアーキテクチャは RHEL10 ではサポートされていません。

重大度が HIGH および MEDIUM のレポート:
1. システム上に Red Hat によって署名されていないパッケージが見つかりました。
2. GRUB2 コアはアップグレード中に自動的に更新されます。
3. カスタム leapp アクターまたはファイルが検出された。
4. Leapp は、RHEL 10 でメンテナンスが終了したカーネルドライバーがロードされていることを検出しました。
5. Leapp は、RHEL 10 でメンテナンスが終了したプロセッサーを検出しました。
6. システムに Berkeley DB (libdb) が検出されました。
7. システムに MariaDB (mariadb-server) が検出されました。

と、トドメを刺された。(CPUががが

レポートファイルを読むと

リスク要因: 高(阻害要因)
タイトル: 現在の x86-64 マイクロアーキテクチャは RHEL10 ではサポートされていません
概要: RHEL10 は以前のバージョンよりも CPU 要件が高く、x86-64-v3 命令セット以上と互換性のある CPU が必要です。

不足しているフラグ: avx2、bmi1、bmi2、f16c、fma、abm、xsave

関連リンク:
- x86-64-v3 マイクロアーキテクチャレベル向けの Red Hat Enterprise Linux 10 のビルド: https://red.ht/rhel-10-intel-microarchitectures
対策: [ヒント] 仮想化を使用する場合、仮想化プラットフォームでは、異なる CPU モデル間の移行時に互換性を確保するために、最小要件の CPU モデルを設定できることがよくあります。最小要件が RHEL10 の要件を下回らないことを確認してください。

x86-64-v3じゃないとダメらしい。

avx2、bmi1、bmi2、f16c、fma、abm、xsave

不足する機能も多い。(諦め

でもアップグレードが無理でも

クリーンインストールなら可能性があるかも(無いかも

嘆いても仕方が無いので仮想マシンかPodmanでAlmaLinux10が動くか確認してみる。

VMを作成できるが、OSのダウンロードで失敗。

でもサーバーが調整中かもしれない(よくある現象

isoイメージをダウンロードして/tmpに配置してみるもisoイメージが読めないエラー

Podmanでイメージをダウンロードし、コンテナを作成すると、【起動】しても一瞬も「実行」にならない。

ブートすらできない。

多分、ブートもx86_64-v3のみ対応するバージョンらしい。

先のISOを使ってi9-9900のWin11のHyper-VでVMを作成してみるとブートするのでDVDイメージ自体がCPUの依存が高い様だ。

そのままインストを続ける

(中略)

で、起動してみると、

残念、J1900はもう先が無い事が確定した。

firewall-cmd –list-allでポートを確認するとSSH(Secure ShellをON)とcockpitはポート開放しているので

接続できたけど、リモートデスクトップ(Desktop Sharing)がWindows11から繋がらない。やはりxrdp入れないとダメかと思ったらポート(3389)を解放したら繋がったがパスワードを覚えてくれないのがめんどくさい。

INTELのcore i3-n300あたりがTDPが7Wと少なくブログサーバに良さそうだ。RasberryPi4は発熱が酷いから常時稼働には不向き、他はパワーがJ1900未満で圧倒的に足りない。

古いCPUでもavx2をサポートしてるので、I5-5200U+DDR3も手かもしれない。

AMDはAMD Ryzen™ Embedded V2000のMini-ITXマザーボードも国外で売ってるっぽいが高い



[AlmaLinux]wasmerって何?

wasmerは色々な言語のソースコードをWebAssemblyに変換して実行環境を作成し実行してみせるパッケージ。WebAssemblyしか動かない軽量なコンテナっぽいモノは外部とは実行時にパラメータとして与えるCallBackしかなさげなので安全性は高め。

また、jsモジュール化したパッケージがあり、ブラウザで動作するためコンテナっぽいモノはドコでも動く感じ。しかし普通に考えればHTMLにコンテナのソース数分つまり大量の<script>タグを書かないと動かない気もするが、wasmerを利用する自前のJSファイルをESモジュール化し、

<script type="module" src="xxxxx.js">

そのjs1行目にwasmer/sdkをimportすれば、

import { init, Wasmer } from "@wasmer/sdk";

後はwasmer/sdkが必要な分のimportをしてくれるので、ブラウザで容易に動作できそうな感じ。

勿論、直接指示しなくて良くても、大量のアクセスが起きうるので、以降のコードも同期が取れるまで待機する様にawait指示が必須になる。

import { init, Wasmer } from "@wasmer/sdk";
await init();
const pkg = await Wasmer.fromRegistry("python/python");

一応外部とはオフラインでもfromFileで利用可能だが、他プロジェクトへ提供しているパッケージではテストスイートが数GBにもなっているモノもあるのが難

※ パッケージを/path/にダウンロード済みなら
Wasmer.fromFile(/path/);
※も可。

無理と思いつつ、wordpressをwasmerでコンパイルする方法をググってみたが、

WordPressをWasmerでコンパイルするというのは、WordPressの主要な言語であるPHPをWebAssembly(WASM)に変換し、
WasmerというWASMランタイム環境で実行するという意味です。しかし、WordPressの構造がPHPに依存しているため、
WordPress全体をWasmerで動かすことは現実的ではありません。Wasmerは主にC/C++、Rustなどの言語で記述された
コードをWASMに変換し、ブラウザの外でも実行できる環境を提供するもので、WordPressのような動的なウェブアプリ
ケーションを直接コンパイルするような仕組みではありません。

やはり無理っぽいが作ってる人もいるが全部ブラウザ上で動くみたいでプラグイン作成者向けらしい。

ふと、spreadsheet5のscriptタグが多すぎなのに気が付いたのでESモジュール化してHTMLを短くし、CSSや処理中表示もカスタムエレメントに組込んだ。jsスクリプトファイルはネームスペースを圧縮した後だったのでごっそりと圧縮前に戻した。HTMLは短くなるけど、1ファイル毎にしっかりimport/exportしないとエラるので大変で、実行時にエラる時もあるのがとてもメンドイ。数値、文字を入力し計算式も結果が出るからOKかな?

本題に戻りwasmerインストの通りにインストしてみる。

$ curl https://get.wasmer.io -sSfL | sh
/usr/bin/which: no wasmer in (/home/***/.local/bin:/home/***/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin)
Welcome to the Wasmer bash installer!

               ww
               wwwww
        ww     wwwwww  w
        wwwww      wwwwwwwww
ww      wwwwww  w     wwwwwww
wwwww      wwwwwwwwww   wwwww
wwwwww  w      wwwwwww  wwwww
wwwwwwwwwwwwww   wwwww  wwwww
wwwwwwwwwwwwwww  wwwww  wwwww
wwwwwwwwwwwwwww  wwwww  wwwww
wwwwwwwwwwwwwww  wwwww  wwwww
wwwwwwwwwwwwwww  wwwww   wwww
wwwwwwwwwwwwwww  wwwww
   wwwwwwwwwwww   wwww
       wwwwwwww
           wwww

downloading: wasmer-linux-amd64
Latest release: v6.0.0
/usr/bin/which: no wasmer in (/home/***/.wasmer/bin)
Downloading archive from https://github.com/wasmerio/wasmer/releases/download/v6.0.0/wasmer-linux-amd64.tar.gz
######################################################################## 100.0%
installing: /home/***/.wasmer
Updating bash profile /home/***/.bashrc
we've added the following to your /home/***/.bashrc
If you have a different profile please add the following:

# Wasmer
export WASMER_DIR="/home/***/.wasmer"
[ -s "$WASMER_DIR/wasmer.sh" ] && source "$WASMER_DIR/wasmer.sh"
check: wasmer 6.0.0 installed successfully ✓
wasmer will be available the next time you open the terminal.
If you want to have the commands available now please execute:

source /home/***/.wasmer/wasmer.sh
$

「すぐ使うなら・・・」と書いてある様に上のsourceコマンドを手打ちすると即使える。

wasmerのtemplatesでserverを検索すると静的WebServerがあったのでインストしてみた。

$ curl --proto '=https' --tlsv1.2 -sSfL https://get.static-web-server.net | sh
info: platform 'x86_64-unknown-linux-gnu' supported
info: downloading the 'static-web-server v2.36.1' pre-compiled binary...
info: installing pre-compiled binary in /usr/local/bin...
Copying SWS pre-compiled binary to /usr/local/bin...
[sudo] password for ***: 
info: pre-compiled binary installed on /usr/local/bin/static-web-server
Version:      2.36.1
Built:        2025-04-01 22:00:32 +00:00
Git commit:   ab44158182e4e29dcece4c3b10068dc596bf9e03
Build target: x86_64-unknown-linux-gnu
Rust version: rustc 1.85.1 (4eb161250 2025-03-15)
License:      MIT OR Apache-2.0
Homepage:     https://static-web-server.net
Author:       Jose Quintana <https://joseluisq.net>
SWS was installed successfully!
To uninstall SWS just remove it from its location.
$
$ static-web-server
Caused by:
    path ./public was not found or inaccessible
$ mkdir public
$ static-web-server
・・・
INFO static_web_server::info: cache control headers: enabled=true
INFO static_web_server::info: security headers: enabled=false
INFO static_web_server::info: http1 server is listening on http://[::]:80
INFO static_web_server::info: press ctrl+c to shut down the server
^C WARN static_web_server::warn: termination signal caught, shutting down the server execution
$ 

デフォルトでは”./public”らしいので、swsの設定ファイルは~/.wasmer/sws/config.tomlに変更し、

# Wasmer config
export WASMER_DIR="/home/***/.wasmer"
export WASMER_CACHE_DIR="$WASMER_DIR/cache"
export PATH="$WASMER_DIR/bin:$PATH"
# for sws
export SERVER_CONFIG_FILE="$WASMER_DIR/sws/config.toml"
# end of wasmer.sh.

その中で、rootフォルダは~/.wasmer/sws/publicに、ポートも8080に変更してみた。

設定のデフォルト値はこちら

[general]

#### Address & Root dir
host = "::"
port = 8080
root = "${WASMER_DIR}/sws/public"

#### Logging
log-level = "error"
・・・


[AlmaLinux]Ninja

wabtのtestスイートのmakeにはPython3が必須。

条件を満たしているのでビルドしてみると

$ make test
which: no emcc in (/home/****/.local/bin:/home/****/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin)
cd out/clang/Debug/ && cmake -G Ninja /home/****/wabt/ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug 
CMake Error: CMake was unable to find a build program corresponding to "Ninja".  CMAKE_MAKE_PROGRAM is not set.  You probably need to select a different build tool.
-- Configuring incomplete, errors occurred!
make: *** [Makefile:156: out/clang/Debug/build.ninja] エラー 1

Ninjaが必要らしいが、dnfのepelリポジトリにも無い

ソースからビルドしてみる。詳細はgithub

# git clone https://github.com/ninja-build/ninja.git
Cloning into 'ninja'...
remote: Enumerating objects: 13476, done.
remote: Counting objects: 100% (369/369), done.
remote: Compressing objects: 100% (222/222), done.
remote: Total 13476 (delta 266), reused 147 (delta 147), pack-reused 13107 (from 6)
Receiving objects: 100% (13476/13476), 5.12 MiB | 1.82 MiB/s, done.
Resolving deltas: 100% (9425/9425), done.
# cmake -Bbuild-cmake ※ビルド事前設定のオプションらしい -DBUILD_TESTING=OFF付加でユニットテスト無効も可
-- The CXX compiler identification is GNU 11.5.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- IPO / LTO enabled
-- Performing Test flag_no_deprecated
-- Performing Test flag_no_deprecated - Success
-- Looking for ppoll
-- Looking for ppoll - found
CMake Warning at CMakeLists.txt:82 (message):
  re2c 2 or later was not found; changes to src/*.in.cc will not affect your
  build.


-- Looking for fork
-- Looking for fork - found
-- Looking for pipe
-- Looking for pipe - found
-- Could NOT find GTest (missing: GTEST_LIBRARY GTEST_INCLUDE_DIR GTEST_MAIN_LIBRARY) 
-- The C compiler identification is GNU 11.5.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Found Python: /usr/bin/python3.9 (found version "3.9.21") found components: Interpreter 
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
-- Found Threads: TRUE  
-- Configuring done (10.9s)
-- Generating done (0.2s)
-- Build files have been written to: /root/ninja/build-cmake
# cmake --build build-cmake
[  1%] Building CXX object CMakeFiles/libninja-re2c.dir/src/depfile_parser.cc.o
[  2%] Building CXX object CMakeFiles/libninja-re2c.dir/src/lexer.cc.o
[  2%] Built target libninja-re2c
[  3%] Building CXX object CMakeFiles/libninja.dir/src/build_log.cc.o
[  5%] Building CXX object CMakeFiles/libninja.dir/src/build.cc.o
[  6%] Building CXX object CMakeFiles/libninja.dir/src/clean.cc.o
[  7%] Building CXX object CMakeFiles/libninja.dir/src/clparser.cc.o
[  8%] Building CXX object CMakeFiles/libninja.dir/src/dyndep.cc.o
[ 10%] Building CXX object CMakeFiles/libninja.dir/src/dyndep_parser.cc.o
[ 11%] Building CXX object CMakeFiles/libninja.dir/src/debug_flags.cc.o
[ 12%] Building CXX object CMakeFiles/libninja.dir/src/deps_log.cc.o
[ 14%] Building CXX object CMakeFiles/libninja.dir/src/disk_interface.cc.o
[ 15%] Building CXX object CMakeFiles/libninja.dir/src/edit_distance.cc.o
[ 16%] Building CXX object CMakeFiles/libninja.dir/src/elide_middle.cc.o
[ 17%] Building CXX object CMakeFiles/libninja.dir/src/eval_env.cc.o
[ 19%] Building CXX object CMakeFiles/libninja.dir/src/graph.cc.o
[ 20%] Building CXX object CMakeFiles/libninja.dir/src/graphviz.cc.o
[ 21%] Building CXX object CMakeFiles/libninja.dir/src/json.cc.o
[ 23%] Building CXX object CMakeFiles/libninja.dir/src/line_printer.cc.o
[ 24%] Building CXX object CMakeFiles/libninja.dir/src/manifest_parser.cc.o
[ 25%] Building CXX object CMakeFiles/libninja.dir/src/metrics.cc.o
[ 26%] Building CXX object CMakeFiles/libninja.dir/src/missing_deps.cc.o
[ 28%] Building CXX object CMakeFiles/libninja.dir/src/parser.cc.o
[ 29%] Building CXX object CMakeFiles/libninja.dir/src/real_command_runner.cc.o
[ 30%] Building CXX object CMakeFiles/libninja.dir/src/state.cc.o
[ 32%] Building CXX object CMakeFiles/libninja.dir/src/status_printer.cc.o
[ 33%] Building CXX object CMakeFiles/libninja.dir/src/string_piece_util.cc.o
[ 34%] Building CXX object CMakeFiles/libninja.dir/src/util.cc.o
[ 35%] Building CXX object CMakeFiles/libninja.dir/src/version.cc.o
[ 37%] Building CXX object CMakeFiles/libninja.dir/src/subprocess-posix.cc.o
[ 37%] Built target libninja
[ 38%] Generating build/browse_py.h
[ 39%] Building CXX object CMakeFiles/ninja.dir/src/ninja.cc.o
[ 41%] Building CXX object CMakeFiles/ninja.dir/src/browse.cc.o
[ 42%] Linking CXX executable ninja
[ 42%] Built target ninja
[ 43%] Building CXX object _deps/googletest-build/googletest/CMakeFiles/gtest.dir/src/gtest-all.cc.o
[ 44%] Linking CXX static library ../../../lib/libgtest.a
[ 44%] Built target gtest
[ 46%] Building CXX object CMakeFiles/ninja_test.dir/src/build_log_test.cc.o
[ 47%] Building CXX object CMakeFiles/ninja_test.dir/src/build_test.cc.o
[ 48%] Building CXX object CMakeFiles/ninja_test.dir/src/clean_test.cc.o
[ 50%] Building CXX object CMakeFiles/ninja_test.dir/src/clparser_test.cc.o
[ 51%] Building CXX object CMakeFiles/ninja_test.dir/src/depfile_parser_test.cc.o
[ 52%] Building CXX object CMakeFiles/ninja_test.dir/src/deps_log_test.cc.o
[ 53%] Building CXX object CMakeFiles/ninja_test.dir/src/disk_interface_test.cc.o
[ 55%] Building CXX object CMakeFiles/ninja_test.dir/src/dyndep_parser_test.cc.o
[ 56%] Building CXX object CMakeFiles/ninja_test.dir/src/edit_distance_test.cc.o
[ 57%] Building CXX object CMakeFiles/ninja_test.dir/src/elide_middle_test.cc.o
[ 58%] Building CXX object CMakeFiles/ninja_test.dir/src/explanations_test.cc.o
[ 60%] Building CXX object CMakeFiles/ninja_test.dir/src/graph_test.cc.o
[ 61%] Building CXX object CMakeFiles/ninja_test.dir/src/json_test.cc.o
[ 62%] Building CXX object CMakeFiles/ninja_test.dir/src/lexer_test.cc.o
[ 64%] Building CXX object CMakeFiles/ninja_test.dir/src/manifest_parser_test.cc.o
[ 65%] Building CXX object CMakeFiles/ninja_test.dir/src/missing_deps_test.cc.o
[ 66%] Building CXX object CMakeFiles/ninja_test.dir/src/ninja_test.cc.o
[ 67%] Building CXX object CMakeFiles/ninja_test.dir/src/state_test.cc.o
[ 69%] Building CXX object CMakeFiles/ninja_test.dir/src/string_piece_util_test.cc.o
[ 70%] Building CXX object CMakeFiles/ninja_test.dir/src/subprocess_test.cc.o
[ 71%] Building CXX object CMakeFiles/ninja_test.dir/src/test.cc.o
[ 73%] Building CXX object CMakeFiles/ninja_test.dir/src/util_test.cc.o
[ 74%] Linking CXX executable ninja_test
[ 74%] Built target ninja_test
[ 75%] Building CXX object CMakeFiles/build_log_perftest.dir/src/build_log_perftest.cc.o
[ 76%] Linking CXX executable build_log_perftest
[ 76%] Built target build_log_perftest
[ 78%] Building CXX object CMakeFiles/canon_perftest.dir/src/canon_perftest.cc.o
[ 79%] Linking CXX executable canon_perftest
[ 79%] Built target canon_perftest
[ 80%] Building CXX object CMakeFiles/clparser_perftest.dir/src/clparser_perftest.cc.o
[ 82%] Linking CXX executable clparser_perftest
[ 82%] Built target clparser_perftest
[ 83%] Building CXX object CMakeFiles/depfile_parser_perftest.dir/src/depfile_parser_perftest.cc.o
[ 84%] Linking CXX executable depfile_parser_perftest
[ 84%] Built target depfile_parser_perftest
[ 85%] Building CXX object CMakeFiles/elide_middle_perftest.dir/src/elide_middle_perftest.cc.o
[ 87%] Linking CXX executable elide_middle_perftest
[ 87%] Built target elide_middle_perftest
[ 88%] Building CXX object CMakeFiles/hash_collision_bench.dir/src/hash_collision_bench.cc.o
[ 89%] Linking CXX executable hash_collision_bench
[ 89%] Built target hash_collision_bench
[ 91%] Building CXX object CMakeFiles/manifest_parser_perftest.dir/src/manifest_parser_perftest.cc.o
[ 92%] Linking CXX executable manifest_parser_perftest
[ 92%] Built target manifest_parser_perftest
[ 93%] Building CXX object _deps/googletest-build/googlemock/CMakeFiles/gmock.dir/src/gmock-all.cc.o
[ 94%] Linking CXX static library ../../../lib/libgmock.a
[ 94%] Built target gmock
[ 96%] Building CXX object _deps/googletest-build/googlemock/CMakeFiles/gmock_main.dir/src/gmock_main.cc.o
[ 97%] Linking CXX static library ../../../lib/libgmock_main.a
[ 97%] Built target gmock_main
[ 98%] Building CXX object _deps/googletest-build/googletest/CMakeFiles/gtest_main.dir/src/gtest_main.cc.o
[100%] Linking CXX static library ../../../lib/libgtest_main.a
[100%] Built target gtest_main
#

うまくいったみたいなのでインスト

# cmake --install ./build-cmake/
-- Install configuration: ""
-- Up-to-date: /usr/local/include
-- Installing: /usr/local/include/gmock
-- Installing: /usr/local/include/gmock/gmock-actions.h
-- Installing: /usr/local/include/gmock/gmock-cardinalities.h
-- Installing: /usr/local/include/gmock/gmock-function-mocker.h
-- Installing: /usr/local/include/gmock/gmock-matchers.h
-- Installing: /usr/local/include/gmock/gmock-more-actions.h
-- Installing: /usr/local/include/gmock/gmock-more-matchers.h
-- Installing: /usr/local/include/gmock/gmock-nice-strict.h
-- Installing: /usr/local/include/gmock/gmock-spec-builders.h
-- Installing: /usr/local/include/gmock/gmock.h
-- Installing: /usr/local/include/gmock/internal
-- Installing: /usr/local/include/gmock/internal/custom
-- Installing: /usr/local/include/gmock/internal/custom/README.md
-- Installing: /usr/local/include/gmock/internal/custom/gmock-generated-actions.h
-- Installing: /usr/local/include/gmock/internal/custom/gmock-matchers.h
-- Installing: /usr/local/include/gmock/internal/custom/gmock-port.h
-- Installing: /usr/local/include/gmock/internal/gmock-internal-utils.h
-- Installing: /usr/local/include/gmock/internal/gmock-port.h
-- Installing: /usr/local/include/gmock/internal/gmock-pp.h
-- Installing: /usr/local/lib64/libgmock.a
-- Installing: /usr/local/lib64/libgmock_main.a
-- Installing: /usr/local/lib64/pkgconfig/gmock.pc
-- Installing: /usr/local/lib64/pkgconfig/gmock_main.pc
-- Installing: /usr/local/lib64/cmake/GTest/GTestTargets.cmake
-- Installing: /usr/local/lib64/cmake/GTest/GTestTargets-noconfig.cmake
-- Installing: /usr/local/lib64/cmake/GTest/GTestConfigVersion.cmake
-- Installing: /usr/local/lib64/cmake/GTest/GTestConfig.cmake
-- Up-to-date: /usr/local/include
-- Installing: /usr/local/include/gtest
-- Installing: /usr/local/include/gtest/gtest-assertion-result.h
-- Installing: /usr/local/include/gtest/gtest-death-test.h
-- Installing: /usr/local/include/gtest/gtest-matchers.h
-- Installing: /usr/local/include/gtest/gtest-message.h
-- Installing: /usr/local/include/gtest/gtest-param-test.h
-- Installing: /usr/local/include/gtest/gtest-printers.h
-- Installing: /usr/local/include/gtest/gtest-spi.h
-- Installing: /usr/local/include/gtest/gtest-test-part.h
-- Installing: /usr/local/include/gtest/gtest-typed-test.h
-- Installing: /usr/local/include/gtest/gtest.h
-- Installing: /usr/local/include/gtest/gtest_pred_impl.h
-- Installing: /usr/local/include/gtest/gtest_prod.h
-- Installing: /usr/local/include/gtest/internal
-- Installing: /usr/local/include/gtest/internal/custom
-- Installing: /usr/local/include/gtest/internal/custom/README.md
-- Installing: /usr/local/include/gtest/internal/custom/gtest-port.h
-- Installing: /usr/local/include/gtest/internal/custom/gtest-printers.h
-- Installing: /usr/local/include/gtest/internal/custom/gtest.h
-- Installing: /usr/local/include/gtest/internal/gtest-death-test-internal.h
-- Installing: /usr/local/include/gtest/internal/gtest-filepath.h
-- Installing: /usr/local/include/gtest/internal/gtest-internal.h
-- Installing: /usr/local/include/gtest/internal/gtest-param-util.h
-- Installing: /usr/local/include/gtest/internal/gtest-port-arch.h
-- Installing: /usr/local/include/gtest/internal/gtest-port.h
-- Installing: /usr/local/include/gtest/internal/gtest-string.h
-- Installing: /usr/local/include/gtest/internal/gtest-type-util.h
-- Installing: /usr/local/lib64/libgtest.a
-- Installing: /usr/local/lib64/libgtest_main.a
-- Installing: /usr/local/lib64/pkgconfig/gtest.pc
-- Installing: /usr/local/lib64/pkgconfig/gtest_main.pc
-- Installing: /usr/local/bin/ninja

とんでもない数

ninja --version
1.13.0.git

一応インストできたっぽい

ところが先のtestスイートの実行がfailする。あきらめてtest/run-tests.py



[apache]パスに.を含むurl

.backupとか見せたくないフォルダとかファイルを個別に.htaccessを作って書くのも面倒なんで

apacheのconfファイルに

## フルパス名にドットで始まるディレクトリィやファイルはブラウザから禁止
  <Directory ~ "\/\..+\/">
    Require all denied
  </Directory>
  <Files ~ "\/\..+\/">
    Require all denied
  </Files>
## .

って書くのはうまくいかないかな?matchなんで正規表現なんでディレクトリィやファイルのフルパス名のどこかに「.で始まる」ディレクトリィがあったらブラウザから禁止にできたっぽい。

ただ、

apacheでは短いパスでいくら禁止しても

そこから続く長いパスでRequireしたら通ってしまうが

単に合致したパターンの長さで判定してないとは「断定しにくい」・・・

何とも(判らん

## フルパス名にドットで始まるディレクトリィやファイルは外部から禁止
  <Directory ~ "\/\..+\/">
    Require all denied
    Require ip 192.168.xxx.xxx/24
  </Directory>
  <Files ~ "\/\..+\/">
    Require all denied
    Require ip 192.168.xxx.xxx/24
  </Files>
## .

と書き加えてreloadしたらLANから見えるので効果はあるっぽい。



[xterm.js]ssh接続その4

段々複雑になってきたので

xterm.jsの画面からは

  1. 画面からWebSocketで何か送信する
    • {
      • ssh : {
        • logon : {
          • username : xxxx
          • etc.
    • }…}
  2. ホスト側で受信
  3. JSONデータをアドオンのエイリアス(ssh)で配分する
    • { ssh : ・・・
    • sshエイリアスなアドオンのjsonRequestを呼び出す
  4. アドオンは受け取ったJSONデータから機能を実行する
    • { logon : ・・・
    • 処理名(logon)を読み取ってlogon処理を実行する
    • logon処理
      • { username : xxxx, …}
  5. 処理名が何か出力したらWebSocketで返信する

にすると後付けが楽な気がしたので、NodeJsで動く部分をアドオン化してみたら更に複雑になった。

├── package.json
├── package-lock.json
├── index.js
├── lock_file.js
├── tree.txt
├── web_socket_entity.js
└── addon
  ├── package.json
  ├── base_addon.js
  ├── js_yaml
  │├── package.json
  │└── js_yaml.js
  └── ssh_client
    ├── package.json
    ├── package-lock.json
    └── ssh_client.js

アドオンフォルダ(./addon)にプロジェクトごとコピーする方式。

AddonManagerのsetupで、アドオンフォルダ(./addon)の中のフォルダにある

package.jsonのmainかexportをからモジュールのjsファイルを見つけて

エイリアス付きでtypeListsにリストアップするダケなのにとっても長い。

/**
 * アドオンマネジャクラス
 */
export class AddonManager extends BaseAddon {
 ・・・省略・・・
  /**
   * リスト
   */
  typeLists = {};
 ・・・省略・・・
  /**
   * コンストラクタ
   */
  constructor() {
    super();
  };
  /**
   * ./addonディレクトリィのパッケージを検索
   * @param (*) pathAddonsDir
   */
  setup = (pathAddonsDir, allAddonInfo) => {
    this.allAddonInfo = allAddonInfo;
    return new Promise((resolve, reject) => {
      fs.readdir(pathAddonsDir, { encoding: 'utf-8', withFileTypes: true, recursive: false },
        (err, dirents) => {
          /**
           * error
           */
          if (err) {
            console.error(err);
            reject(err);
            return;
          }
          /**
           * ディレクトリィのみに絞込む
           */
          dirents = dirents.filter((d) => d.isDirectory());
          /**
           * とりあえず配列分ループ
           */
          const arP = dirents.map((dirent) => {
            return new Promise((resolve, reject) => {
              const addonPath = `${pathAddonsDir}/${dirent.name}`;
              console.log(addonPath);
              // dirent配下のpackage.jsonを読む
              const packageJsonPathname = `${addonPath}/package.json`;
              const packageJsonText = fs.readFileSync(packageJsonPathname);
              const packageJson = JSON.parse(packageJsonText);
              const mainFile = packageJson.main || packageJson.exports;
              if (mainFile !== undefined) {
                const mainFilePathname = `${addonPath}/${mainFile}`;
                import(mainFilePathname)
                  .then((module) => {
                    // 動的に読み込まれたモジュール
                    const addonModule = new module.default;
                    // アドオンリストに追加
                    this.setAddonList(addonModule.addonAlias, addonModule);
                    resolve(true);
                  })
                  .catch((ex) => {
                    console.error(ex);
                    reject(ex);
                  });
              } else {
                console.error(`addonSetup : ${packageJsonPathname} not found main or exports`);
              }
            });
          });
          Promise.all(arP)
            .then((values) => {
              resolve(true);
            });
        }
        // end of for (const dirent of dirents)
      );
    });
  };
 ・・・省略・・・
};

アドオンを呼び出す時は

AddonManager::getAddonObject({アドオンのエイリアス})でオブジェクトを取得

  /**
   * アドオン・オブジェクトを取得
   * @param {*} alias 
   * @returns addon
   */
  getAddonObject = (alias) => {
    const addonInfo = this.typeLists['addon'][alias];
    if (addonInfo) {
      const object = new addonInfo.constructor(this.allAddonInfo[alias]);
      return object;
    } else {
      return undefined;
    }
  };

画面からの要求を{アドオンのオブジェクト}::jsonRequest(WebSocket, json)から

アドオンオブジェクトを作成し実行させている。

  /**
   * JSONリクエスト処理
   * @param {*} webSocketEntity 
   * @param {*} json 
   * @param {*} addonObjectList 
   */
  jsonRequest = (webSocketEntity, json, addonObjectList) => {
    const addonModules = this.typeLists['addon'];
    for (const alias in json) {
      const addon = addonObjectList[alias] ?? this.getAddonObject(alias);
      if (!addonObjectList[alias]) { addonObjectList[alias] = addon; }
      if (addon) {
        try {
          addon.request(webSocketEntity, json[alias]);
        } catch (ex) {
          console.log(`AddonManager::jsonRequest : unknown addon alias  '${alias}'`);
        }
      } else {
        console.log(`AddonManager::jsonRequest : unknown addon alias  '${alias}'`);
      }
    }
  };

レスポンスはアドオンオブジェクトにWebSocket宛に送信してもらった。

  /**
   * クライアントに返信する処理
   * @param {string} type 
   * @param {string} commandName 
   * @param {any} data 
   */
  sendClient = (type, commandName, data) => {
    //console.log(`${commandName} : ${data}`);
    const responce = {};
    responce[type] = data;
    const blob = new Blob([JSON.stringify(responce)], { type: "application/json" });
    this.connect.send(blob);
  };

やっと画面側もtype毎に処理を分けないといけない事に気が付く。

めんど



[node.js]node –inspect-wait

リモートホストで動くnode.jsのアプリ(index.js)をPCのchromeからデバッグできた。

# node --inspect-wait {ソース名}

で実行しchrome待ちになってるけど、Chromeの「chrome://inspect/」 のページの

Discover network targetsの【Configure】ボタンを押して

{リモートホストのIPアドレス}:9229

を追記しても、

Remote Target #{リモートホストのIPアドレス}

に index.js が表示されない。

man node でオプションを調べてみると

--inspect-wait=[host:]port
        Activate inspector on host:port and wait for debugger to be attached.

[host:]port ってIPアドレスやポートを指定できるんだ。

# node --inspect-wait={自分のIPアドレス}:{chromeと通信するポート番号} {ソース名}

で、chromeのDevToolsで、ファイル選択でindex.jsを選ぶと普通にデバッグできた。

但し、これはリモートホスト側でポート開放必須でinspectも何でも出来る様なので、

SSLでポートフォワードする方法の方がよさそう。

> ssh -L 9229:localhost:9229 {ユーザ名}@{リモートホストのIPアドレス}
$ node --inspect-wait index.js

これでChromeでデバッグしながら、ポートを解放せずにsshでソース修正ができるから結構使い道がありそうだし、chrome操作さけなら「うっかりソースのバグを修正していまう」コトも無い(ハズ

to 管理者:ポート開放して

from 管理者:無理

ってありそうだし(笑

最近のWindowsはkey-genできるしsshできるし便利になったね。(大笑

あ、BATファイルにすればいいなぁ

cmd /C ssh -L 9229:localhost:9229 {ユーザ名}@{リモートホストのIPアドレス}

あれ無限ループ?

ファイル名が悪かった(再起してた

cmd /C ssh -L 9229:localhost:9229 {ユーザ名}@{リモートホストのIPアドレス}

これでポートも繋がりすぐ node –inspect-wait index.js できる。

メデタシメデタシ

ps.2025/4/11

大元ネタはココらしい。



[xterm.js]ssh接続その3

WebSocketをxtermのアドオン@xterm/addon-attachに渡してるけど、このままではログをちょっとみたいとかできない。

@xterm/addon-attachから送信されるデータがいつもUint8Arrayなので、挟むコマンドはテキストで送ればいいのかと思ったら、クライアント側でstring, ArrayBuffer, Blobのどれをsendしようが、サーバ側にはUint8Arrayとして引き渡されていたので、サーバ側はいつもJSONデータが渡ってくる前提でコード。

/**
 * WebSocketクライアント(xterm.js)からメッセージ受信時の処理
 */
ws.on('message', async (event) => {
  // JSON.stringify()でテキストで送信しているハズ
  const textJson = await new Response(event).text();
  // JSONに成~れ!
  try {
    const json = JSON.parse(textJson);
    // JSONに成った!
    // sshかな?
    if (json.ssh) {
      if (typeof json.ssh === 'string') {
        console.log(`resv ssh text : '${json.ssh}'`);
        stream.write(json.ssh);
      } else if (json.ssh instanceof Uint8Array) {
        // いつものUint8Array
        const text = new TextDecoder().decode(json.ssh);
        console.log(`resv ssh binary : '${text}'`);
        stream.write(json.ssh);
      } else {
        console.log(`resv ssh unknown type[${typeof json.ssh}] : '${json.ssh}'`);
      }
    } else if (json.{その他1}) {
      // {その他1}かな?
      const resultText = {その他1}(json.{その他1});
      console.log(`{その他1}('${json.{その他1}}')\n='${resultText}'`);
      // 結果をJSONに置き換えて
      const responce = {
        {その他1}: resultText,
      };
      // 送信
      const blob = new Blob([JSON.stringify(responce)], { type: "application/json" });
      ws.send(blob);
    } else if (json.{その他2}) {
      ・・・省略・・・
    } else if (json.{その他n}) {
      ・・・ほぼ{その他1}と似た感じ
    } else  {
      // しらないコマンド
    }
  } catch (ex) {
    // 私はJSONに成れないのか!
    console.log(`resv not json's text : ex : '${ex}'`);
  }
});

クライアント側は、WebSocketのmessageメソッドに

webSocket.addEventListener("message", async (event) => {
 ・・・
  preventDefault();
}, { passive: false });

しても、xtermjs画面にデータを表示してしまう。

仕方が無い。

@xterm/addon-attachを外して・・・

自前でWebSocketを送受信する。(前途多難そう

/**
 * WebSocketの処理 ***************************
 */
/**
 * WebSocketのmessageイベント処理
 */
webSocket.addEventListener("message", async (event) => {
  // テキストにする
  const textJson = await new Response(event.data).text();
  // JSONに成~れ
  try {
    const json = JSON.parse(textJson);
    // 内容で分岐
    if (json.ssh) {
      const s = json.ssh;
      terminal.write("string" == typeof s ? s : new Uint8Array(s));
    } else if (json.{その他1}) {
      //  {その他1}のレスポンス
      console.log(`{その他1}='${json.{その他1}}'`);
    } else if (json.{その他2}) {
・・・
    } else if (json.{その他n}) {
      //  {その他n}のレスポンス
      console.log(`{その他n}='${json.{その他n}}'`);
    } else {
       // 知らないコマンド
    }
  } catch (ex) {
    // JSONに成れなかった
  }
});
/**
 * WebSocketのcloseイベント処理
 */
webSocket.addEventListener("close", ((event) => {
  terminal.write('*** disconnection ***');
  console.log('*** disconnection ***');
}));
/**
 * WebSocketのerrorイベント処理
 */
webSocket.addEventListener("error", ((event) => {
  terminal.write('*** socket error ***');
  console.error(`socket error : ${event}`);
}));
/**
 * Terminalのイベント処理
 */
/**
 * Terminalのdataイベント処理
 */
terminal.onData((event) => {
  // sshに送信するJSONに変換
  const msg = {
    ssh: event,
  };
  // テキストに展開しBlobで送信
  const blob = new Blob([JSON.stringify(msg)], { type: "application/json" });
  webSocket.send(blob);
});
/**
 * Terminalのbinaryイベント処理
 */
terminal.onBinary((event) => {
// 呼ばれてないので省略
});

@xterm/addon-attachを代行する処理は、

WebSocketのmessageの処理は長いけど(独自コードが多い

xtermjsのデータを送信するのは短くてよかった。

本当は色々チェックが必要なんだろうけど。(ま、いいか



[xterm.js]ssh接続その2

リモートホストとPCの両方をVScodeで観れるようになると色々注意されるのが気になってきたので修正

1.CommonJSモジュールをESモジュールに変えてみる

ソースの…が気になるので見てみると

云うのでクイックフィックスしてみると

と修正候補のESモジュールに変換するをクリック。

それはそれでもいいけどね

2.負傷1

別のソースで同様に変換すると

import { WebSocket } from 'ws';
・・・
const wss = new WebSocket.Server({ port: sshInfo.wsport });

WebSocket.Server にコンストラクタ無いっすエラー

ググって他人のソースを検索してみたら

import { WebSocketServer } from 'ws';
・・・
const wss = new WebSocketServer({ port: sshInfo.wsport });

と書いていたので真似た。

3.負傷2

クイックフィックスが一気に書き換えて実行すると

別に{}を取ればいいだけなんだが

import fs from 'fs';

で実行すると

はいはいpackage.jsonもESモジュール宣言ね。

ps.2025/4/8

ブラウザ用のjsファイルもESモジュールにできるみたいだ。

しかし、TypeScriptで書いたモジュールのjsファイルは

同名の.d.tsファイルを作り、export宣言をしても、ブラウザがエラってしまう。

inportmapを作ってもダメ。



[xterm.js]ssh2接続

ブラウザとシェルはWebSocketで非同期に相互に(つまり自分勝手)に通信する。

node.jsでspawnすればシェルと繋がるけど、改行入力で1行入力(readline)っぽくシェルがやってくれると思ってたら、改行しても応答無しstdinストリームを閉じまで入力したコマンドを実行する気配が無かった、BackSpaceもそのまま渡ってしまう・・・

どうやらsshサービスが頑張ってる様だ。

node.jsからシェル起動は諦めて、ssh2サービスに繋ぐことにする。

1.WebSocketサーバーの作成

# mkdir testServer
# cd testServer
# npm init
・・・・・
# npm install ssh2            ※SSH2パッケージ
# npm install ws              ※WebSocketパッケージ
const WebSocket = require('ws');
const { sshTerm } = require('./sshTerm.js');
// SSH接続情報
const sshInfo = {
user      : '{SSHのログイン・ユーザ名}',
password  : '{SSHのログイン・パスワード}' {または} privateKey: require('fs').readFileSync('/PATH/id_rsa') ,
ipaddress : '{SSHのホストIPアドレス}',
sshport   : {SSHのポート番号},
wsport    : {WebSocketのポート番号},
httpport  : {apacheのポート番号},
httpath   : '{urlのパス}',
};
// ポート${sshInfo.wsport}でWebSocketサーバーを作成
const wss = new WebSocket.Server({ port: sshInfo.wsport });
// 案内文
console.log(`ssh ready, URL http://${sshInfo.ipaddress}:${sshInfo.httpport}/${sshInfo.httpath}`);
// WebSocketクライアント(xterm.js)接続時の処理
wss.on('connection', function connection(ws) {
  const env = process.env;
  console.log('connection WebSocket client.');
   const bash = sshTerm(ws, sshInfo);
});

passwordかprivateKeyかprivateKeyPathのいづれかを指定できる。

const { Client } = require('ssh2');
// SSH接続を確立
const sshTerm = async (ws, sshInfo) => {
  const commandName = 'bash';
  const conn = new Client();
  conn.on('ready', () => {
    // ssh2接続準備完了
    console.log('Client :: ready');
    // shell対応
    conn.shell((err, stream) => {
      if (err) throw err;
      stream.on('close', () => {
        // エラった時
        if (code !== 0) {
          console.log(`${commandName} process exited with code : ${code}.`);
        } else {
          console.log(`${commandName} process exited.`);
        }
        ws.send(`disconnected.`);
        conn.end();
      }).on('data', (data) => {
        // ssh2からデータが送られた時の処理
        console.log(`${commandName} : ${data}`);
        data = data.toString().replaceAll('\n', '\r\n');
        ws.send(data);
      });      
      // WebSocketクライアント(xterm.js)からメッセージ受信時の処理
      ws.on('message', async (message) => {
        console.log('受信したメッセージ: %s', message);
        stream.write(message);
      });      
      // WebSocketクライアント(xterm.js)クライアントから切断された時の処理
      ws.on('close', function close() {
        console.log('クライアントとの接続が切断');
        stream.end();
      });
      // Ctrl+Cで止められた時の処理      
      process.on('SIGHUP', ()=> {
        console.log('Got SIGHUP. ');
        stream.end();
        console.log('クライアントの接続をパージ');
        ws.close();
     });
     // 以上
    });
  }).connect({
    host: sshInfo.ipaddress,
    port: sshInfo.sshport,
    username: sshInfo.user,
    password: sshInfo.password {または} privateKey: sshInfo.privateKey,
  });
};
exports.sshTerm = sshTerm;
  • privateKey
    • ‘~/.ssh/id_rsa` の様な ‘~’ を使うとopenエラー。
      • 代用はprocess.env[‘HOME’]
  • privateKeyPath
    • エラってしまう。

testServerプロジェクトの雰囲気

  • testServer
    • index.html
    • sshTerm.js
      • node_modules
        • ssh2
        • ws

ここで、

# node index.js

でサーバー側の準備は終わり。

2.WebSocketクライアントの作成

クライアントはapacheのhtmlの下にtestディレクトリィを作りxtermのアドオン等を追加する。

# cd /var/www/html
# mkdir test
# cd test
# npm init
・・・
# npm install @xterm/xterm
# npm install @xterm/addon-web-links
# npm install @xterm/addon-attach
# npm install @xterm/addon-clipboard
# npm install @xterm/addon-fit
# npm install @xterm/addon-image
# npm install @xterm/addon-web-links
# npm install @xterm/addon-webgl

必要なのは、node_modules/@xterm下の cssファイル、jsファイル、mapファイルだけなので不要なファイルは削除する。

index.htmlにアドオンのファイルを追記。

<!doctype html>
  <html lang="ja">
    <meta charset="UTF-8">
    <title>xterm.js test page</title>
    <head>
      <link rel="stylesheet" href="node_modules/@xterm/xterm/css/xterm.css" />
      <script src="node_modules/@xterm/xterm/lib/xterm.js"></script>
      <script src="node_modules/@xterm/addon-web-links/lib/addon-web-links.js"></script>
      <script src="node_modules/@xterm/addon-attach/lib/addon-attach.js"></script>
      <script src="node_modules/@xterm/addon-clipboard/lib/addon-clipboard.js"></script>
      <script src="node_modules/@xterm/addon-fit/lib/addon-fit.js"></script>
      <script src="node_modules/@xterm/addon-image/lib/addon-image.js"></script>
      <script src="node_modules/@xterm/addon-web-links/lib/addon-web-links.js"></script>
      <script src="node_modules/@xterm/addon-webgl/lib/addon-webgl.js"></script>
      <script src="test.js"></script>
    </head>
    <body>
      <div id="terminal"></div>
    </body>
  </html>

test.jsにもアドオンの初期化等を追記

// 初期化処理
const sshInfo = {
wsport    : {WebSocketのポート番号},
};
window.addEventListener('load', () => {
  const term = new Terminal();
  //
  term.open(document.getElementById('terminal'));
  term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ');  
  // addon  
  // @xterm/addon-attach
  // initialization
  const webSocket = new WebSocket(`ws://${location.hostname}:${sshInfo.wsport}`);
  const attachAddon = new AttachAddon.AttachAddon(webSocket);
  term.loadAddon(attachAddon);
  // @xterm/addon-clipboard
  // initialization
  const clipboardAddon = new ClipboardAddon.ClipboardAddon();
  term.loadAddon(clipboardAddon);
  // @xterm/addon-fit
  // initialization
  const fitAddon = new FitAddon.FitAddon();
  term.loadAddon(fitAddon);
  fitAddon.fit();
  // @xterm/addon-image
  // customize as needed (showing addon defaults)
  const customSettings = {
    enableSizeReports: true,    // whether to enable CSI t reports (see below)
    pixelLimit: 16777216,       // max. pixel size of a single image
    sixelSupport: true,         // enable sixel support
    sixelScrolling: true,       // whether to scroll on image output
    sixelPaletteLimit: 256,     // initial sixel palette size
    sixelSizeLimit: 25000000,   // size limit of a single sixel sequence
    storageLimit: 128,          // FIFO storage limit in MB
    showPlaceholder: true,      // whether to show a placeholder for evicted images
    iipSupport: true,           // enable iTerm IIP support
    iipSizeLimit: 20000000      // size limit of a single IIP sequence
  }
  // initialization
  const imageAddon = new ImageAddon.ImageAddon(customSettings);
  term.loadAddon(imageAddon);
  // @xterm/addon-web-links
  term.loadAddon(new WebLinksAddon.WebLinksAddon());
  // @xterm/addon-webgl
  term.loadAddon(new WebglAddon.WebglAddon());
});

new ClipboardAddon.ClipboardAddon()とか変な書き方になってるのは

サンプルソースでは new ClipboardAddon() なので実行するとコンストラクタが無いエラーになる。

ブラウザのJavaScriptではグローバルな宣言は、windowオブジェクトにぶら下がるので、

Chromeの開発ツールの監視でwindow値を見ると ClipboardAddon が見つかる

見た感じではClipboardAddon オブジェクトにClipboardAddon クラスがぶら下がっている感じだったのであの様に new している。

多分、TypeScriptのコードをビルドしたソース(というかESModuleコード全般)をJavaScriptから利用するとexportsした名前の下にexportsしたい対象がブラ下がる様に見えるらしい。

/var/www/html/testプロジェクトの雰囲気

  • test
    • index.html
    • test.js
      • node_modules
        • (いっぱい)

後はブラウザから http://localhost/test/ を開く。

topコマンドやviコマンドも普通に使える。

ブラウザで複数のセッションを開けるけど、多人数で使うのは無理。

とても少ないコードでここまで動くのは大助かり。

もっとも

VisualStudioCodeのSSH接続でソース修正やデバッグができるので

もう不要と云えば不要だけどね。

ps.2025/4/9

アドオンがうまく動作していなかったけど、参考を見て紛れてるミスを訂正したらマウスホバーでリンク表示とかCtrl+クリックでジャンプ出来る様になった。




top