共通マスタ インポート/エクスポート ジョブ その2

はじめに

こんにちは。開発部のS.Rです。気がついたら一年の半分が過ぎていました。
今後は有意義な時間が過ごせるように、振り返りのスパンを短くしようと思っています。

今回は前回の『共通マスタ インポート/エクスポート ジョブ』の続きとなります。
共通マスタをインポート/エクスポートする際のデータファイルについて解説します。

データファイル

インポート/エクスポートする際には二種類のデータファイル形式が利用可能です。

CSV フォーマット
XML フォーマット

以下ではそれぞれの違いについてまとめます。

インポートにおける違い

CSV XML
文字コード 設定ファイル内の文字コード必須 設定ファイル内の文字コード無視(XMLファイルのヘッダで判断される)
設定ファイル エンティティの指定が必須 エンティティの指定が不要
インポートするデータファイル エンティティ単位 データ領域単位

Tips
データ領域とエンティティの関係はこちらを参照ください。

3. データ領域 — IM-共通マスタ インポート・エクスポート仕様書   第11版 2020-08-01   intra-mart Accel Platform

以下に例を出します。

データ領域 エンティティ
会社グループ 会社グループ、会社グループ内包、会社グループ所属
ユーザ ユーザ、ユーザ分類所属
通貨 通貨、通貨精度、通貨換算コード、通貨レート

エクスポートにおける違い

CSV XML
ファイルの出力単位 エンティティ エンティティ、データ領域
扱える期間情報 指定した日付上の1期間のみ 存在するすべての期間

おわりに

intra-mart初学者の方にとって、情報の"期間化"は馴染み無いものと思いますが、使いこなせばとても便利なものです。
インポート/エクスポート等、使用する際の制約や注意事項を一つずつ抑えていきましょう。

公式ドキュメント document.intra-mart.jp

intra-mart 共通マスタ インポート/エクスポート ジョブ

はじめに

こんにちは。開発部のS.Rです。 時が経つのも早いもので、4月で入社2年目になりました。 これからもどうぞよろしくお願いします!

今回は共通マスタのインポート/エクスポート ジョブを取り上げます。 初歩的な内容のみですので詳細は公式ドキュメントをご確認ください。

必要なもの

インポートに必要なものは2点

1.インポート実行の設定ファイル

2.インポートするファイル

エクスポートでは1点

1.エクスポート実行の設定ファイル

初学者には馴染みがあまり無いかと思いますが、 インポート/エクスポートどちらにも、それぞれ「設定ファイル」が必要です。 ジョブを流したら、よしなにインポートなどを行ってくれるわけではないのですね。。

設定ファイルについて

配置するパスは以下

■インポート設定ファイル: <パブリックストレージ>/im_master/config/import_config.xml

■エクスポート設定ファイル: <パブリックストレージ>/im_master/config/export_config.xml

インポート設定ファイルの例です。

<app-master-import>
   <company-group-import>
       <name>sample_import</name>
       <format>XML</format>
       <file>im_master/import/data.xml</file>
       <directory>im_master/import/data.</directory>
       <start-date>2000-01-01</start-date>
       <end-date>2999-12-31</end-date>
       <encoding>UTF-8</encoding>
       <extension-param name="replace-pattern">true</extension-param>
       <extension-param name="sub-dirs">false</extension-param>
   </company-group-import>
</app-master-import>

インポートがうまくいかない場合は上記のformatで指定されるファイルの種類がインポートの資材と合っているか、 またはファイル名や文字コードなどを確認してみるとよいかもしれません。

詳細はまた次の機会に。

公式ドキュメント

document.intra-mart.jp

AWS Serverless Application Modelの概要とローカルでの動作確認における注意点

AWSが提供しているServerless Application Modelサービスの概要と、これを使って構築したアプリケーションの動作確認時の注意点を紹介したいと思います。

AWS Serverless Application Model(SAM)とは?

AWS Serverless Application Model(以下SAM)とは、サーバレスな構成、すなわち常時稼働する固有のサーバマシンを持たないようなシステムをAWS上に構築するためのサービスです。 固有のマシンを持たないというのは、具体的にはAWS Lambdaなどのサービスを使って、処理が呼び出されたときだけ一時的にCPUやメモリを割り当てられて動作するような仕組みになります。 SAMはAPI GatewayやLambdaなどの内容を設定ファイルに記述してシステム構成を定義し、それをコマンドひとつでAWSに実際のシステムとして組み建てることができます。 これによりシステムの内容をすべてソースコードや設定ファイルに落とし込んで管理することが可能になります。 インフラをコードで定義して管理する構成はInfrastructure as Code(IaC)と呼ばれており、SAMはこれを実現するためのサービスのひとつです。

ちなみに単にAWS上のリソースをコードで管理するなら、AWS CloudFormationというサービスがSANより以前から存在します。 CloudFormationは、EC2のインスタンスなどサーバレスなリソース以外も全て管理できるサービスです。 しかしその分記述する内容が細かく、管理が煩雑になる場合があります。 SAMはCloudFormationをサーバレスなシステム向けに拡張・特化させたもので、CloudFormationより自由度が下がる分、記述が簡潔で済む利点があります。

また、SAMは限定的ですがローカル環境で動作確認を実行できるという機能もあります。 簡単なデバッグなどを行いたいときは、いちいちAWSに反映させずとも済むため便利です。

インストール

SAM CLIをインストールして、各種コマンドを利用できるようにする必要があります。 AWS公式サイトに十分説明があるので、詳細な説明は割愛します。

docs.aws.amazon.com

Windows、Max、Linuxそれぞれインストール可能です。 ただしローカルでの動作確認にはDockerが必須なので、それは別途インストールしておく必要があります。

また、AWSにデプロイするにはAWS認証情報の設定が行われている必要があります。 こちらも公式ドキュメントに手順が記載されているので、そちらを参照ください。

ローカルでの動作確認手順

以下のコマンドを実行することで、必要な初期設定やサンプルファイルを構築することができます。

sam init

こちらも詳細はチュートリアルに記載されています。

docs.aws.amazon.com

チュートリルにも記載されてますが、以下のコマンドでローカル上でアプリケーションを動作させることができます。

sam local start-api

localhost:3000にアクセスすれば、起動したアプリケーションにアクセスできます。

ローカルで動作確認する場合の注意点(落とし穴?)

チュートリアルでは使われていませんが、SAMはWebSocket用のAPI Gatewayや、DynamoDBなども定義できます。 しかし、これらはAWSにデプロイすると動作しますが、ローカルでは動作しません(2021/03/25 現在)。 そのため、これらを使う前提の処理は、ローカルでそのまま動作確認できません。 動作するのはREST APIAPI Gatewayや、Lambdaだけのようです。

