mongoシェルの機能拡張(ソース改造によるログ機能追加)

# 概要
MongoDBのクライアントであるmongoシェルの機能拡張をソースを改造して行います。拡張内容はmongoシェルの入力および出力をファイルにも出力するログ機能を追加します。
拡張するJavaScriptのNative関数は次の通りです。

- startLog("ファイル名")
- endLog()

# 開発環境
- Windows 10
- Visual Studio 2017 Community版
- Python3.7
- mongodb ソース 4.2.0-rc2
- git

mongodbのバージョンを4.2にしたのはVisual Studioがもともと2017版をインストールしていたためです。 バージョンごとに必要なソフトのバージョンは「ソースからのビルドの方法」が記載されている次のURLを参照してください。

- https://github.com/mongodb/mongo/wiki/Build-Mongodb-From-Source

ソース改造内容はLinuxなどでも有効のはずです。 

## 準備

上記各ソフトとmongodbソースをダウンロード・インストールします。

メニューから`VS 2017 用 x64 Native Tools コマンド プロンプト`クリックしてVSの環境を設定して`Python3.7`,`git`も使えるようにします。

次にビルドに必要なPythonパッケージをインストールします。

```
git clone https://github.com/mongodb/mongo
cd mongo
git checkout r4.2.0-rc2
pip install -r etc\pip\dev-requirements.txt
```

ソース改造前にビルドが成功するかビルドします。40分位かかりました。

```
python buildscripts\scons.py --ssl mongo.exe
```

`MONGO_VERSION`が要求されるときがあります。

```
python buildscripts\scons.py --ssl MONGO_VERSION=4.2.0-rc2 mongo.exe
```

# ソースの改造

改造するソースは次のソースです

- src/mongo/shell/shell_utils_extended.cpp
    - Native関数が定義されている
- src/mongo/shell/dbshell.cpp
    - mongo.exeのmainです
- src/mongo/logger/console.cpp
    - Windowsでは改造が必要ですが、それ以外では不要だと思います



既存のソースの改造量を少なくするために、次のソースを追加します。

- src/mongo/shell/mylog.h



## src/mongo/shell/mylog.hの作成

```cpp:src/mongo/shell/mylog.h
#pragma once

#include <fstream>
#include <iostream>
#include <istream>
#include <sstream>


class MyLogger : public std::stringbuf {
public:
    MyLogger() : std::stringbuf() {
        logfile = NULL;
        prevCout = NULL;
    }

    ~MyLogger() {
        end();
    }

    void end() {
        if (prevCout != NULL)
           std::cout.rdbuf(prevCout);
        if (logfile != NULL) {
            logfile->close();
            delete logfile;
        } 
        logfile = NULL;
        prevCout = NULL;
    }
    bool open(const char *name) {
        if (logfile != NULL)
            return false;
        lnLast = true;
        logfile = new std::ofstream(name, std::ios_base::app);
        prevCout = std::cout.rdbuf(this);
        return true;
    }
    void write(const char* str) {
        if (logfile != NULL) {
            *logfile << str;
            checkln(str);
        }
    }
    void writeInput(std::string &code) {
        std::stringstream ss{code};
        std::string buf;
        while(std::getline(ss, buf, '\n')) {
            write("# ");
            write(buf.c_str());
            write("\n");
        }
    }
    void outputEnd() {
        if (lnLast)
            return;
        write("\n");
        printf("\n");
    }
private:
    void checkln(const char* str) {
        int len = strlen(str);
        if (str[len-1] == '\n') 
            lnLast = true;
        else
            lnLast = false;   
    }
private:
    std::ofstream* logfile;
    std::streambuf* prevCout;
    bool  lnLast;
protected:
	std::streamsize xsputn(const char_type* str, std::streamsize count) override {
        write(str);
        printf("%s", str);
        checkln(str);
		return count;
	}
};


extern MyLogger *mylog;

```

エラー処理などはしていません。

