JunshuO
ServiceNow Employee

How We Got ServiceNow to Print Cards at Knowledge 25 with MID Servers.png

C3シリーズの他の記事もご覧ください

 

はじめに:クラウドと物理ハードウェアの融合

 
いったいどうすれば、会議ブースでクラウドソフトウェアを使って物理的に印刷されたカードを吐き出させることができるのでしょうか?これは、Knowledge 25 で参加者向けにCreatorCon C3カードを印刷することに決めたときに遭遇した質問です。そしていくつかの可能な解決策を思いつきましたが、ServiceNowでできる限り多くのことを行いたいという願望を揺るがすことができなかったため、ファイアウォールの背後にある友人であるMIDサーバーに目を向けました。しかし、正直なところ、これは最終的に非常に人気のあるカードを現実世界へと届けたソリューションを、大幅に単純化しすぎたものです。

 

弊社のソリューションには、MIDサーバー、Javaファイル操作、サードパーティ製ホットフォルダーユーティリティ、そして最も重要な要素である、仮想MIDサーバーとそのホストを跨いだ複雑な連携が含まれていました。

 

それは、カードをサイズに合わせてカットし、プリンターに紙を補充し、カードをプラスチックのスリーブに詰め、受け取りのためにブースに整理してくれた素晴らしいスタッフの皆さんのことです。

 

おお。イントロも複雑でした。そこで、クラウドソフトウェアと物理ハードウェアを橋渡しするシステムをどのように構築したか、そしてその過程でMIDサーバーの統合について学んだことを詳しく見ていきましょう。

 

課題:クラウドからの印刷

 

ServiceNow はクラウド上にあります。プリンターは会議フロアに設置されています。彼らに話してもらうには、そのギャップを埋める必要があり、それが MID サーバーの目的です。

 

しかし、これは単なる「印刷ジョブを送信する」という単純なシナリオではありませんでした。弊社が必要としたのは、以下を処理できるものでした。

 

  • ServiceNow のステータス変更によってトリガーされるオンデマンド印刷
  • 複数のプリンターにまたがる負荷分散
  • カードの準備が整ったことをユーザーが把握できるようにする印刷完了追跡
  • 印刷および再印刷の失敗に関するエラー処理
  • MIDサーバーへの手動操作を必要とせず、ServiceNowからすべてを管理
  • Knowledgeへの輸送に必要な物理ハードウェアの重量を最小化

 

解決策:ServiceNow → MID → ホットフォルダー → プリンター

 

Printing Process Sequence Diagram.png

 

これらの課題を解決するために、最終的に構築されたフローは次のとおりです。

 

  1. ServiceNow:プロファイルレコードのステータスが「印刷キューへ送信中」に更新されます。
  2. ビジネスルール:
    1. カスタム負荷分散により、最もビジーでないMIDサーバーを選択します。
    2. MIDサーバーに印刷ジョブを送信するためのecc_queueジョブを作成します。
  3. MIDサーバー実行:JavascriptProbeがカスタムスクリプトを実行し、REST経由で画像をダウンロードします。
  4. ファイルシステム:画像がMIDサーバー上のホットフォルダーに保存されます。
  5. DNPホットフォルダーユーティリティ:画像ファイルを検出し、プリンターに送信し、ファイルをアーカイブフォルダーに移動します。
  6. 印刷同期:スケジュール済みジョブが、ホットフォルダーから移動した保留中の印刷ジョブをチェックし、それらのステータスを「カードピックアップ待ち」に更新します。
  7. 完了通知:ユーザーは、カードの準備ができたことをアプリ内通知で受け取ります。

 

Sync Print Queue Sequence Diagram.png

 

ハードウェアキット:ポータブルMIDサーバー

 

C3 MID Server Configuration.png

 

印刷ソリューションの優れた点の1つは、ソフトウェアとハードウェアの両方のソリューションを構築する必要があったことです。通常、ServiceNowで開発する場合、物理デバイスを気にする必要はありません。しかし、このケースでは、MIDサーバーをどこでホストするか、そしてKnowledgeイベントにすべての機材をどう運ぶかについて、さらに多くの考慮事項がありました。

 