DynamoDBについては別途ローカルに構築して接続させることである程度対処可能ですが、WebSocketは代わりの方法も無く難しそうです。 ローカルでもWebSocketを動作できるように機能追加する動きがあるようですが、残念ながらまだ未実装のままとなっています。 なのでこれらを使う場合、デバッグはある程度AWS上で行う必要がありそうです。 SAMのメリットを十分に行かせないのは残念ですが・・・まだ新しいサービスなので、今後のアップデートに期待したいと思います。

まとめ

AWS SAMについて概要と注意点を紹介いたしました。非常に有用ではありますが、開発時にトラブルが起きないよう、十分に検証しておく必要がありそうです。 もう少し機能が成熟してくれるとより安定して使えると思いますので、今後期待したいところです。

WSL2上でのDocker環境構築について

はじめに

Windows Subsystem Linux 2(以下WSL2)が正式リリースされてだいぶ経ちます。 簡単にLinux環境を利用できるので、とても便利です。 WindowsLinux環境を使える利点はいろいろありますが、今回はDockerをインストールして使う手順と注意点を紹介したいと思います。

WSL2のセットアップ

WSL2のインストール手順は、Microsoft公式サイトで公開されているので、それを参考にインストールしましょう。

docs.microsoft.com

ディストリビューションは任意のものを選びましょう。シェアで考えるとUbuntuあたりが無難でしょうか。 WSL2とディストリビューションをインストールしたら、起動して環境内部のセットアップに移ります。

systemdの起動構築

一般的なLinux環境では、systemd(システム管理デーモン)が起動していますが、WSL2の環境ではこれが存在していません。 そのため、Dockerをインストールしてもサービスが動作しません。 これを解決する手段として、genieというものが開発されてます。

github.com

これをインストールして実行することで、systemdを動作させることが出来ます。

genie -i

ただしWSL2が一度終了して再度起動した場合は、またsystemdが存在しない状態になります。 起動のたびに上記コマンドを実行すれば解決・・・はするのですが、手作業では面倒くさい。 自動的に実行したいですが、systemdが動いてないのでcronで定義しても動作しません。rc.localファイルに書いてもダメです。 ではどうするのかというと・・・いくつか方法はありますが、私はWindows側からコマンドを実行させる方法を使っています。 以下のコマンドをWindowsコマンドプロンプトで実行すると、WSL2環境にコマンドを実行させることが出来ます。

wsl /bin/bash -l -c "genie -i"

batファイルにして実行するとコマンドプロンプトが一瞬表示されてちょっと気になるので、VBScriptを使い非表示のまま実行するように手を加えます。

CreateObject("Wscript.Shell").run "wsl /bin/bash -l -c 'genie -i'", vbHide

これをWindowsのスタートアップ時に実行させれば、自動的にWSL2環境のsystemdを動作させるようにできます。 コマンドプロンプトを管理者権限で開き、以下のコマンドを実行すると、スタートアップ時に上記の処理を書いたスクリプトファイルを実行するようにできます。

echo CreateObject("Wscript.Shell").run "wsl /bin/bash -l -c 'genie -i'", vbHide > "%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\StartUp\wsl2_genie.vbs"

Docker Engineのインストール

公式サイトの手順に従ってインストールできます。 OSごとに手順が記載されているので、自分の環境にあった手順を見てインストールしましょう。

docs.docker.com

前述したsystemdの起動ができていれば、それ以上特に特殊な手順は必要ありません。 Dockerサービスが起動していれば、以下のコマンドがエラーにならず正常に動作するはずです。

docker ps

Docker Composeのインストール

こちらはDocker Engineだけで十分な場合は、インストールしなくても問題ありません。 インストールする場合は、こちらも公式サイトの手順に従って行いましょう。

docs.docker.com

日本語でインストール手順を紹介した記事も多数ありますが、注意しないと古いバージョン番号を指定して書かれていることがあります。 公式サイトを参照したほうが無難かと思います。

まとめ

WSL2のセットアップと、その環境にDockerを構築する手順を説明しました。 Docker自体には別に特殊な手順は必要ありませんでした。systemdなどWSL2独自の特殊な仕様は注意が必要です。 WSL2は便利ですが、事前に知っておかないと困る仕様などもあります。利用する場合はそれを承知したうえで使っていきましょう。

intra-martのIM-BISで文字列アイテムを数値アイテムのように使用する際の注意点

2020年入社したM.Sと申します。
本年度からプログラミングを始めたため 若輩者ですが、私の記事が皆さんのお役にたてればと思います。

今回は私が業務でミスしてしまった箇所を紹介します。

なぜ文字列アイテムを使うのか

IM-BISで数値アイテムを使用すると.00が表示されずに省略されてしまいます。 .00を表示させる仕様であったため、数値アイテムを使用せず、文字列アイテムを使って数値表示をすることになりました。

何が起きたか

IM-BISのアクション設定において、文字列アイテムの入力イベントで小数点の付与と指定以下の小数点の切り捨てを行う以下のスクリプトを実装しました。

let comma = (Math.floor(100 * str) / 100).toFixed(2);

ところが、例として72445.40と入力すると72445.39になってしまい、入力内容が変わってしてしまう問題が発生しました。

デバッグしてみたところ、Math.floor(100 * str)の計算結果が7244539.999999999になってしまうことが分かりました。

何故こうなるかを調べたところ以下の記事で挙げられている通り、浮動小数点数の計算時に丸め誤差が発生していたことが原因でした。
https://qiita.com/Chinats/items/e2647ca7900dfe7835a8

どう対処したか

intra-martのクライアントサイドAPI(ImDecimalFormatter)を使用してフォーマット処理を行うこととしました。

前準備としてFormaアイテムの入力値を取得する関数と反映する関数を実装しておきます。 アイテムの値取得・値反映については以下に記載があります。
https://www.intra-mart.jp/document/library/bis/public/bis_specification/texts/spec/csjs_script.html

// 文字列 入力値を取得する関数
function getItemValue(type, name) {
    let data = {}, _type = type || 'product_72_textbox', item = formaItems[_type];
    return item.getItemData[name]();
};

// 文字列 入力値を反映する関数
function setItemValue(type, name, value) {
    let data = {}, _type = type || 'product_72_textbox', item = formaItems[_type];
    data[name] = value;
    item.setItemData[name]({
        data : data
    });
};

次に、ImDecimalFormatterを用いて数値文字列のフォーマットする処理を実装します。
以下はImDecimalFormatterのAPIドキュメントです。
https://www.intra-mart.jp/apidoc/iap/jsdoc/symbols/ImDecimalFormatter.html