`std::cout`を使ってコンソール出力をしているものを`class MyLogger`に変更して`xsputn`メソッドでコンソール出力すると同時にログファイルにも出力します。　
次のサイトの「標準出力の出力先をすげかえる」を参考にしました。
https://blog.emattsan.org/entry/20120613/1339584605

- open
    - ログファイルをappendモードでオープンします。それと同時に`std::cout`を使った出力を自身に変更します。
- end
    - ログファイルへの出力を終了します。さらに、`std::cout`の出力先の元に戻します。
- write
    - ログファイルの書き込みをします。
- wtiteInput
    - コンソールから入力されたコマンドをログファイルに書き込みます。
    - 入力されたコマンドの各行の先頭には`# `を付けてコンソールへの出力と区別するようにします。
    - コマンドを行単位にSplitするのは次のサイトを参考にしました
        - http://www.hakase73.com/entry/2017/05/18/233519
- outputEnd
    - コマンドの出力が終わったことを示す。最後の出力が改行で終わっていないときは改行を追加する。
- checkln
    - 出力の最後の文字が改行かどうかをチェックする。
- xsputn
    - `std::cout`に書き込まれると、このメソッドが呼び出されます



## src/mongo/shell/shell_utils_extended.cppの改造

```diff:src/mongo/shell/shell_utils_extended.cpp
diff --git a/src/mongo/shell/shell_utils_extended.cpp b/src/mongo/shell/shell_utils_extended.cpp
index 2db4d7713a..16fe156695 100644
--- a/src/mongo/shell/shell_utils_extended.cpp
+++ b/src/mongo/shell/shell_utils_extended.cpp
@@ -51,6 +51,8 @@
 #include "mongo/util/str.h"
 #include "mongo/util/text.h"

+#include "mongo/shell/mylog.h"
+
 namespace mongo {

 using std::ifstream;
@@ -372,6 +374,21 @@ BSONObj getFileMode(const BSONObj& a, void* data) {
     return BSON("" << fileStatus.permissions());
 }

+BSONObj startLog(const BSONObj& a, void* data) {
+    uassert(10000, "startLog() takes one argument, the path to a file",
+            a.nFields() == 1 && a.firstElementType() == String);
+    uassert(10001, "MyLogger instance is null", mylog != NULL);
+    uassert(10002, "logger is already started",
+       mylog->open(a.firstElement().str().c_str() ));
+    return undefinedReturn;
+}
+
+BSONObj endLog(const BSONObj& a, void* data) {
+    uassert(10003, "MyLogger instance is null", mylog != NULL);
+    mylog->end();
+    return undefinedReturn;
+}
+
 void installShellUtilsExtended(Scope& scope) {
     scope.injectNative("getHostName", getHostName);
     scope.injectNative("removeFile", removeFile);
@@ -388,6 +405,8 @@ void installShellUtilsExtended(Scope& scope) {
     scope.injectNative("passwordPrompt", passwordPrompt);
     scope.injectNative("umask", changeUmask);
     scope.injectNative("getFileMode", getFileMode);
+    scope.injectNative("startLog", startLog);
+    scope.injectNative("endLog", endLog);
 }
 }
 }
```


`scope.injectNative`でNative関数を追加します。
追加した`startLog`,`endLog`の第一引数はJavaScriptで呼び出すときの引数のコレクションです。
第二引数は`scope.injectNative`の第三引数に指定したものが渡されます。しかし、今回は指定していないので使いません。
`uassert`の第一引数の数値は適当にしましたが、重複などするとエラーになります。第三引数が**false**のとき第二引数のメッセージがコンソールに出力され以後の処理は実行されません。

## src/mongo/shell/dbshell.cppの改造

