HUMCOM SubSystem

XPagesアプリケーションを補助するシステム(サブシステム)があると、いろいろ便利。

まぁ、Dominoにはサーバエージェントがあるのだが、実は触ったことがない。その代わりになるものを自作してみようというもの。

ここで紹介しているスクリプトはsubSystem.zipからコピーしてほしい。

基本的なしくみ

SubSystem専用のWindowsPCを用意して、NotesClientとRubyをいれておく。 あと、RubyからNotesClientを制御するNotesgripもいれておく。

HUMCOM SubSystemのしくみ

ここでSubSystemのRubyスクリプトを動作させておき、タイマーやコマンドレシーバを仕掛けておくことで、NotesClientを通してできることは何でもできる。

XPagesアプリからHUMCOM SubSystemに指示を送る方法だが、一般的にはWebAPIを使うのが普通。

今回はちょっと変わった方法で、メールを使ってみよう。メールのSubjectにコマンドと認証キーを入れて送ればSubSystemに届く。 ただ、WebAPIとちがってサーバからの応答を得てから、何か処理するという方法が取れない。XPagesから一方的に指示を送っておき、サブシステムの方で適当に処理しといてよ、というタイプに使える。

例えば、発番処理。サーバから応答をもらって番号を書き込むのではなく、番号の発番を依頼しておき、そのうち番号がSubSystemによって書き換えられるようにしておく。 で、書き換えが行われるまで文書を開けないようにしておけばいい。

SubSystemサーバの実装

Dominoサーバからのメールを受信する仕組み。意外と簡単。

メールサーバを作る

DominoサーバからはSMTPプロトコルでメールが送信される。それを受け取る細かい処理はライブラリに任せるとして、メインの処理はこんなもの。

  01: #!/usr/local/bin/ruby -Ks
  02: require 'socket'
  03: require './lib/smtpsession'
  04:
  05: server = TCPServer::new("192.168.1.11",10040)
  06: loop do
  07:   puts "wait for connect."
  08:   sock = server.accept
  09:   puts "connect."
  10:   Thread.new do
  11:     smtp_session = SmtpSession::new(sock, 'myhostname')
  12:     puts "smtp_session.subject = #{smtp_session.subject}"
  13:     puts "smtp_session.to = #{smtp_session.header['TO'].inspect}"
  14:
  15:     sock.close
  16:   end
  17: end
  

これで、宛先と件名の内容から指示内容をXPagesアプリから受け取ることができる。 後はその指示内容に応じて、NotesClientを制御するようなコードを書けばいい。

コマンド解析機能

ただ、スレッドの中で直接NotesClientを制御すると、制御が並列で行われてしまうのでよろしくない。

そのため、キューに指示を書き込んでおき、メインスレッドで指示を取り出しながら順に処理していけばいい。

汎用性を持たせるなら、キューに単純に指示をいれておくのではなく、実行時刻もセットしておき、実行時刻を過ぎたものから実施していく方法をとれば、夜中に実行させたいバッチ処理も記述できる。

このあたりを実装するとこんな感じになる。一部のメソッドは省略してるので、このままでは動かない。

  01: MY_IPADD = "192.168.1.5"
  02: MY_PORT = 10040
  03: server = TCPServer::new(MY_IPADD, MY_PORT)
  04: @action_queue = Queue.new
  05:
  06: Thread.new do
  07:   loop do
  08:     puts "wait for connect."
  09:     sock = @server.accept
  10:     puts "connect."
  11:     Thread.new do
  12:       smtp_session = SmtpSession::new(sock, 'myhostname')
  13:       puts "smtp_session.subject = #{smtp_session.subject}"
  14:       puts "smtp_session.to = #{smtp_session.header['TO'].inspect}"
  15:       sock.close
  16:       dest_mailadd = smtp_session.header['TO'].downcase
  17:       if (dest_mailadd == "subsystem@tech-notes.org")
  18:         req, params = analize_request(smtp_session.subject)
  19:         puts "[req, params] = #{[req, params].inspect}"
  20:         action = Action.new(req, params)
  21:         @action_queue.push action
  22:       end
  23:     end
  24:   end
  25: end
  

コマンドの実行部

メールコマンドを受信するスレッドとは別スレッドで、@action_queueにActionオブジェクトが積まれたら、それを実行する処理を実装する。

  01: loop do
  02:   action = @action_queue.pop
  03:   params = action.params
  04:   db = @db_list[params['db_name']]
  05:   if db == nil
  06:     puts "#{params['db_name']} is not deined DB."
  07:     next
  08:   end
  09:   case action.req
  10:   when "CHECK_ATTFILES"
  11:     db.chec_attfiles()
  12:   when "CREATE_DOCID"
  13:     db.create_docid()
  14:   when "UPDATE_INDEX"
  15:     db.update_index()
  16:   else
  17:     puts "Undefined req:#{action.req}"
  18:   end
  19: end
  