function format(name){
    let formatter = ImDecimalFormatter.getAccountInstance();
    // 小数部分の最大桁数を設定
    formatter.setMaximumFractionDigits(2);
    // 小数部分の最小桁数を設定
    formatter.setMinimumFractionDigits(2);

     let str = getItemValue('product_72_textbox', name);

    // ImBigDecimal(任意精度のスケールなしの整数値)に変換
    formatter.parseToBigDecimal(str,function(data, textStatus, jqXHR){
        if(data.error) {
            console.log(data);
            setItemValue('product_72_textbox', name, '');  
            return;
        }    

        // 小数点以下の桁数を丸める
        let bd = data.data;
        // 一つ目めの引数は、求める小数点以下の桁数を指定
        // 二つ目の引数は、列挙型定数のRoundingMode.DOWNを指定し切り捨て
        bd = bd.setScale(2,ImRoundingMode.DOWN);

        // 区切り文字付きでフォーマット
        formatter.format(bd, function(data, textStatus, jqXHR){
            // フォーマットエラーの場合はブランクを設定
            if(data.error) {
                console.log(data);
                setItemValue('product_72_textbox', name, '');
                return;
            }
            setItemValue('product_72_textbox', name, data.data);
        });
    }); 
}

これで小数を入力した場合でも、誤差なく入力した通りの値を表示することができました。

おわりに

JavaScriptを学んでいく中で計算と日付周りは罠が多いとよく目にしましたが、 実際に望まない挙動が起こるとその意味を実感しました。 まだまだ理解が浅いため学習を続け、罠に注意して開発を行っていきたいと思います。

slackで予約投稿する方法

今年度に入社したY.Kです。

slackで予約投稿をするには、現状API拡張機能を利用するしかありません。 今回はAPIを使ってslackに予約投稿をする方法をご紹介します。

使用するAPI

今回使用するAPIはこちらになります。

chat.scheduleMessage method | Slack

投稿準備

このAPIを使うには『OAuthAccessToken』を取得する必要があります。 このOAuthAccessTokenを取得するためにはslackAppを作成しなければならないので、これよりslackAppを作成する手順を紹介します。

1.『slackAPI』にアクセス

こちらのリンクからslackAPIのサイトにアクセスします。

Slack API: Applications | Slack

アクセスできたら『Create New App』を選択します。 f:id:yamatotox:20201222132904p:plain

2.名前とワークスペースを決める

作成するslackAppの名前と、Appを作成するワークスペースを決めます。

f:id:yamatotox:20201222133021p:plain

3.『Permissions』を選択

Permissionsを選択します。

f:id:yamatotox:20201222133108p:plain

4.『Add an OAuth Scope』からスコープを選択

『Add an OAuth Scope』の中から、『chat:write』を選択します。

f:id:yamatotox:20201222133246p:plain

f:id:yamatotox:20201222133148p:plain

5.ワークスペースにインストール

『Install to Workspace』を選択します。

確認画面が出てくるので問題なければ許可をします。

許可されれば選択したワークスペースにslackAppが作成されます。

f:id:yamatotox:20201222133341p:plain

f:id:yamatotox:20201222133410p:plain

OAuthAccessTokenを取得

ワークスペースにslackAppが作成されるとこの画面に飛ぶので、ここからOAuthAccessTokenを取得します。

f:id:yamatotox:20201222133441p:plain

以上が、OAuthAccessTokenの取得手順になります。

chat.scheduleMessage APIを使う

それではこのOAuthAccessTokenを用いてchat.scheduleMessage APIを使っていきます。 こちらが投稿ページになります。

https://api.slack.com/methods/chat.scheduleMessage/test

投稿する上で以下4つの項目が必要になります。

  • token・・・・取得したOAuthAccessToken
  • channel・・・・投稿したいチャンネル名
  • post_at・・・・投稿日時
  • text・・・・本文

その他の引数について気になる場合は、Testerタブの左側にあるDocumentationを参考にしましょう。 注意点として投稿日時はUnixTime型なので、こちらから送信したい時刻をUnixTimeに変換しなければなりません。

UnixTime相互変換ツール

設定したら『Test Method』ボタンを押します。

f:id:yamatotox:20201222133558p:plain

成功すると設定した投稿日時にメッセージが予約されます。

f:id:yamatotox:20201222133622p:plain

そして予約投稿時刻にslackを見ると、

f:id:yamatotox:20201222133650p:plain

このように投稿されます。

以上、APIを使ったslackの予約投稿方法でした。

拡張機能を使った方法

googlechrome拡張機能を使った方法もあるので簡単にご紹介します。

Slack Send Later

こちらの拡張機能を入れた後、オプションに飛びます。

f:id:yamatotox:20201222133729p:plain

tokenを設定する場所があるので、そこにOAuthAccessTokenを設定します。 後はSlack Send Laterを起動してチャンネル名とメッセージ、送信予定日時を入れて送信予約ボタンを押せば予約投稿ができます。

f:id:yamatotox:20201222133818p:plain

まとめ

slackで予約投稿する方法を紹介しました。使いどころはそこまでないとは思いますが、APIを使う良い練習にはなると思うので是非使ってみてください。

IEでアロー関数が使えなかった話

はじめまして。2020年新卒入社しました。S.Rです。
まだまだ勉強中の身ですが、私の記事が皆さんの
お役にたてれば幸いです。

今回は初学者の私が少しつまずいたトピックを取り上げます。

何が起きたか

タイトル通りではありますが、ブラウザInternet Explorer(以下IE)での動作テスト中にエラーが起こる。Google Chromeでの動作は問題が無い。。。
→調べると、クライアント側のJavaScriptにおいてアロー関数がIEでは使えないことが判明しました。

アロー関数とは

下記がES5以前の書き方です。(ESについては後述します。)

var normal = function arrow() {  
    return '一般的な関数';  
};  

一方こちらがアロー関数です。
【 => 】・・・矢印(アロー)

var arrow = () => {  
    return 'アロー関数';  
};  

同じ内容を少ない文字数で書くことができるだけでなく、その他にもメリットはありますがここでは割愛します。 詳細は下記を参照ください。 https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Arrow_functions

なぜ

なぜ同じプログラミング言語でもブラウザが異なることで一部の関数だけが使えなくなるのでしょうか。

理由は大きく2点
JavaScriptにバージョンがある
②ブラウザ側がバージョンに対応する必要がある

JavaScriptにはES〇〇(数字または西暦)の名前でバージョンが複数存在します。
2015年以降は毎年新しいバージョンがリリースされています。

それに対し、IEは最新バージョンに対応していないため、
過去にリリースされたJavaScriptの機能には対応できても、
一部の新しい機能には対応できないという状況になっています。

IEではES5(2009年リリース)までが使用可能ですが、アロー関数はES6(2015年リリース)から実装された関数であるため、IEではアロー関数が使えないのです。

以下ES6の各ブラウザ対応表です。(IEは真っ赤ですね。。。)
http://kangax.github.io/compat-table/es6/

どうする?

私は今回アロー関数を使わない方針で、書き直しました。
書き直す際には以下のサイトが便利です。
https://babeljs.io/

おわりに

初学者の私にとってプログラミング言語が日々進化し、バージョン毎に違いがあることは新鮮な感覚でした。願わくば私自身も日々成長を続けられたらな、といったところです。

今年も押し迫り、体調管理には気を抜けない日々が続いておりますが、どうぞよいお年をお迎えください。

JavaScriptでAmazon Chime SDKを動かしてみる