そのために、MIDサーバーをホストするために手のひらに収まるほど小さいGMKTec Mini PCを購入することになりました。各ミニPCは、USBケーブルを介して1台のプリンターに接続されていました。これらのミニPCの構成は次のとおりです:

 

  • 第12世代 Alder Lake N97 プロセッサ
  • 12GB DDR5 RAM
  • Windows 11 Pro
  • LANポートを介してネットワークに接続
  • USB経由で接続されたDNP昇華型プリンター
  • Docker Desktop がインストール済み
  • DNPホットフォルダー印刷ユーティリティがインストール済み - プリンターメーカー提供のサードパーティユーティリティ
  • Linux Dockerコンテナとして実行されているMIDサーバー
    • ホットスワップ印刷ユーティリティとMIDサーバーによって共有されるホストへのフォルダーバインド
  • 必要に応じて2台のミニPC間で交換したポータブル周辺機器
    • キーボード
    • マウス
    • モニター

 

MID Server PC.jpg

 

Knowledge 25 で2台のプリンターと2台のMIDサーバーを実行しました。幸いなことに、ミニPC、ポータブルキーボードとマウスの組み合わせ、ポータブルモニター、および必要なすべてのケーブルがバックパックの持ち物に収まるので、K25への持ち運びは簡単でした。セットアップについて理解できたところで、次はプロセスの詳細をさらに深く掘り下げていきましょう。

 

ServiceNow 側:プリントのオーケストレーション

 

ビジネスルール:印刷キューに送信

 

カード画像の合成が終了すると、簡単なビジネスルールによってプロセスが開始されます。

 

(function executeRule(current, previous) {
    const printingEnabled = (gs.getProperty('x_snc_cctcg_photo.printing_enabled') == 'true');

    if (printingEnabled) {
        const printer = new C3MidPrinter();
        printer.print({ profileId: current.sys_id.toString() });
    } else {
        current.state = 'Complete';
    }
})(current, previous);

 

プロパティフラグを使用すると、コードを変更せずに印刷のオン/オフを切り替えることができます。これは、会議で印刷を開始するタイミングをテストおよび制御するために重要です。

 

スクリプトインクルード:C3MidPrinter

 

実際のオーケストレーションは、C3MidPrinter スクリプトインクルードで行われます。

 

var C3MidPrinter = Class.create();
C3MidPrinter.prototype = {
    initialize: function () {},

    _selectMidServer: function() {
        let selectedMid = {
            queueLength: Infinity,
            name: '',
        };
        let queueLengths = {};

        // Count pending prints per MID server
        let profileAggregate = new GlideAggregate('x_snc_cctcg_photo_profile');
        profileAggregate.addQuery('state', 'IN', 'Pending Print,Sending to Print Queue');
        profileAggregate.groupBy('mid_printer');
        profileAggregate.addAggregate('count');
        profileAggregate.query();

        while (profileAggregate.next()) {
            queueLengths[profileAggregate.mid_printer] = profileAggregate.getAggregate('count');
        }

        // Find MID server with shortest queue
        let mid = new GlideRecord('ecc_agent_application_m2m');
        mid.addQuery('agent.status', 'Up');
        mid.addQuery('application', '64103b0b47926a10e52659db416d43e5'); // C3 Print application
        mid.query();

        while (mid.next()) {
            let queueLength = queueLengths['mid.server.' + mid.agent.name.toString()] || 0;

            if (queueLength < selectedMid.queueLength) {
                selectedMid = {
                    queueLength: queueLength,
                    name: mid.agent.name.toString(),
                };
            }
        }

        return 'mid.server.' + selectedMid.name;
    },

    print: function ({ profileId, verbose = false }) {
        const script = `
            var printer = new C3Printer();
            printer.print();
        `;

        var instanceUrl = gs.getProperty('glide.servlet.uri');
        var outputFolder = gs.getProperty('x_snc_cctcg_photo.onsite_print_directory', '/home/mid/printqueue/');
        var midServer = this._selectMidServer();

        // Add trailing slash
        outputFolder = outputFolder.endsWith('/') ? outputFolder : outputFolder + '/';

        // Create ECC Queue record for JavascriptProbe
        var egr = new GlideRecord("ecc_queue");
        egr.agent = midServer;
        egr.queue = "output";
        egr.state = "ready";
        egr.topic = "JavascriptProbe";
        egr.name = 'C3 Card Print';
        egr.payload = `
        <?xml version="1.0" encoding="UTF-8"?>
        <parameters>
            <parameter name="script_type" value="custom_javascript_typ" />
            <parameter name="script" value="${script}" />
            <parameter name="profileId" value="${profileId}" />
            <parameter name="instanceUrl" value="${instanceUrl}"/>
            <parameter name="verbose" value="${verbose}"/>
            <parameter name="outputFolder" value="${outputFolder}"/>
        </parameters>
        `;
        egr.insert();

        // Track which MID server is handling this print
        var profileGr = new GlideRecord('x_snc_cctcg_photo_profile');
        if (profileGr.get(profileId)) {
            profileGr.mid_printer = midServer;
            profileGr.update();
        }
    },

    type: 'C3MidPrinter'
};

 