DBクラスの用意

先ほどメソッドを呼び出してたDatabaseをあらわすクラスだけど、こんな感じで定義するといい。

さて、実例。これはPC管理台帳のXPagesアプリをあらわしているクラス。

  01: class DB_pcBook
  02:   include DBCommon
  03:   ATTFILE_FIELD = "Body"
  04:   VIEW_CHEKATTFILES = "添付チェック待ち"
  05:   ATTFILE_CHECK_FIELD = "添付ファイル要チェック"
  06:
  07:   SERVERNAME = "technotes"
  08:   def initialize(ns, filename)
  09:     @filename = filename
  10:     @db = ns.database(SERVERNAME, filename)
  11:     @view_attfiles = @db.view(VIEW_CHEKATTFILES)
  12:   end
  13:
  14:   attr_reader :filename
  15:
  20:   def update_index()
  21:     puts "update_index() is called."
  22:     if @db.isFTIndexed()
  23:       @db.updateFTIndex(false)
  24:       puts "@db.updateFTIndex() is called."
  25:     end
  26:     puts "update_index() is completed."
  27:   end
  28:
  29: end
  

とまぁ、RubyとNotesgripを使うとここまですっきりと書けるわけだ。 DB_pcBookのオブジェクトを生成するときはこんな感じのコードになる。

  ns = Notesgripp:NotesSession.new
  pcBook = DB_pcBook.new(ns, "pc_book.nsf")
  

こうやってSubSystemにやらせたい処理がでてくるごとにメソッドを追加していけばいい。

XPagesアプリ側

今度はサブシステムに指示を出す側の処理を書いてみよう。

文書保存時に走る処理の中にこんな感じで書くだけでいい。

  01: var db_path = @DbName()[1];
  02: subject = "REQ:UPDATE_INDEX TARGET:" + db_path;
  03: var mail_doc = $hc.createMailDoc(subject, "subsystem@tech-notes.org");
  04: mail_doc.send();
  

大体、send()して5秒後くらいにサブシステムにメールが届く。リアルタイム性はまったくないので注意。

機能の追加

添付ファイルのウイルスチェック

XPagesアプリの添付ファイルのウイルスチェックを行う機能をサブシステムに追加してみよう。 原理はこう。

これを先ほどのDB_pcBookクラスに追加するとこんな感じになる。

  01: class DB_pcBook
  02:   ATTFILE_FIELD = "Body"
  03:   VIEW_CHEKATTFILES = "ファイル要チェック"
  04:   ATTFILE_CHECK_FIELD = "要チェック"
  05:
  06:   SERVERNAME = "technotes"
  07:   def initialize(ns, filename)
  08:     @filename = filename
  09:     @db = ns.database(SERVERNAME, filename)
  10:     @view_attfiles = @db.view(VIEW_CHEKATTFILES)
  11:     @attfile_field = ATTFILE_FIELD
  12:     @attfile_check_field = ATTFILE_CHECK_FIELD
  13:   end
  14:
  15:   attr_reader :filename
  16:
  17:
  18:   EXTRACT_DIR = "./FILES"
  19:   def check_attfiles()
  20:     puts "check_attfiles() is called."
  21:     unless @view_attfiles
  22:       puts "@view_attfilesがない"
  23:       return nil
  24:     end
  25:     @view_attfiles.refresh()
  26:     @view_attfiles.each {|doc|
  27:       puts "  #{doc['管理番号'].text} を処理"
  28:       if ext_allFiles(doc)
  29:         doc[@attfile_check_field].text = ""
  30:         doc.save()
  31:       end
  32:     }
  33:   end
  34:
  35:   private
  36:
  37:   def ext_allFiles(doc)
  38:     puts "ext_allFiles() is called."
  39:     unless File.exist?(EXTRACT_DIR)
  40:       FileUtils.mkdir_p(EXTRACT_DIR)
  41:     end
  42:     ext_filenames = []
  43:     unid = doc.UNID
  44:
  45:     file_field = doc[@attfile_field]
  46:     file_field.each_embeddedFile {|att_file|
  47:       dir_path = File.join(EXTRACT_DIR, unid)
  48:       unless File.exist?(dir_path)
  49:         FileUtils.mkdir_p(dir_path)
  50:       end
  51:       file_path = File.join(dir_path, att_file.name)
  52:       puts "extrace filename = #{file_path}"
  53:       att_file.ExtractFile(file_path)
  54:       ext_filenames.push file_path
  55:     }
  56:     ext_filenames.each {|path|
  57:       return false unless File.exist?(path)
  58:     }
  59:     return true
  60:   end
  61: end