Amazon Chime SDKの動作を確認するクライアントサイドをJavaScriptで実装してみたいと思います。

事前準備

サーバサイド

サーバサイドにて、ミーティング(meeting)と参加者(attendee)を作成して、クライアントサイドへ渡してあげる必要があります。 以下などを参考に、サーバサイドを実装してください。

gsol.hatenablog.com

Amazon Chime SDKダウンロード

以下READMEを参考に、Amazon Chime SDKをダウンロードしてください。

https://github.com/aws/amazon-chime-sdk-js/tree/master/demos/singlejs

実装内容

<html lang="ja">
<head>
  <script src="aws/amazon-chime-sdk.min.js"></script>
  <script type="text/javascript">
    var logger = '';
    var deviceController = '';
    var configuration = '';
    var meetingSession = '';
    
    // Meeting参加
    function addMeeting() {
      // サーバサイドにてミーティングと参加者を作成し、クライアントサイドで受け取る
      var meeting = {XXXX};
      var attendee = {XXXX};
      (async () => {
        logger = new ChimeSDK.ConsoleLogger('MyLogger', ChimeSDK.LogLevel.ERROR);
        deviceController = new ChimeSDK.DefaultDeviceController(logger);
        console.log('deviceController', deviceController);
             
        // ミーティングセッション作成
        configuration = new ChimeSDK.MeetingSessionConfiguration(meeting, attendee);
        console.log('configuration', configuration);
        meetingSession = new ChimeSDK.DefaultMeetingSession(configuration, logger, deviceController);
        console.log('meetingSession', meetingSession);

        // 入出力デバイス取得(ブラウザはマイクとカメラの許可を求める)
        const audioInputDevices = await meetingSession.audioVideo.listAudioInputDevices();
        console.log('audioInputDevices', audioInputDevices);
        const audioOutputDevices = await meetingSession.audioVideo.listAudioOutputDevices();
        console.log('audioOutputDevices', audioOutputDevices);
        const videoInputDevices = await meetingSession.audioVideo.listVideoInputDevices();
        console.log('videoInputDevices', videoInputDevices);

        meetingSession.audioVideo.chooseVideoInputQuality(1280,720,3,1000);
        await meetingSession.audioVideo.chooseVideoInputDevice(videoInputDevices[0].deviceId);
        await meetingSession.audioVideo.chooseAudioInputDevice(audioInputDevices[0].deviceId);
        if (audioOutputDevices[0]) {
          await meetingSession.audioVideo.chooseAudioOutputDevice(audioOutputDevices[0].deviceId);
        }

        // オーディオ要素取得・バインド
        const audioElement = document.getElementById('audio-preview');
        meetingSession.audioVideo.bindAudioElement(audioElement);

        const videoElements = {}; // ビデオタイル要素
        for (let i = 0; i < 16; i++) {
          videoElements[i] = document.getElementById(`video-preview` + i);
        }
        const indexMap = {};

        const acquireVideoElement = tileId => {
          // 既にバインドされている場合、同要素を返却
          for (let i = 0; i < 16; i++) {
            if (indexMap[i] === tileId) {
              return videoElements[i];
            }
          }
          // バインド可能な要素を返却
          for (let i = 0; i < 16; i++) {
            if (!indexMap.hasOwnProperty(i)) {
              indexMap[i] = tileId;
              return videoElements[i];
            }
          }
          return;
        }
        if (!acquireVideoElement) {
          alert('利用可能なビデオ要素がありません。');
        };

        // observer設定
        const observer = {
          audioVideoDidStart: () => {
            console.log('Started');
          },
          // 映像要素取得・バインド
          videoTileDidUpdate: tileState => {
            if (!tileState.boundAttendeeId) {
              return;
            }
            console.log('Start video');
            meetingSession.audioVideo.bindVideoElement(tileState.tileId, acquireVideoElement(tileState.tileId));
          }
        };

        // 出力開始
        meetingSession.audioVideo.addObserver(observer);
        meetingSession.audioVideo.start();
        meetingSession.audioVideo.startLocalVideoTile();

        document.getElementById('meetingId').value = meetingInfo.Meeting.MeetingId;
        document.getElementById('attendeeId').value = attendeeInfo.Attendee.AttendeeId;
       }) ();
    },
    error : function() {
      console.log('通信エラーです');
    }

    // Meeting退室
    function leaveMeeting() {
      // observer設定
      const observer = {
        audioVideoDidStop: sessionStatus => {
          const sessionStatusCode = sessionStatus.statusCode();
          if (sessionStatusCode === ChimeSDK.MeetingSessionStatusCode.Left) {
            alert('退室しました。');
          } else {
            alert('セッションが切れました。ステータスコード: ' + sessionStatusCode);
          }
        }
      };
    }
  </script>
</head>
<body>
  <div>
    video test<br>
    MeetingId : <input id="meetingId" value="" style="width:300px"/><br>
    AttendeeId : <input id="attendeeId" value="" style="width:300px"/>
    <br>
    <button type="button" value="" onclick="addMeeting()">参加</button>
    <button type="button" value="" onclick="leaveMeeting()">退室</button>
  </div>
  <video id="video-preview0" style="width:100%; height: 200px"></video>
  <video id="video-preview1" style="width:100%; height: 200px"></video>
  <video id="video-preview2" style="width:100%; height: 200px"></video>
  <video id="video-preview3" style="width:100%; height: 200px"></video>
  <video id="video-preview4" style="width:100%; height: 200px"></video>
  <video id="video-preview5" style="width:100%; height: 200px"></video>
  <video id="video-preview6" style="width:100%; height: 200px"></video>
  <video id="video-preview7" style="width:100%; height: 200px"></video>
  <video id="video-preview8" style="width:100%; height: 200px"></video>
  <video id="video-preview9" style="width:100%; height: 200px"></video>
  <video id="video-preview10" style="width:100%; height: 200px"></video>
  <video id="video-preview11" style="width:100%; height: 200px"></video>
  <video id="video-preview12" style="width:100%; height: 200px"></video>
  <video id="video-preview13" style="width:100%; height: 200px"></video>
  <video id="video-preview14" style="width:100%; height: 200px"></video>
  <video id="video-preview15" style="width:100%; height: 200px"></video>
  <audio id="audio-preview"></audio>
</body>
</html>

各メソッドなどは以下を参照してください。

https://aws.github.io/amazon-chime-sdk-js/

動作確認

ミーティング参加

f:id:sanok-gsol:20201130173256p:plain

ミーティング退出

f:id:sanok-gsol:20201130173311p:plain

注意点

ミーティングセッションの自動終了

生成したミーティングは、音声接続が無いまま5分経過すると自動的にセッションが終了します。 その他ミーティングのセッションが自動的に終了する場合がありますので、以下をご参照ください。

https://docs.aws.amazon.com/chime/latest/dg/mtgs-sdk-mtgs.html

ブラウザの制約