カスタム負荷分散

 

MID Server Load Balancing Cluster.png

 

この _selectMidServer() 関数は、カスタムのラウンドロビン負荷分散を実装しています。技術的には、ServiceNowのMIDサーバー実装は負荷分散(ロードバランシング)によるクラスタリングをサポートしていますが、これはMIDサーバー自体のビジー状況に基づいています。このセットアップでは、プリンターとプリンターのホットフォルダーキューがリソースの制約であり、MIDサーバーの処理ではありません。このため、印刷キューのステータスと、CreatorCon C3プロファイル内でキューが属するMIDサーバーの両方を追跡することで、印刷キューの深度を考慮したカスタム負荷分散を作成する必要がありました。
 
MID Server Custom Load Balancing.png

 

これにより、紙切れやその他の技術的な問題で一時的に印刷が停止しても、どちらかのプリンターに過負荷をかけることなく、両方のプリンターを常に稼働させることができました。仮にプリンターが数分間動作を停止したにもかかわらず、ジョブを送り続けたとしたら、どれほど状況が悪化したか想像してみてください。キューサイズに基づく負荷分散は優れたソリューションでした。

 

また、Hackzone の MID サーバーと C3 の MID サーバーを区別するロジックを追加する必要があったため、クエリが C3 アプリケーションに割り当てられた MID サーバーのみを対象とするようにしました...はい、これはハードコードされたsys_idです。

 

MIDサーバー側:ファイルの操作と印刷

 

JavascriptProbe:MIDサーバーでのカスタムスクリプトの実行

 

JavascriptProbe を使うと、MIDサーバー上でカスタムJavaScriptを実行できます。重要なのは、ECC キューペイロードを正しく設定することです。

 

<parameter name="script_type" value="custom_javascript_typ" />
<parameter name="script" value="var printer = new C3Printer(); printer.print();" />

 

この custom_javascript_typ パラメータは非常に重要です。以前使用していたMIDサーバーの古いバージョンから変更されており、間違ったり省略したりすると、スクリプトが実行されません。

 

C3Printer MID サーバースクリプトインクルード

 

実際の作業はMIDサーバーで実行されるC3Printerスクリプトで行われるので、これに関して、 John James Andersen氏 に感謝の意を表したいと思います。彼の記事は、この件に関して私にとって時間を大幅に節約してくれました。

 

// Kudos to https://john-james-andersen.com/blog/service-now/javascriptprobe-and-mid-server-script-includes.html
var C3Printer = Class.create();
C3Printer.prototype = {
    initialize: function () {
        this.File = Packages.java.io.File;
        this.FileOutputStream = Packages.java.io.FileOutputStream;
        this.HttpClient = Packages.org.apache.commons.httpclient.HttpClient;
        this.GetMethod = Packages.org.apache.commons.httpclient.methods.GetMethod;

        this.profileId = probe.getParameter('profileId');
        this.instanceUrl = probe.getParameter('instanceUrl');
        this.verbose = probe.getParameter("verbose");
        this.outputFolder = probe.getParameter('outputFolder');
    },

    print: function () {
        try {
            this.debug('Download file for profile: ' + this.profileId);

            var imageUrl = this.instanceUrl + '/api/x_snc_cctcg_photo/creatorcon_c3_webhooks/image?type=card_for_onsite_print&parentId=' + this.profileId;

            var client = new this.HttpClient();
            var get = new this.GetMethod(imageUrl);
            var status = client.executeMethod(get);
            var result;

            if (status == "200") {
                var filename = this.outputFolder + this.profileId + '.png';
                this.debug("Attempted to save the file to filename: " + filename);
                var f = new this.File(filename);
                var inputStream = get.getResponseBodyAsStream();
                var out = new this.FileOutputStream(f);
                var buf = Packages.java.lang.reflect.Array.newInstance(Packages.java.lang.Byte.TYPE, 1024);
                
                // Stream file to disk
                while ((len = inputStream.read(buf)) > 0) {
                    out.write(buf, 0, len);
                }
                out.close();
                inputStream.close();
                
                result = {
                    success: true,
                    filename: filename,
                    profileId: this.profileId,
                };
            } else {
                result = {
                    success: false,
                    profileId: this.profileId,
                    message: 'Invalid HTTP Response.  Exiting Early.'
                };
            }

            this.debug(result);
            return JSON.stringify(result);
        } catch (err) {
            result = {
                success: false,
                profileId: this.profileId,
                message: err.name,
                err: {
                    message: err.message,
                    name: err.name,
                    fileName: err.filename,
                    lineNumber: err.lineNumber,
                    stack: err.stack,
                }
            };

            this.debug(result);
            return JSON.stringify(result);
        }
    },

    debug: function (m) {
        if (this.verbose == "true") {
            ms.log(m);
        }
    },

    type: 'C3Printer'
};

 

