変奏現実

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

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

[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毎に処理を分けないといけない事に気が付く。

めんど



[podman]コンテナからごそっとファイルを取り出す場合

1ファイルなら

# podman cp {コンテナ名}:コピー元フルパスのファイル名 コピー先ファイル名

で済む。2個でも済むけど、多い時はフォルダごと

# podman cp {コンテナ名}:コピー元フルパスのディレクトリィ名/ コピー元ディレクトリィ名

して後はtarコマンドでまとめれば普通はOK。

但し、コマンドを実行したユーザに所有者が変わってしまい後々面倒なことになるので、

できればコンテナの中でtarファイルにした方がいい。

# パラメータチェック
if [ $# -eq 3 ]; then
  CONTER_NAME=$1
  TARGET_DIR=$2
  TARGET_TAR_PATH_NAME=$3
  TARGET_TAR_NAME=$(echo $TARGET_TAR_PATH_NAME | rev | cut -d'/' -f1 | rev) ※ファイル名のみ取得
  TMP_DIR="/tmp" ※都合悪い場合は書換え
  CMD1="podman exec -it ${CONTER_NAME} /bin/bash -c 'cd ${TARGET_DIR}; ls -l; tar zcvf ${TMP_DIR}/${TARGET_TAR_NAME} . ; ls -l ${TMP_DIR}/${TARGET_TAR_NAME}'"
    # cd ${TARGET_DIR}
    # ls -l ※無くてもOK
    # tar zcvf ${TMP_DIR}/${TARGET_TAR_NAME} . 
    # ls -l ${TMP_DIR}/${TARGET_TAR_NAME} ※無くてもOK
  CMD2="podman cp ${CONTER_NAME}:${TMP_DIR}/${TARGET_TAR_NAME} $TARGET_TAR_PATH_NAME"
  CMD3="podman exec -it ${CONTER_NAME} /bin/bash -c 'ls -l ${TMP_DIR}/${TARGET_TAR_NAME} ; rm -f ${TMP_DIR}/${TARGET_TAR_NAME} ; ls -l ${TMP_DIR}/${TARGET_TAR_NAME};'"
    # ls -l ${TMP_DIR}/${TARGET_TAR_NAME} ※無くてもOK
    # rm -f ${TMP_DIR}/${TARGET_TAR_NAME}
    # ls -l ${TMP_DIR}/${TARGET_TAR_NAME} ※無くてもOK
  CMD4="ls -l $TARGET_TAR_PATH_NAME"
  
  echo CMD1:$CMD1
  echo CMD2:$CMD2
  echo CMD3:$CMD3
  echo CMD4:$CMD4
  
  /bin/bash -c "$CMD1" > log.txt
  wait $!  ※いい感じで待ってくれる
  /bin/bash -c "$CMD2" >> log.txt
  wait $!  ※いい感じで待ってくれる
  /bin/bash -c "$CMD3" >> log.txt
  wait $!  ※いい感じで待ってくれる
  /bin/bash -c "$CMD4" >> log.txt
  wait $!  ※いい感じで待ってくれる
else
  echo usage: $0  {PODMAN_CONTER_NAME}  {FULL_PATH_IN_CONTER}  {OUTPUT_TAR_FILE_NAME}
  exit 1
fi

念のためTARGET_TAR_NAMEは、../conf.tar.gz とか保存先ディレクトリィを指定できる様にしてある。

余談

TARGET_TAR_NAMEが空っぽになるコード

TARGET_TAR_NAME=   $(echo $TARGET_TAR_PATH_NAME | rev | cut -d'/' -f1 | rev)

$(・・・)の前に空白があると、TARGET_TAR_NAMEが空文字列になってしまう。

TARGET_TAR_NAME=$(echo $TARGET_TAR_PATH_NAME | rev | cut -d'/' -f1 | rev)

昔のUNIX(SystemV)の頃のawkも(…)や{…}の前後のスペーシングに敏感だったが今はそんな事ないんでウッカリしてた。



[VSCode]デバッグコンソール

リモートホストに繋いでindex.jsを「デバッガの開始」すると

デバックコンソールに案内文(url付)を表示すると、ポートに①が付くので開くと、

urlのドメインがlocalhostなら困るだろうと自動的にポートフォワードするらしい。

デバックコンソールでCTRL+クリックでブラウザで開くとブラウザのURLが

デバックコンソールに表示しているポート番号からポートフォワードのポートに変わってる。

ちなみに、「ターミナル」を開くとcockpit入れてると、https://localhost:9090とか出るので、ポート9090をlocalhostの9090と同じ番号。10000未満だと違う番号にすると別のサービスに被るから変えてないのだろう。でも、VSCodeでデバッグする時だけポート番号が違うとメンドクサイので

「20090:20090」の様に2つ番号を入れれば好きな番号に振ってくれるので助かる。

でもこれだけでは繋がらない。リモート側で「20090:20090」という番号らしくないポート番号を開こうとして挫折してるっぽので、一旦「20090:20090」を削除して、今度は「20090」で作成。

あれ?たった今、https://localhost:9090が出なくなった?

でも、WindowsのコマンドラインからSSHすると

ここだけ表示する?

と思ったら、デバッグコンソールでも出るようになった。(怪しい

ps.2050/4/15

なぜlocalhost側のポート番号+1してしまうか不思議だったけど

自分でフォワードポートしたまま、listenしようとして失敗してたらしい。

よく見ると転送されたアドレスのlocalhost:9090あたりで右クリックしてローカルアドレスポートの変更を選択すると

こんな風に直接手入力で変更できる。

機能多すぎて説明を読む気もしないから

ショートカットもほぼ使ってない状況なせいかな?



[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のデータを送信するのは短くてよかった。

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



[VScode]リモートサーバに繋ぐ※メモ書き

VScodeでHTMLやJavaScripの作成やデバッグができるけど、リモートなサーバにはWinSCP等でコピっていたけど今はSSH接続でターミナルやファイル転送やデバッグができる。

1.SSH接続の設定を追加する

まず、左のリモートエクスプローラ(><っぽい奴)からSSHの歯車をクリック。

一番上のconfigを選択

そして適当に追記

Host {SSH接続先メニューのタイトル}
    HostName {リモートホストのIPアドレス}
    User {SSH接続時のユーザ名}
    Port 22  ※多分22でOK
    IdentityFile ~/.ssh/{秘密鍵ファイル名のハズ}

Host のタイトルを {ユーザ名}@{表示名} のように書いたら、

プロセスが、存在しないパイプに書き込もうとしました。

とかエラーになった。

{表示名}_{ユーザ名}はOKなので@だけ失敗するかもしれない。

タダの表示名では無い様だ。

Ctrl+Sで保存

2.SSH鍵ファイルの作成と転送

昔はPuTTYをインストしてKEY-GENで作って出来たものを変換して・・・だったけど、

今はWindowsのコマンドプロンプトで作成できる。

※元ネタ:SSH鍵の生成と使用ガイド(Windows対応)

> dir "%USERPROFILE%\.ssh"         ※ SSH鍵を保存する .ssh フォルダがあるか調べる
> mkdir "%USERPROFILE%\.ssh" ※ .sshフォルダが無かったら実行する
> ssh-keygen -t rsa -b 4096 -f "%USERPROFILE%\.ssh\id_rsa" -N ""    ※このコマンドで作ってくれる

出来た公開鍵を使って下のスクリプト7行を{…}内を修正した内容をコピって

PowerShellにペーストしてリモートのホストの認証リストに追記させる。

※元ネタ:Windowsでssh-copy-idっぽいことをしたい

※改行を無視させる文末の「`」の前に半角空白が必須

$sshUser = '{SSH接続ユーザ名}'
$sshHost = '{リモートホストのIPアドレス}'
cat ~/.ssh/id_rsa.pub | ssh ${sshUser}@${sshHost} `
" `
mkdir -p ~/.ssh && chmod 700 ~/.ssh && `
cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys `
"

※ログインするのでパスワード入力応答あり、

これでリモートホストのフォルダを見る度にパスワード入力しなくてもいい。

リモートホストを選択し

なぜかリモートホストのプラットフォームをちゃんと選択すると

自動的に選択したプラットフォーム用のVSCode-Serverがリモートホストにインストされる

それはいいけど英語のメッセージ

和訳すればホッとする内容

これで毎回パスワード入力画面(下図)を見たくて良くなったのはとても気分がいい。

接続するとターミナルか

エクスプローラからソースを選択すれば編集できて保存もやってくれる。

デバッグもソースを手前に表示している状態でメニューの【実行】から

Node.jsを選択すれば、デバッグコンソールで

$  node  {表示してるJSファイル}

してデバッガが起動するのはいつも通り。

ブレークポイントとかウォッチ式も使える。※ import.meta.url 等一部不可

但しVSCodeで開いたフォルダがプロジェクト以外のディレクトリィだとそのディレクトリィをカレントディレクトリィとしてnodeを実行するみたいなので

privateKey: fs.readFileSync(`${__dirname}.ssh/id_rsa`), ※__dirname がホームディレクトリィ
↓
privateKey: fs.readFileSync(new URL(`.ssh/id_rsa`, import.meta.url)), ※ソースのフルパスを使う

とかちょっと修正が必要。



[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+クリックでジャンプ出来る様になった。



[xterm.js]文字を入力できるだけのサンプル

概ねxterm.jsのサンプルの通りにやってみた。

# mkdir test
# cd test
# npm init ※プロジェクトを初期化

# npm install @xterm/xterm
<!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="test.js"></script>
    </head>
    <body>
      <div id="terminal"></div>
    </body>
  </html>
// 初期化処理
window.addEventListener('load', () => {
  var term = new Terminal();
  term.open(document.getElementById('terminal'));
  term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ');
  // 何かが送られた時
  term.onData((data) => {
    // とりあえず画面に書く
    if(data === '\r') {
      // 改行して欲しいから差替え
      term.write('\r\n');
    } else {
      term.write(data);
    }
  });
  // 何かキーが押された時
  term.onKey((e) => {
    // 特に何もしない
  });
});

上の2ファイルとnode_modulesディレクトリィをapacheのドキュメントルートのtestフォルダにコピー

  • {apacheのDocumentRoot}
    • test
      • index.html
      • test.js
      • node_modules
        • @xterm
          • xterm
            • css
              • xterm.css
            • lib
              • xterm.js

で、ブラウザからhttp://localhost/testで

【Enter】で行頭へジャンプするけど改行しないのでスクリプトで改行コードを追加した。

【Del】と【BackSpace】キーは無反応。

右クリックでコピペは出来るが【切り取り】は無反応。

複数行で貼り付けると、改行混ざってるからちゃんと置換しないとダメか。

// 初期化処理
window.addEventListener('load', () => {
  var term = new Terminal();
  term.open(document.getElementById('terminal'));
  term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ');
  // 何かが送られた時
  term.onData((data) => {
    // とりあえず画面に書く
    data = data.replaceAll('\r', '\r\n');  // 改行コードの置換
    term.write(data);
  });
  // 何かキーが押された時
  term.onKey((e) => {
    // 特に何もしない
  });
});

中途半端だけどnode.js入れたついでにlinuxシェルと繋げてみるか?

そのためのアドインがTypeScriptで出来てるっぽいのでどうしたものか?



[sed]面白い使い方

1.見たいところを抽出

# sed -n '開始行,終了行p' ファイル名

-nオプション:指定した行だけ表示する。-nとpコマンドの表記は重複するトコがお茶目
オプションは、-n, –quiet, –silent のいづれかでOK
例:sed -n ‘1,4p’ file
‘…’はシェルに特定の文字やパターンを変換させない指示なので’1,4p’なら1,4pでも構わない
例:sed -n 1,4p file

sed -n '/{開始行のパターン}/,/{開始行のパターン}/p' ファイル名

たまに逆のやり方が必要になるかもしれない

sed -n '/{開始行のパターン}/,/{開始行のパターン}/!p' ファイル名

例:sed -n ‘/start/,/end/p’ file

# sed -n '/<Directory \/>/,/<\/Directory>/p' /etc/httod/conf/httpd.conf
<Directory />
    AllowOverride none
    Require all denied
</Directory>
#

2.その範囲の中で特定のパターンを置換する

AllowOverride noneやRequire all deniedは何度の出現するので

http.confの

<Directory />と</Directory>の行の間の

AllowOverride noneをAllowOverride allに書き換えるなら

sed -i '/{開始行のパターン}/,/{開始行のパターン}s/{置換元パターン}/{置換後パターン}/' ファイル名

-iでファイルを上書きするので要バックアップ

# cp httpd.conf httpd.conf.bk ※バックアップは大事

# sed -n '/<Directory \/>/,/<\/Directory>/s/AllowOverride none/AllowOverride all/ p' httpd.conf
    AllowOverride all

で置換したテキストが表示されたので

注意深く修正して上書きする

# cp httpd.conf httpd.conf.bk ※バックアップは大事

# sed -i '/<Directory \/>/,/<\/Directory>/s/AllowOverride none/AllowOverride all/' httpd.conf
# diff  httpd.conf.bk httpd.conf
108c108
<     AllowOverride none
---
>     AllowOverride all
#

うっかり後ろのpを消し忘れるとコピーコマンドpが誤爆する

# sed -i '/<Directory \/>/,/<\/Directory>/s/AllowOverride none/AllowOverride all/ p' httpd.conf
# diff  httpd.conf.bk httpd.conf
108c108,109
<     AllowOverride none
---
>     AllowOverride all
>     AllowOverride all
#



top