Chromiumをベースにしているブラウザ(Chrome、Edgeなど)では、「https」もしくは「localhost」にてアクセスしないと、「getUserMedia」メソッドが正常に動作しないため、 ビデオ映像や音声などが出力されません。 以下をご参照ください。

https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins

Amazon Chime のミーティングと参加者をJavaで作成する

AWS SDK for Java(以下SDK)を用いて、Amazon Chime(以下Chime)のミーティングと参加者を作成してみたいと思います。

事前準備

credentialsの設定

まず、IAMユーザを作成して「AmazonChimeFullAccess」、「 AmazonChimeSDK」の権限を付与したcredentialsを作成してください。 credentialsをサーバに設定する方法は以下をご参照ください。

https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html

AWS SDK for Javaダウンロード

以下を参照し、SDKをダウンロードして展開してください。

https://docs.aws.amazon.com/ja_jp/sdk-for-java/v1/developer-guide/setup-install.html

実装内容

ディレクトリ構成例

{project}
│  .classpath
│  .project
│
├─.settings
│      org.eclipse.jdt.core.prefs
│
├─bin
│  └─chime
│          createMeeting.class
│
├─lib
│      aws-java-sdk-1.11.884.jar
│
├─src
│  └─chime
│          createMeeting.java
│
└─third-party
    └─lib
          aspectjrt-1.8.2.jar
          aspectjweaver.jar
          aws-swf-build-tools-1.1.jar
          commons-codec-1.11.jar
          commons-logging-1.1.3.jar
          freemarker-2.3.9.jar
          httpclient-4.5.9.jar
          httpcore-4.4.11.jar
          ion-java-1.0.2.jar
          jackson-annotations-2.6.0.jar
          jackson-core-2.6.7.jar
          jackson-databind-2.6.7.3.jar
          jackson-dataformat-cbor-2.6.7.jar
          javax.mail-api-1.4.6.jar
          jmespath-java-1.11.884.jar
          joda-time-2.8.1.jar
          netty-buffer-4.1.48.Final.jar
          netty-codec-4.1.48.Final.jar
          netty-codec-http-4.1.48.Final.jar
          netty-common-4.1.48.Final.jar
          netty-handler-4.1.48.Final.jar
          netty-resolver-4.1.48.Final.jar
          netty-transport-4.1.48.Final.jar
          spring-beans-3.0.7.RELEASE.jar
          spring-context-3.0.7.RELEASE.jar
          spring-core-3.0.7.RELEASE.jar
          spring-test-3.0.7.RELEASE.jar

ダウンロードした「AWS SDK for Java」から持ってきた「{project}/lib」配下と「{project}/third-party/lib」配下のライブラリにはビルド・パスを通してください。

ソース(createMeeting.java)

package chime;

import com.amazonaws.AmazonClientException;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.services.chime.AmazonChime;
import com.amazonaws.services.chime.AmazonChimeClientBuilder;
import com.amazonaws.services.chime.model.Attendee;
import com.amazonaws.services.chime.model.CreateAttendeeRequest;
import com.amazonaws.services.chime.model.CreateAttendeeResult;
import com.amazonaws.services.chime.model.Meeting;
import com.amazonaws.services.chime.model.CreateMeetingRequest;
import com.amazonaws.services.chime.model.CreateMeetingResult;

public class createMeeting {

    public static void main(String[] args) {
    
        // credentials確認
        ProfileCredentialsProvider credentialsProvider = new ProfileCredentialsProvider();
        try {
            credentialsProvider.getCredentials();
        } catch (Exception e) {
            throw new AmazonClientException(
                    "Cannot load the credentials from the credential profiles file. " +
                    "Please make sure that your credentials file is at the correct " +
                    "location (~/.aws/credentials), and is in valid format.",
                    e
            );
        }
        
        // Chime リージョン設定
        AmazonChime chime = AmazonChimeClientBuilder.standard().withRegion("us-east-1").build();
        
        // Meetingリクエスト作成
        CreateMeetingRequest meetingReq = new CreateMeetingRequest();
        meetingReq.setClientRequestToken("test");
        meetingReq.setMediaRegion("ap-northeast-1");
        // Meeting作成
        CreateMeetingResult meetingRes = chime.createMeeting(meetingReq);
        Meeting meeting = meetingRes.getMeeting();
        // MeetingId取得
        String meetingId = meeting.getMeetingId();
        
        // Attendeeリクエスト作成
        CreateAttendeeRequest attendeeReq = new CreateAttendeeRequest();
        attendeeReq.setExternalUserId("test_user");
        attendeeReq.setMeetingId(meetingId);
        // Attendee作成
        CreateAttendeeResult attendeeRes = chime.createAttendee(attendeeReq);
        Attendee attendee = attendeeRes.getAttendee();
        
        System.out.println(meeting);
        System.out.println(attendee);
    }
}

各クラスやメソッドなどは以下を参照してください。

https://sdk.amazonaws.com/java/api/latest/

動作確認

以下の通り、meeting情報とattendee情報が生成されていることが確認できます。 ※一部マスクしてます

・meeting

{
  MediaPlacement:{
    AudioFallbackUrl: "wss://haxrp.m3.an1.app.chime.aws:443/calls/f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    AudioHostUrl: "XXXXc6f84f16ea0ec5ddbd5bbab101a5.k.m3.an1.app.chime.aws:3478",
    ScreenDataUrl: "wss://bitpw.m3.an1.app.chime.aws:443/v2/screen/f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    ScreenSharingUrl: "wss://bitpw.m3.an1.app.chime.aws:443/v2/screen/f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    ScreenViewingUrl: "wss://bitpw.m3.an1.app.chime.aws:443/ws/connect?passcode=null&viewer_uuid=null&X-BitHub-Call-Id=f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    SignalingUrl: "wss://signal.m3.an1.app.chime.aws/control/f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    TurnControlUrl: "https://ccp.cp.ue1.app.chime.aws/v2/turn_sessions"
  },
  MediaRegion: "ap-northeast-1",
  MeetingId: "f02353d3-122c-4c21-9cbb-076e56e2XXXX"
}

・attendee

{
  Attendee:{
    AttendeeId: "efca3edd-1973-3801-517f-69cc2df3XXXX",
    ExternalUserId: "test_user",
    JoinToken: "ZWZjYTNlZGQtMTk3My0zODAxLTUxN2YtNjljYzJkZjM1YTAyOjA0ODBiN2VmLTcwNTctNGJiNS05NmM0LWI2NmMwOWQ5NDXXXX"
  }
}

サーバサイドの処理については以上となります。ここで生成した情報を利用することで、ミーティングに参加してビデオチャットを行えるようになります。画面の実装については、続きの記事に記載していますので、そちらもご参考ください。

gsol.hatenablog.com

Apache Airflowの紹介(kintoneとの連携編)

Airflowとkintoneアプリを連携して、期日が近づくとメール送信・Slack通知する処理を作成したので紹介します。

処理の概要