```diff
diff --git a/src/mongo/shell/dbshell.cpp b/src/mongo/shell/dbshell.cpp
index f7ff703b66..46f47e7e21 100644
--- a/src/mongo/shell/dbshell.cpp
+++ b/src/mongo/shell/dbshell.cpp
@@ -83,6 +83,10 @@
 #include <unistd.h>
 #endif

+#include "mongo/shell/mylog.h"
+
+MyLogger *mylog;
+
 using namespace std::literals::string_literals;
 using namespace mongo;

@@ -1068,6 +1072,9 @@ int _main(int argc, char* argv[], char** envp) {
         std::string prompt;
         int promptType;

+        MyLogger log;
+        mylog = &log;
+
         while (1) {
             inMultiLine = false;
             gotInterrupted = false;
@@ -1141,6 +1148,8 @@ int _main(int argc, char* argv[], char** envp) {
                 break;
             }

+            log.writeInput(code);
+
             bool wascmd = false;
             {
                 std::string cmd = linePtr;
@@ -1186,7 +1195,7 @@ int _main(int argc, char* argv[], char** envp) {
                     lastLineSuccessful = false;
                 }
             }
-
+            log.outputEnd();
             shellHistoryAdd(code.c_str());
             free(line);
         }
```

`MyLogger log;`はローカル変数で、そのポインターをグローバル変数に代入しているので、本来はこのようにしない方良いと思いますが、改造が簡単になるためにこのようにしました。

## src/mongo/logger/console.cppの改造

Windows以外は~~`std::out`~~ `std::cout`を使っているので改造の必要はないです。
Windowsでは**utf-8**の文字コードをユニコードに変換して出力するためにWindows APIの`MultiByteToWideChar`と`WriteConsoleW`が使われています。さらに、**ascii**文字のみの出力には`std::cout`が使われているようです。

```diff:src/mongo/logger/console.cpp
diff --git a/src/mongo/logger/console.cpp b/src/mongo/logger/console.cpp
index ce8c053779..9114f8c647 100644
--- a/src/mongo/logger/console.cpp
+++ b/src/mongo/logger/console.cpp
@@ -38,6 +38,7 @@

 #ifdef _WIN32
 #include <io.h>
+#include "mongo/shell/mylog.h"
 #endif

 namespace mongo {
@@ -181,7 +182,10 @@ private:
             unwrittenCount -= written;
             unwrittenBegin += written;
         }
-
+        if (mylog != NULL) {
+            _buffer[n] = '\0';
+            mylog->write(_buffer);
+        }
 #ifdef MONGO_CONFIG_DEBUG_BUILD
         if (IsDebuggerPresent()) {
```

`_buffer`にある出力**utf-8**文字をログファイルに出力します。

# 実行例

```
mongo
> startLog("mongo.log")
> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
> db.col.insert({a:1, b:"あいうえお"})
WriteResult({ "nInserted" : 1 })
> db.col.find()
{ "_id" : ObjectId("5d1603de543037aaf9d9b216"), "a" : 1, "b" : "あいうえお" }
> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
test    0.000GB
> show collections
col
> for(var i=0;i < 5; i++) {
... print(i)
... }
0
1
2
3
4
> exit
bye
```

`WriteResult({ "nInserted" : 1 })`は改行で終わっていません。さらに`std::out`で出力されています。

`mongo.log`の中身

```text:mongo.log
# show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
# db.col.insert({a:1, b:"あいうえお"})
WriteResult({ "nInserted" : 1 })
# db.col.find()
{ "_id" : ObjectId("5d1603de543037aaf9d9b216"), "a" : 1, "b" : "あいうえお" }
# show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
test    0.000GB
# show collections
col
# for(var i=0;i < 5; i++) {
# print(i)
# }
0
1
2
3
4
bye
```

最後の`exit`が出力されていないのは`log.writeInput(code);`の以前で終了するためです。

# まとめ

数年ぶりにC++のソースを書きました、随分忘れているみたいです。
また、膨大なC++のソースを解析するのはなかなか大変です。今回は簡単に出来るものだけ追加してみました。すべての出力がログに出力されるかどうかは分かりませんが、これだけでも十分有用だと思います。
新規のNative関数は`scope.injectNative`で関数を追加して、複雑な処理は他の言語などで`dll`として作成したほうが開発し易いかも良いかもしれません。