ファイル操作のための Java パッケージアクセス

 

最も難しいのは、MIDサーバーのJavaScriptからファイルシステムとやり取りする方法を理解することでした。Javaパッケージを使用する必要があります。

 

this.File = Packages.java.io.File;
this.FileOutputStream = Packages.java.io.FileOutputStream;

 

Packages.java.io の使用方法を理解することで、標準のServiceNow APIでは利用できないファイルシステム操作が可能になります。これは、MIDサーバーにカスタムJARファイルをドロップする機能など、他のJavaパッケージにも当てはまる可能性があります。基本的に、MIDサーバーをあらゆる操作に対応できる強力なツールにしてくれます。

 

印刷完了追跡

 

ファイルがホットフォルダーに到達して印刷されたら、完了したことをどうやって知ることができるでしょうか?DNPホットフォルダーユーティリティは印刷されたファイルをアーカイブフォルダーに移動するため、ファイルがまだホットフォルダーに存在するかどうかを確認することで完了を検出できます。

 

MIDサーバーの同期機能

 

この同期機能は(MIDサーバースクリプトインクルードを介して)MIDサーバー上で実行され、指定されたMIDサーバーの「印刷処理待ち」ステータスのプロファイルレコードを照会し、ホットフォルダーにファイルがあるかどうかを確認します。そうでない場合は、印刷されてアーカイブフォルダーに移動されたとみなされます。そのため、プロファイルステータスを「カードピックアップ待ち」に更新できます。

 

syncPrintCompletion: function() {
    try {
        var updated = [];
        var profileGr = new GlideRecord('x_snc_cctcg_photo_profile');
        profileGr.addQuery('state', 'Pending Print');
        profileGr.addQuery('mid_printer', this.agent);
        profileGr.query();

        while (profileGr.next()) {
            this.debug('Record found: ' + profileGr.sys_id.toString());
            var profileId = profileGr.sys_id.toString();
            var filename = this.outputFolder + profileId + '.png';
            var f = new this.File(filename);

            // If file no longer exists, it must have been printed
            if (!f.exists()) {
                updated.push(profileId);
                profileGr.state = 'Pending Card Pickup';
                profileGr.update();
            }
        }

        return JSON.stringify({
            success: true,
            updatedProfiles: updated,
            agent: this.agent,
        });
    } catch (err) {
        // Error handling...
    }
}

 

スケジュール済みジョブ:同期のトリガー

 

MIDサーバーに印刷キューをインスタンスと同期するように知らせるecc_queueエントリを作成するスケジュール済みジョブを介して同期を実行しました。

 

const printingEnabled = gs.getProperty('x_snc_cctcg_photo.printing_enabled') == 'true';

if (printingEnabled) {
    const printer = new C3MidPrinter();
    printer.syncPrintCompletion({ verbose: true });
}

 

デフォルトではこのジョブは10分ごとに実行されていましたが、「カードの準備が完了しました」という通知を遅らせる必要がある場合は、カードをカットしてスリーブに入れるスタッフが長いキューをさばく時間を与えるために、調整を行いました。理想的には、カードをスキャンして完了したことをマークするか、カードがいつカットされ、スリーブに入り、受け取る準備ができているかを正確に識別する他のメカニズムを用意していたでしょう。しかし、それには解決にもっと時間がかかり、おそらくより多くのハードウェアが必要だったので、それは後日に残しました。

 

MIDサーバー応答の処理

 

ECC キューテーブルの2つのビジネスルールは、MIDサーバーからの応答を処理します。

 