次のようなイメージで処理を作成していきます。

  • kintoneの契約書管理アプリから各レコードの契約満了日を取得。
  • 契約自動更新が無しで直近(一週間以内)で契約満了となるのものについて、契約更新のお知らせメール・Slackメッセージを送信。

kintoneアプリ

デフォルトで用意されている契約書管理アプリを利用します。
f:id:toheih:20201012154335p:plain

  • 連絡先にはメールアドレスを指定することとします。

AirflowのDAG作成

以下のタスクを実行するDAGを作成します。

  • kintoneからREST APIでデータ取得。
    • レコードを一括取得するカーソルを作成。(SimpleHttpOperator)
    • カーソルIDをXComから取得。(PythonOperator)
    • カーソルからデータを取得。(SimpleHttpOperator)
  • 取得結果からメール本文を作成し、メール送信処理用のDAGを実行(PythonOperator)
  • メール送信処理を実行(EmailOperator)

XComとは、タスク間で変数のやり取りをするための仕組みです。
また、メール送信処理については複数回実行可能とするためデータ取得処理のDAGとは別のDAGを作成し、データ取得処理DAGから呼び出すようにします。

kintoneのデータ取得処理

複数レコードを取得する必要があるため、カーソルを利用して取得します。

# REST API実行結果判定処理
def response_check(response):
    result=response.json()
    return response.status_code == requests.codes.ok

# カーソル作成タスク
create_cursor = SimpleHttpOperator(
    task_id='create_cursor',
    http_conn_id='',
    method='POST',
    endpoint='https://(サブドメイン名).cybozu.com/k/v1/records/cursor.json',
    data=json.dumps(
        {
            'app': '(アプリID)',
            'fields' : ['company_name', 'tanto_name', 'tanto_address', 'keiyaku_manryo_date'],
            'query' : 'keiyaku_manryo_date <= THIS_WEEK() and keiyaku_auto_update not in ("有")'
        }
    ),
    headers={'X-Cybozu-API-Token': '(kinoneのAPIトークン)', 'Content-Type' : 'application/json'},
    response_check=response_check,
    log_response=True,
    xcom_push=True,
    dag=dag,
)
  • response_checkで指定した関数によって、タスクを正常終了とするかエラーとするかを決定できます。今回はHTTPステータスコードで判定しています。
  • xcom_push=Trueにすることで、タスクの実行結果をXComに保存することが出来ます。
    • 保存した値はタスクのViewLogから確認することが出来ます。

f:id:toheih:20201015160247p:plain

次に、カーソルからレコードデータを取得します。
取得にはカーソルIDが必要で、カーソル作成APIの取得結果から取得します。

# カーソルID取得処理
def get_id_from_xcom(**context):
    jsonvalue = context['ti'].xcom_pull(task_ids='create_cursor')
    value = json.loads(jsonvalue)
    return value['id']

# カーソルID取得タスク
get_id = PythonOperator(
    task_id='get_id',
    dag=dag,
    provide_context=True,
    xcom_push=True,
    python_callable=get_id_from_xcom,
)

# レコードデータ取得タスク
open_cursor = SimpleHttpOperator(
    task_id='open_cursor',
    http_conn_id='',
    method='GET',
    endpoint='https://(サブドメイン名).cybozu.com/k/v1/records/cursor.json',
    data={"id" : "{{ ti.xcom_pull(task_ids='get_id') }}"},
    headers={'X-Cybozu-API-Token': '(kinoneのAPIトークン)'},
    response_check=response_check,
    log_response=True,
    xcom_push=True,
    dag=dag,
)
  • context['ti'].xcom_pull(task_ids='xxxx')でXComに保存された値を取得することが出来ます。
    • また"{{ ti.xcom_pull(task_ids='xxxx') }}"と記載することで文字列に埋め込むことも出来ます。
  • カーソルから取得したレコードデータは以下の通りXComに保存されます。

f:id:toheih:20201015160553p:plain

メール・メッセージ送信処理

レコードデータからメール・メッセージ本文を作成し、別DAGに渡します。

# 本文作成処理
def create_mail_content_from_xcom(**context):
    jsonvalue = context['ti'].xcom_pull(task_ids='open_cursor')
    value = json.loads(jsonvalue)
    for keiyaku_info in value['records']:
        k = {'mail_content' : '', 'slack_content' : '', 'mail_address' : keiyaku_info['tanto_address']['value'] }
        mail_content = keiyaku_info['company_name']['value'] + '<br/>'
        mail_content += keiyaku_info['tanto_name']['value']+ '様<br/><br/>'
        mail_content += '平素よりサービスをご利用いただき、誠にありがとうございます。<br/>'
        mail_content += 'お客様の契約有効期限が間近となっておりますのでお知らせいたします。<br/>'
        mail_content += 'このメールは ' + keiyaku_info['keiyaku_manryo_date']['value'] +' に契約満了を迎える方へ送信しています。<br/>'
        mail_content += '更新のお手続きをお願い致します。'
        k['mail_content'] = mail_content
        k['slack_content'] = mail_content.replace('<br/>','\n')
        trigger_dag(dag_id='sub_notice',
                    conf=json.dumps(k),
                    execution_date=None,
                    replace_microseconds=False)
        pass
    return

# 本文作成処理実行タスク
create_mail_content = PythonOperator(
    task_id='create_mail_content',
    dag=dag,
    provide_context=True,
    xcom_push=False,
    python_callable=create_mail_content_from_xcom,
)
  • trigger_dagで別DAGを実行することが出来ます。またconfに指定した値は別DAGで参照することが可能です。

別DAGではメール送信処理・Slackメッセージ送信処理を実装します。

# メール送信処理タスク
send_email = EmailOperator(
    task_id='send_email',
    dag=sub_dag,
    to="{{ dag_run.conf['mail_address']}}",
    subject='契約満了のお知らせ',
    html_content="{{ dag_run.conf['mail_content']}}",
    mime_charset='utf-8',
)

# Slackメッセージ送信処理タスク
send_slack = SlackWebhookOperator(
    task_id='send_slack',
    dag=sub_dag,
    http_conn_id='slack_test',
    message="{{ dag_run.conf['slack_content']}}",
)

send_email >> send_slack
  • Slack送信を行う場合はあらかじめConn IdとWebhook URLをAirflow側に登録しておく必要があります。
    • Admin > Connection から登録することが出来ます。
    • Conn IdはSlackWebhookOperatorのhttp_conn_idに指定します。

f:id:toheih:20201015165504p:plain

動作確認

実際にkintoneにデータを登録します。
f:id:toheih:20201015182719p:plain

AirflowのDAGを実行し、エラーなく完了しました。
f:id:toheih:20201015182956p:plain

メールが届くことが確認できました。
f:id:toheih:20201015183059p:plain

Slackのほうにもちゃんとメッセージが送られています。
f:id:toheih:20201016131639p:plain

