Windows環境でMySQLのテーブルを変更した際に、外部プログラムと通信したい

SQL

環境

Windows 10 64bit

MySQL 8.0.22(途中で8.0.31にアップデート)

 

やりたいこと

MySQLのテーブルに変更(Insert)が加わったとき、外部プログラムと通信したい。

 

構成

・親プロセスを待機、これはIPC(プロセス間通信)の通信を待つプログラム。

・データベース変更を検知⇒MySQLのトリガー発行⇒外部ユーザー定義関数を実行(C++)⇒子プロセスプログラム(C#)を実行し、IPC通信により情報を伝達する

とした。

大きく分けて

①MySQLのLoadableFunction(UDF)

②プロセス間通信(IPC)

③MySQLのトリガーにUDFを登録する

の3要素が関わってきて結構複雑。

 

準備1:Loadble Function(ユーザ定義関数・UDF)の作成方法とテスト

とりあえずは公式のサンプルを動かすところまでやって、それからカスタム関数を作る。

MySQL公式

なお、2021年にUDFからLoadable Functionに改名したそうで、UDFをネット検索すると古い情報しか見つからないのはそのせい。

 

公式に従ってとりあえずはサンプルを動作させてみる。

メイン内容は以下。

udfサンプルを動かすためにはMySQL source distributionが必要なため色々たどる。

2.1.3 How to Get MYSQLの下段( Section 2.9.5, “Installing MySQL Using a Development Source Tree”. )

→2.9.5 Installing MySQL Using a Development Source Treeの下の方のリンク

You can also browse commit history and source code on the GitHub MySQL site.

→mysql-serverのリンク

→sqlフォルダ

→その直下のudf_example.ccとudf_example.defをコピー

・CMakeをインストールする

・CMakeのmakefileを作る。

※公式ではmysqlのインストールディレクトリがwindows標準と異なる表記のため、以下に変更した。

変更しないとビルドができない。

PROJECT(udf_example)

# Path for MySQL include directory
INCLUDE_DIRECTORIES("C:\Program Files\MySQL\MySQL Server 8.0\include")

ADD_DEFINITIONS("-DHAVE_DLOPEN")
ADD_LIBRARY(udf_example MODULE udf_example.cc udf_example.def)
TARGET_LINK_LIBRARIES(udf_example wsock32)

・CMakeを使ってvisual studioのプロジェクトを作る

FRONTL1NE (フロントライン)

オープンソースのライブラリやらを使おうとしたとき、Visual Studioのプロジェクトファイルが見つからないことがあ…

・CMake.txt(作ったもの)をドラッグアンドドロップ

・Configureを押し、Visual Studioのバージョンを指定(ここでは2019を指定)

・Generateを押し生成

・Open ProjectよりVSプロジェクトを開き、リビルドを実行

・作業フォルダにDebugフォルダが生成され、そこにudf_example.dllが生成される。

 

・作った.dllをC:\Program Files\MySQL\MySQL Server 8.0\lib\pluginにコピー

・MySQLコマンドラインクライアントを起動。

・使用例は以下。

関数の読み込み:

create function myfunc_int returns integer soname 'udf_example.dll';

なお、一度CRATEしてDROPしなければPCを再起動してもUDFは有効になっている。

関数の使用:

select myfunc_int(1,1);

関数のdrop:

drop function myfunc_int;

次からdllの中身を入れ替えるが、ロード済みのfunctionはすべてdropしておかないとdllは使用中となって上書きできない。

udfの一覧を取得したいとき:

select * from performance_schema.user_defined_functions;

 

準備2:MySQLクライアントから外部プログラムを実行するUDFの実行

実際に外部プログラムを呼ぶUDFを作成する。

myfuncの中の適当な関数を書き換えて作ったため、余分なインクルード有。(ここではmyfunc_intを参考にした)

なお、C++ど初心者のため、いろいろ突っ込みどころがあると思いますが詳しい方は適宜読み替え願います。

[udf_example.cc]
#include <assert.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <mutex>
#include <new>
#include <regex>
#include <string>
#include <vector>

#include "mysql.h"  // IWYU pragma: keep
#include "mysql/udf_registration_types.h"
#include "windows.h"

#include <iostream>
#include <fstream>
#include <string>


#ifdef _WIN32
/* inet_aton needs winsock library */
#pragma comment(lib, "ws2_32")
#endif

#ifndef _WIN32
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#endif


class TestClass
{
    int num;
public:
    explicit TestClass(int i);

    void printDataConst(const char* s);
    void printData(LPWSTR s);
};

//ここからクラスのメンバ関数の定義
TestClass::TestClass(int i)
{
    num = i;
}

void TestClass::printDataConst(const char* s)
{
    std::ofstream writing_file;
    std::string filename = "c:\\workspace\\sample.txt";
    writing_file.open(filename, std::ios::app);
    std::string writing_text = s;
    writing_file << writing_text << std::endl;
    writing_file.close();
}
void TestClass::printData(LPWSTR s)
{
    std::wofstream writing_file;
    std::wstring filename = L"c:\\workspace\\sample.txt";
    writing_file.open(filename, std::ios::app);
    std::wstring writing_text = s;
    writing_file << writing_text << std::endl;
    writing_file.close();
}

extern "C" bool exec_external_exe_init(UDF_INIT*, UDF_ARGS*, char*) {
    return false;
}

extern "C" long long exec_external_exe(UDF_INIT*, UDF_ARGS * args, unsigned char*,
    unsigned char*) {
    TestClass tc(1);

    PROCESS_INFORMATION p;
    STARTUPINFOW s;
    ZeroMemory(&s, sizeof(s));
    s.cb = sizeof(s);

    WCHAR Y[] = L"C:\\workspace\\VisualStudio\\ConsoleApplication1\\x64\\Debug\\ConsoleApplication1.exe";
    LPWSTR X = Y;

    //メモ帳を実行
    int ret = CreateProcessW(
        NULL, // 実行可能モジュールの名
        X, // コマンドラインの文字列
        NULL, // セキュリティ記述子
        NULL,// セキュリティ記述子
        FALSE, // ハンドルの継承オプション
        NULL, // 作成のフラグ
        NULL,// 新しい環境ブロック
        NULL, // カレントディレクトリの名前
        &s, // スタートアップ情報
        &p // プロセス情報
    );

    if (!ret)
    {
        //外部プログラムの起動に失敗!
        printf("ng");
        tc.printDataConst("失敗");
        printf("CreateProcess failed (%d).\n", GetLastError());
        return -1;
    }
    else
    {
        printf("ok");
        tc.printDataConst("成功");
        CloseHandle(p.hThread);

        //外部プログラムが終了するまで待つ
        WaitForSingleObject(p.hProcess, INFINITE);
        CloseHandle(p.hProcess);
    }
    return 0;
}
[udf_example.def]
LIBRARY		udf_example
VERSION		1.0
EXPORTS
  exec_external_exe_init
  exec_external_exe

 

注記。

・udfの仕様なのか何か分からないがマルチバイトコードの使用がデフォルトで設定されているため、インターネット上のサンプルがそのままで動かない。そのため一部”W”関数の使用を強制するよう変更した。

・MySQLコンソールウィンドウでは標準出力(printfなど)が使用できないため、ファイル書き込みによりデバック出力を確保する必要がある。上記にてクラスを定義しているのはただそれだけのため。

・notepad.exeなど、GUIを含むプログラムは呼ばないほうが無難。呼びだしたプログラムのGUIは表示されない。タスクマネージャーから終了する必要がある。

・ここで呼んでいるC:\\workspace\\VisualStudio\\ConsoleApplication1\\x64\\Debug\\ConsoleApplication1.exeは起動時、ファイルに書き込みをする「だけ」のプログラム。

[ConsoleApplication1.cpp]
#include <iostream>
#include <windows.h>
#include <fstream>
#include <string>

class TestClass
{
	int num;
public:
	explicit TestClass(int i);

	void printDataConst(const char* s);
	void printData(LPWSTR s);
};

//ここからクラスのメンバ関数の定義
TestClass::TestClass(int i)
{
	num = i;
}

void TestClass::printDataConst(const char* s)
{
	std::ofstream writing_file;
	std::string filename = "c:\\workspace\\sample.txt";
	writing_file.open(filename, std::ios::app);
	std::string writing_text = s;
	writing_file << writing_text << std::endl;
	writing_file.close();
}
void TestClass::printData(LPWSTR s)
{
	std::wofstream writing_file;
	std::wstring filename = L"c:\\workspace\\sample.txt";
	writing_file.open(filename, std::ios::app);
	std::wstring writing_text = s;
	writing_file << writing_text << std::endl;
	writing_file.close();
}

int main()
{
	TestClass tc(1);
	tc.printDataConst("ファイル書き込み");
	return 0;
}

上述の通りpluginフォルダに入れ、

 select exec_external_exe();

とすれば起動できる。(引数は不要)

 

参考1:外部アプリケーションの起動(C++)

 

参考2:LPWSTRパラメータと互換性が無い件に対する対処

型 “const wchar_t *” の引数は型 “LPWSTR” のパラメーターと互換性がありませんというエラーが出た対処は以下を参考にした。

teratail[テラテイル]

こんにちは。現在C++/CLIにおいてLPWSTRを以下のように使おうとしたのですが「型 const wchar_t *…

WCHAR Y[] = L"Input";
LPWSTR X = Y;

というようにワンクッション置けばいいらしい。

なお、これはVisual Studioの構成プロパティ⇒文字セット⇒マルチバイト文字セットを使用する

の設定も大きく影響しているそう。とりあえず不満は無いのでそのままとした。

 

準備3:IPC(プロセス間通信)

下記サイトの組み合わせで作る。

C# プロセス間通信(IPC):その下位リンクも必須(反映しないと5分未満で接続が切れる)

C# プロセス間通信 IPC .Netリモーティング…

【C#】Mutexクラスを使ったプログラムの排他制御:Console版のMutexのサンプルコード。

Qiita

はじめに アプリケーションの多重起動を阻止したい場合はMutexクラスを使用すれば良い。 Mutexクラスを使うこ…

async/await についての勉強メモ:C#標準のTask機能はすぐ忘れるのよね…

勉強メモというか参考リンクが全て…

 

以下サンプルとしての完成系コード

cmd.exeから起動用。Serverを起動後Clientを起動する毎にcountが1アップして表示される。

[IPCServer.cs]
using System;
using System.Collections;
using System.Diagnostics;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Threading;
using System.Threading.Tasks;

// System.Runtime.Remotingのアセンブリ参照が必要

namespace IPCServer
{
    public class IpcRemoteObject : MarshalByRefObject
    {
        public int Counter { get; set; }
        /// <summary>
        /// 自動的に切断されるのを回避する 入れないと5分で自動切断される
        /// </summary>
        public override object InitializeLifetimeService()
        {
            return null;
        }
    }
    class IPCServerProgram
    {
        // ミューテックス生成
        private static System.Threading.Mutex _Mutex;
        static void Main(string[] args)
        {
            if (MutexTest())
            {
                Console.WriteLine("True 多重起動です。");
            }
            else
            {
                Console.WriteLine("false 単体起動です。");
                new IpcServer();
            }
        }

        static bool MutexTest()
        {
            //ミューテックスクラスのインスタンス生成
            _Mutex = new System.Threading.Mutex(false, "SubProcess");

            //Mutexの所有権を要求
            if (_Mutex.WaitOne(0, false) == false)
                return true;

            //プロセスを取得
            string AppName = Process.GetCurrentProcess().MainModule.FileName;
            var ps = Process.GetProcessesByName(AppName);

            bool ProcessFlg = false;

            foreach (var item in ps)
            {
                ProcessFlg = true;
                break;
            }

            //起動済ならreturn
            if (ProcessFlg)
                return true;

            return false;
        }

    }
    class IpcServer
    {
        public IpcRemoteObject RemoteObject { get; set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public IpcServer()
        {

            // サーバーチャンネルの生成
            Hashtable properties = new Hashtable();
            properties.Add("portName", "ipcSample");
            properties.Add("authorizedGroup", "Users");
            IpcServerChannel channel = new IpcServerChannel(properties, null);
            //// サーバーチャンネルの生成
            //IpcServerChannel channel = new IpcServerChannel("ipcSample");

            // チャンネルを登録
            ChannelServices.RegisterChannel(channel, true);

            // リモートオブジェクトを生成して公開
            RemoteObject = new IpcRemoteObject();
            RemotingServices.Marshal(RemoteObject, "test", typeof(IpcRemoteObject));
            Console.WriteLine("Main  BEGIN. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
            SubAsync(RemoteObject);
            Console.WriteLine("Main  END.   ThreadId={0}", Thread.CurrentThread.ManagedThreadId);

            Console.ReadLine();
        }
        static async void SubAsync(IpcRemoteObject ipcRemoteObj)
        {
            int oldCount = 0;
            Console.WriteLine("Sub   BEGIN. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
            await Task.Run(async () =>
            {
                while (true)
                {
                    if(ipcRemoteObj.Counter != oldCount)
                    {
                        Console.WriteLine($"Count changed to {ipcRemoteObj.Counter}");
                        oldCount = ipcRemoteObj.Counter;
                    }
                    Console.WriteLine("Delay BEGIN. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
                    await Task.Delay(TimeSpan.FromSeconds(1));
                    Console.WriteLine("Delay END.   ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
                }
            });
            Console.WriteLine("Sub   END.   ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
        }
    }
}
[IPCClient.cs]
using IPCServer;
using System;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;

namespace IPCClient
{
    class IPCClientProgram
    {
        static void Main(string[] args)
        {
            new IpcClient();
        }
    }
    class IpcClient
    {
        public IpcRemoteObject RemoteObject { get; set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public IpcClient()
        {
            // クライアントチャンネルの生成
            IpcClientChannel channel = new IpcClientChannel();

            // チャンネルを登録
            ChannelServices.RegisterChannel(channel, true);

            // リモートオブジェクトを取得
            RemoteObject = Activator.GetObject(typeof(IpcRemoteObject), "ipc://ipcSample/test") as IpcRemoteObject;
            RemoteObject.Counter++;
        }
    }
}

Client側はIpcRemoteObjectの参照が必要な都合上、IPCServerへの参照が必要。

 

準備4:MySQLのトリガーの準備

トリガーはMySQLの標準機能を使用する。簡単な説明は以下。

Let'sプログラミング

MySQL では新しくトリガーを作成するときに CREATE TRIGGER 文を使用します。ここでは MySQL でト…

通常使用の分には上記を参照で困らない。UDFを呼ぶ場合は少しひねる必要があり、

delimiter $$
create trigger ins_external after insert on server_external_process_control for each row begin
set @aa = exec_external_exe();
end;
$$
delimiter ;

のようにdelimiterの変更処理を挟む必要がある。

(たぶんUDFの返り値のせい。メモ帳で書いてコピペでも動作する)

このとき、exec_external_exe()がユーザー定義関数。

参考:

にょきにょきブログ

概要 MySQL の関数は自前で定義出来る。この記事では、MySQL からシェルコマンドを叩けるようにする関数を作成する…

 

実行

cmd.exeでIPCServer.exeを実行、待機しておく。

UDF exec_external_exeから呼ぶ関数はIPCClientとしておく。.

MySQLの対象テーブル(上記ではserver_access)を変更(update)し、IPCServer.exeに反応があることを確認する。

Count changed to 〇の部分が反応。プログラム起動回数に応じカウント変更が確認できた。

 

その他

エラー関係

上記プログラムを作る間にいくつかエラーに遭遇した。

・UDF実行するとMySQLが強制的にダウンするんだけど? / Lost Connectionになる?

⇒VisualStudioのプロパティやビルドオプションなどを適当に直したらいつの間にか解消していた。原因は何だったのか…(MySQLのバージョンも上げたが、バージョンアップだけでは解決しなかった)

なお、エラーログは

C:\ProgramData\MySQL\MySQL Server 8.0\Data\(ユーザー名).err

にある。しかし、この内容からエラー対処はできなかった。

・create function する際に、ERROR 1046 No database selectedと表示される?

⇒コマンド引数の綴りを確認。returnsがreturnになってないか、など。

 

外部関数を呼ぶ一般的な方法

lib_mysqludf_sys.dllという、公開されているライブラリがあり、そこからsys_exec(引数)という関数を読み込めば外部プログラムの実行はできるらしい

…が、以下理由で断念した。

・Windows向けの情報が無く、元のコードからビルドすることができなかった。

・dllとして配布があるものの、中身が不明(ウイルス等の危険がある)

自分でビルドできて問題にも対処できるのならば近道になる可能性はある。

一応下記にdllの情報は書いておく。

インターネット上からダウンロードでき、実際に取り込むことができたdll

GitHub

読んでいくと、もともとのコードに修正すべき点がいくつかありなおす必要があるそう。

 

MySQLのバージョンアップ後、C#プログラムからデータベースにアクセスできない

原因は各プログラムが参照しているMySql.Dataが古いこと。

このdllはC:\Program Files (x86)\MySQL\Connector NET 8.0\Assemblies\v4.5.2\MySql.Data.dllにあり(v4.5.2はプログラムのバージョンによる。当記事は4.7.2で作成しているので下側で4.5.2を選択)、このdll参照を更新することで解決した。

方法はVisual Studioを開き、MySql.Dataの参照を削除、新しいMySql.Dataを参照しなおし、全プログラムを個々にリビルドし直し解決した。

 

 

 

下記は参考

・トリガーを削除する

Let'sプログラミング

作成済みのトリガーを削除するには DROP TRIGGER 文を使用します。ここでは MySQL でトリガーを削除する方…

・TEXT()