C3 プリントセンサー(印刷ジョブの応答を処理):

 

(function executeRule(current, previous) {
    printer = new C3MidPrinter();
    printer.handleResult(current);
})(current, previous);

 

C3 印刷完了センサー(同期応答を処理):

 

(function executeRule(current, previous) {
    printer = new C3MidPrinter();
    printer.handleResult(current);
})(current, previous);

 

両方とも同じ handleResult 関数を呼び出し、その関数がXML応答を解析してプロファイルレコードを適切に更新します。

 

会議運営:現実世界のテスト

 

良かった点

 

  • 自動化が機能しました:カードはServiceNowのステータス変更から印刷出力まで自動的に処理されました。
  • 負荷分散が機能しました:どちらのプリンターも過負荷になることなく、常に稼働していました。
  • 再印刷が容易でした:ステータスをリセットしてシーケンスを再実行するUIアクションがありました。
  • 堅牢なエラー処理:DNPユーティリティは破損したファイルを無視し、有効なファイルを処理し続けました。
  • プロセス全体がServiceNowインスタンスから追跡されていたため、MIDサーバーで行うデバッグはほとんど必要ありませんでした。

 

ボトルネック

 

型抜きとスリーブのプロセスが本当の制約になりました。スタッフは次のことを行う必要がありました。

 

  1. カードが印刷されるまで待機
  2. ダイカッターを使用して写真用紙からカードを切り取る
  3. 透明なプラスチックバッジスリーブに滑り込ませる
  4. アルファベット順に整理してブースカウンターに配置する

 

より多くの人々が手伝いに飛び込み、アルファベット順の編成を実施しましたが、会議中ずっと混乱したままでした。

 

競合状態

 

同期は10分ごとに実行されるため、カードがまだスリーブされている間に、カードの準備が整ったことが通知されることがありました。通知が届くかどうかにかかわらず、ほとんどの人がブースに現れたので、これは大きな問題ではありませんでした。

 

ServiceNow 開発者のための重要なポイント

 

1. JavascriptProbe は強力ですが、扱いにくい点があります
MIDサーバーでは任意のJavaScriptを実行できますが、構文要件(特に script_type )は厳密で、それに関するドキュメントや記事はあまり多くありません。

 

2. Javaパッケージアクセスによりファイル操作が可能になる
Packages.java.io の使用方法を理解することで、標準のServiceNow APIでは利用できないファイルシステム操作が可能になります。これは、MIDサーバーにカスタムJARファイルをドロップする機能など、他のJavaパッケージにも当てはまる可能性があります。基本的に、MIDサーバーをあらゆる操作に対応できる強力なツールにしてくれます。

 

3. カスタム負荷分散は、状況によってデフォルトよりも優れている
実際のキュー深度を追跡した結果、ServiceNowのデフォルトのMIDサーバーの選択よりもプリンターの使用率が向上しました。これは、プリンターがMIDサーバー自体ではなく、リソースの制約であったためです。

 

4. リモートデバッグのための設計
MIDサーバー応答で包括的なデータを返すように設計することで、MIDサーバーに物理的にアクセスしなくてもServiceNowからデバッグできます。特に、会議でコンシューマーグレードのMIDサーバーをキャビネットの下に押し込む場合はなおさらです。快適なデスクからリモート接続できるようにすべてが設定されている場合は、それほど重要ではないかもしれません。


この記事は機械翻訳されております。最新は元となる記事をご覧ください: https://www.servicenow.com/community/developer-advocate-blog/how-we-got-servicenow-to-print-cards-at...。

免責事項: 一部の日本語は、翻訳ソフトウェアを使用してお客様の便宜のために翻訳されています。正確な翻訳をご提供できるよう相当な努力を払っておりますが、いかなる自動翻訳も人間の翻訳者に代わすることはなく、そのようなことは意図されておりません。翻訳は「現状のまま」提供されています。他言語への翻訳の的確性、信頼性または正確性については、明示または黙示を問わず、いかなる保証も行われません。翻訳ソフトには限界があるため、一部のコンテンツが正確に翻訳されていない場合があります。これらの資料の公用言語は英語です。翻訳の際に生じる相違または不一致は、コンプライアンスまたは履行の目的に関しては拘束力を有さず、法的効力はないものとします。

This article was machine translated and replicated from the original source: https://www.servicenow.com/community/developer-advocate-blog/how-we-got-servicenow-to-print-cards-at...。