今回はDAGを手動で実行しましたが、スケジュール実行も可能なので定期的にデータをチェックして通知することも可能です。

以上、Airflowとkintoneの連携について紹介しました。

Apache Airflowの紹介(プラグイン編)

前回に引き続きAirflowについて紹介します。
今回はプラグインについてです。

プラグインとは

Airflowで使用するOperatorやHook、Sensorを独自に作成することができる機能です。
※Hookとは外部リソースとのやり取りをするプログラムで、Sensorはあるアクションが発生した際に実行されるOperatorです。
今回は独自のOperator(カスタムOperator)を作成してみます。

作成方法

事前準備

前回用意したdocker-compose.xmlにvolumesの設定を追加します。
プラグイン用のフォルダを設定します。

 volumes:
     - ./dags:/usr/local/airflow/dags
     - ./plugins:/usr/local/airflow/plugins

フォルダ構成

docker-compose up をするとpluginsフォルダが作成されます。
pluginsフォルダ配下に以下のファイル、フォルダを作成します。

plugins
├─sample_plugin.py・・・・・・・プラグインクラス
└─operators
     └─ sample_operator.py ・・・カスタムOperatorクラス

プラグインクラスの作成

sample_plugin.pyはプラグインクラスになります。
プラグインとして追加するOperatorの名前を定義します。
以下のように定義します。

from airflow.plugins_manager import AirflowPlugin
from operators.sample_operator import SampleOperator

class SamplePlugin(AirflowPlugin):
    name = "sample_plugin"
    operators = [SampleOperator]

プラグインクラスでは、airflow.plugins_manager.AirflowPluginクラスを継承する必要があります。
またSampleOperatorはこれから作成するカスタムOperatorクラスです。

カスタムOperatorクラスの作成

sample_operator.pyはカスタムOperatorクラスになります。
このクラスで具体的に実行する処理を定義します。
今回は公式サイトにあるサンプル処理(渡された引数に「Hello」をつけてログ出力する処理)を使用します。

from airflow.models.baseoperator import BaseOperator
from airflow.utils.decorators import apply_defaults

class SampleOperator(BaseOperator):

    @apply_defaults
    def __init__(
            self,
            name: str,
            *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.name = name

    def execute(self, context):
        message = "Hello {}".format(self.name)
        print(message)
        return message

カスタムOperatorクラスではairflow.models.baseoperator.BaseOperatorクラスを継承する必要があります。

動作確認

作成したOperatorを実行するDAGを作成します。
Operatorの引数には「toheih」を渡します。

from datetime import timedelta
from airflow import DAG
from airflow.utils.dates import days_ago

from airflow.operators.sample_plugin import SampleOperator

default_args = {
    'owner': 'airflow',
    'depends_on_past': False,
    'start_date': days_ago(0),
    'email': ['airflow@example.com'],
    'email_on_failure': False,
    'email_on_retry': False,
    'retries': 0,
    'retry_delay': timedelta(minutes=5),
}
dag = DAG(
    'hello',
    default_args=default_args,
    description='hello DAG',
    schedule_interval=timedelta(days=1),
)

hello = SampleOperator(
    task_id='sample-task',
    name='toheih',
    dag=dag,
)

hello

Aiflowにデプロイして動作させてみます。
プラグインを有効にするにはAirflowを再起動する必要があります。
f:id:toheih:20200611132834p:plain

エラーなく動作完了しました。
f:id:toheih:20200611132827p:plain

以下の通り「Hello toheih」と表示されることが確認できました。
f:id:toheih:20200611132831p:plain


以上、簡単ですがAirflowのプラグイン機能について紹介しました。

Apache Airflowの紹介(概要・環境構築編)

今回は、ApacheOSSとして提供しているAirflowを調査しましたので
Airflowでできることと、環境構築について紹介したいと思います。

Airflowでできること

Airflowではワークフローの作成とスケジュール実行、モニタリングを行うことができます。
ここで言うワークフローとは、複数のタスクを依存関係に従って実行していくものを言います。

ワークフローの作成

AirflowではワークフローはDAGと呼ばれています。
DAGとは有向非巡回グラフのことで、大雑把に説明すると各ノード(タスク)に順番をつけることができ(有向)、一度実行したノードを再実行しない(非巡回)グラフです。
ja.wikipedia.org
DAGはPythonで各タスクを定義することで作成します。

DAGを構成するタスクはOperatorと呼ばれており
Bashコマンドの実行や外部Pythonスクリプトの実行、HTTPリクエストの送信、Slackへの通知など様々なOperatorが用意されています。
Python API Reference — Airflow Documentation

簡単なワークフローであれば
用意されているOperatorを組み合わせるだけで作成することができます。

また、独自に開発したOperatorをプラグインとして追加することも可能です。
Plugins — Airflow Documentation

スケジュール実行

DAGに対して開始日時や実行間隔などが設定でき、定期実行させることが可能です。
タスクでエラーが発生した場合の再実行回数など、エラーハンドリングについても設定可能です。
設定はDAGのパラメータとして記述します。
https://airflow.apache.org/docs/stable/tutorial.html#default-arguments

モニタリング

DAGの一覧、各タスクの実行状況、タスクの依存関係などを確認できる管理コンソールが用意されており、Webブラウザ上で確認することができます。
失敗したタスクを手動で再実行させることも可能です。

f:id:toheih:20200513151757p:plain
DAGに定義された各タスクの実行状況が視覚的に確認できます。

Airflowの環境構築

Airflowのセットアップ

公式サイトでは数コマンドで完了させていますが、
githubにてDockerfileが公開されており、今回はこちらを利用して環境構築を行いました。
GitHub - puckel/docker-airflow: Docker Apache Airflow

以下3つの構築方法(Executor)があるようで
今回はLocalExecutorを利用しました。

  • SequentialExecutor
  • LocalExecutor
  • CeleryExecutor

コンテナの開始後、http://localhost:8080Webブラウザでアクセスし以下画面が表示されればセットアップ完了です。
f:id:toheih:20200513154903p:plain

DAGの作成とデプロイ

コンテナ開始後、dagsフォルダが自動で作成されます。
そこにPythonスクリプトで記述したDAGを配置すると、自動デプロイされ実行可能になります。
今回は公式サイトにあるチュートリアルのDAGを配置します。
airflow.example_dags.tutorial — Airflow Documentation

f:id:toheih:20200513160903p:plain
DAGを配置すると自動でデプロイされます。

DAGの実行

左端に表示されているトグルをOnにすると、
DAGが設定したスケジュール通りに実行されます。
チュートリアルDAGの場合、2日前を基準にして2回実行されます。
なお、右側のボタンをクリックするとスケジュールに関係なく即時実行されます。
f:id:toheih:20200513162203p:plain

DAG名をクリックするとTree View画面が表示され、DAGの実行状況が確認できます。
f:id:toheih:20200513162905p:plain

チュートリアルDAGでは以下タスクを定義していますが、
これらの実行結果を画面から確認することができます。

  • 現在日付を表示
  • Bashのsleepコマンドで5秒間処理を遅延
  • Jinja Templateを利用して現在日付と一週間後の日付、メッセージを表示

各タスクのステータスをクリックするとタスクの詳細画面が表示されます。
ViewLogをクリックして実行ログを確認します。
f:id:toheih:20200513165234p:plain

実行ログに現在の日付が表示されていることが確認できます。
f:id:toheih:20200513165527p:plain


以上、簡単ですがAirflowについて紹介しました。
今後は、Operatorプラグインの開発やREST APIを利用した外部サービス連携などについて紹介できればと思います。

httpのバージョンによる違いと古い場合の問題について

開発部のS.Kです。 今回は、普段意識しないhttpのバージョンによる小話を紹介したいと思います。

httpとは

httpというのは、ブラウザがWebサーバと通信するときに使っている通信プロトコルです。 現在はバージョン3(HTTP/3)まで規格されています。 バージョン1では1つデータをリクエストすると、それが返ってくるまで次のデータをリクエストできないという問題がありました。 バージョン2では複数のデータをまとめてリクエストできるようになり、効率が飛躍的に向上しています。 バージョン3は色々特殊になるので今回は割愛。

ブラウザによる工夫

http/1で単純にページを表示しようとすると、効率が悪く時間がかかります。 そこで、現在使われているようなブラウザでは、サーバとの接続を複数本作り、ある程度平行してデータを要求できるように工夫しています。 ただこの方法も限度があり、同時接続数には上限が設定されています。 例えばGoogleChromeでは6つが上限のようです(2020年1月現在)。

http/1で通信するときに起きるトラブル

http/2で通信するには、ブラウザとサーバがそれぞれ対応している必要があります。 ブラウザは、よほど古いものでなければ、まず対応しているでしょう。 それに対してサーバは様々なので、中にはhttp/1しか対応していない可能性もあります。 普通に使う分にはhttp/1でもそれほど気にならないと思いますが、特殊なケースではうまく動かなくなることがあります。 例えばintra-martのIMBoxでは、簡易チャット機能としてDirectMessageというものがあります。 相手からメッセージが来ると画面に表示してくれるのですが、この機能を実現するために、常にサーバと接続を保っています。 なので、サーバがhttp/1しか対応しておらず、DirectMessageを使える画面を6つ以上開くと、接続数が上限に達して、それ以上画面を表示できなくなります。 その状態では別ページに移動することも出来ないので、急に何も操作できなくなって驚くことでしょう。

まとめ

http/1で通信すると、表示が遅いし、多重にページを開くとうまく動かないことがある、という小話でした。 サーバを運営している方は、きちんとhttp/2で動くようになっているか確認しましょう。

Formaのエラーメッセージの表示順を変える方法

intra-martのFormaDesigerで、入力チェックエラー時のエラーメッセージの表示順を変える方法を紹介します。

やりたいこと

FormaDesignerで作った画面で、入力チェックエラーの表示の順番を変えたい、と思ったことありませんか?

例えば以下のような画面の場合。画面項目の並びとエラーメッセージの表示順が合っていません。

画面項目の並び順と同じく、エラーメッセージも

  • 新住所
  • 郵便番号
  • 電話番号
  • 旧住所

の順で出したいですよね。どうやったら変更できるのでしょうか。

f:id:hamaguchiyu:20191220173741p:plain

やること

アイテムの配置順を変える

やり方としては、Fromaのフォーム編集画面で「アイテムの配置順」を変えるという方法があります。「アイテムの配置順」はアイテムを選択し、右クリックで開いたメニューから変更できます。

今回の場合は「郵便番号」に対し、「背面へ移動」× 2回を行っています。

f:id:hamaguchiyu:20191220174244p:plain

変更したアイテムの配置順はラベル一覧で確認することができます。配置順を変えた結果、ラベル一覧の順番も以下のように変わっています。

f:id:hamaguchiyu:20191220173841p:plain

結果

f:id:hamaguchiyu:20191220173904p:plain

アイテムの配置順を変えたことでエラーメッセージも

  • 新住所
  • 郵便番号
  • 電話番号
  • 旧住所

の順で表示されるようになりました。

VirtualBoxで仮想マシンを最小サイズでエクスポートするテクニック

開発部のS.Kです。 VirtualBox仮想マシンをエクスポートするときの、ちょっとしたテクニックを紹介したいと思います。

概要

VirtualBox仮想マシンを作成して、色々なアプリケーションをインストールしてからエクスポートすると、他の環境でもすぐにその環境が使えて便利です。 しかし、OSをインストールしてアプリケーションも追加した後の仮想マシンをそのままエクスポートすると、かなり大きなファイルサイズになってしまいます。 場合によっては数十GBにもなり、流石にここまでファイルサイズが大きいと扱いにくて大変です。

ですがこれに少し工夫をすることで、エクスポートファイルのサイズを最小限にすることが出来ます。 今回は、そのテクニックを紹介します。

前提条件

仕組み

エクスポートファイルの中身の大半は、仮想ハードディスクです。 これをいかに小さく出来るかがポイントになります。 可変サイズの仮想ストレージを使っていると、保存領域が不足したときに、上限に達するまで少しずつ自動で拡張されていきます。 しかし、拡張は自動で行われても、縮小は自動で行われず、その後に空き領域が十分に出来てもそのままになります。 この状態でエクスポートすると、本当は使っていない領域の分までエクスポートされ、ファイルサイズが大きくなってしまいます。

縮小を行うコマンドは用意されており、手動で実行は出来ます。 ただし、これを使う場合は事前に未使用領域を0埋めしておく必要があります。 なので、まずは未使用領域を0埋めして、その後に仮想ストレージの縮小コマンドを実行することで、仮想ストレージを最小サイズにすることが出来ます。

手順

まずは、仮想マシンにログインして、空き領域を0埋めするために以下のコマンドを実行します。

$ sudo dd if=/dev/zero of=zero bs=4k
$ sudo rm zero

これを実行すると、空き容量と同じサイズのzeroというファイルが作られ、直後に削除されます。 zeroは中身に全て0が入力されたバイナリデータです。 これを空き領域いっぱいに作成してから消すことで、空き領域全てに0が書き込まれます。

この後、仮想マシンをシャットダウンして、ホストマシンで以下のコマンドを実行します。

VBoxManage.exe modifyhd "仮想ストレージのuuid" --compact

実行すると、0で埋められた未使用領域を開放して、仮想ストレージのファイルサイズを縮小してくれます。 実行前後で仮想ストレージファイルを比べると、ファイルサイズが減っていることが分かると思います。

最後に、この状態で仮想マシンのエクスポートを行います。 問題なければ、最小限のサイズで仮想マシンがエクスポートされるはずです。

まとめ

仮想マシンを最小サイズでエクスポートする方法を紹介しました。 それほど大きな手間も掛からないため、仮想マシンをエクスポートするときは、常に行っても良いと思います。