Python のプログラムから mysqld を run, shutdown してみた (Windows 10)

# Python のプログラムから mysqld を run, shutdown してみた (Windows 10)
## 環境
Windows 10 Pro
mysqld 10.4.27-MariaDB (XAMPP)
Python 3.8.5 (Anaconda)
## 要約
mysqld は XAMPP でインストールしたものを使う。mysqld.exe は C:\xampp\mysql\bin にあるとして、mysqld を実行・終了する bat ファイルを作り、これを Python から呼び出す。

起動は次のようなバッチファイルを作る。
```runmysql.bat
cd \xampp\mysql
bin\mysqld
```
終了は次のバッチファイルを作る。
```shutdownmysql.bat
cd \xampp\mysql
bin\mysqladmin -u root -Pxxxxxxxx shutdown
```
これを Python から次のように呼び出す。

```
import subprocess
import time

～

    def run_mysqld(self):
        pobj = subprocess.Popen(['runmysqld.bat'])
        time.sleep(1)
        return pobj.returncode

    def shutdown_mysqld(self):
        subprocess.run(['shutdownmysqld.bat'], shell=True)
```
## 背景
Windows pc で XAMPP の mysql を使っているのだが、夜中に windows update みたいなナニが勝手に再起動して、テーブルとか大破壊してくれる事故が多発していた。ていうか今もしている(笑)。

その都度修復するのは大変なので、XAMPP から mysqld を起動した後、作業が終わったらできるだけ速やかに stop するように心がけているのだが、夜中に処理したいこともあったりするし、そもそも忘れるので解決になっていないし、そういう時になぜか勝手に Windows が再起動しているから訳がわからない。

もしかして Windows の service で登録しておけば済む話なのかもしれないが、まあそれはおいといて、

ということで、XAMPP から mysqld を起動するのではなく、python の処理コードを使うときに、その都度、実行直前に mysqld を起動して、処理が終わったら mysqld を shutdown する。こうすれば mysqld が起動している途中に Windows が落ちることはないのでは、と考えた。

今回のポイントは、mysqld.exe を実行してバックグラウンドで実行させる方法。つまり、C言語の fork のような処理を Python でどうすればいいか、という話。

## Solution
いろいろ真面目に考えるのが面倒なので、チートじゃないけど、こんなプランでいくことにする。

mysqld の起動は、コマンドプロンプトからこのように実行すればいい。

```
cd \xampp\mysql
bin\mysqld
```

終了は、このようにする。

```
cd \xampp\mysql
bin\mysqladmin -u root -Pxxxxxxxx shutdown
```

それで、この2つを bat ファイルにしておいて、python からは subprocess.Popen を使ってコレを呼び出すことにする。バッチファイルに直接パスワードはあまり書きたくないのだが、local pc だけで使っているからまあいいか、みたいな。

この処理を埋め込みたいクラスに、次のような関数を定義する。

```
import subprocess
import time

～

    def run_mysqld(self):
        pobj = subprocess.Popen(['runmysqld.bat'])
        time.sleep(1)
        return pobj.returncode
```

Popenは瞬時に戻ってくるので、time.sleep(1) で気持ち待ってあげる。mysqld の起動にそれ以上かかったらどうなるか分からないが、こだわらない。

shutdown はこんな感じ。

```
        subprocess.run(['shutdownmysqld.bat'], shell=True)
```

shutdown はすぐに戻ってくる必要はないので、run で呼び出して、終わるまで待つ。

説明は面倒なのでいきなりだが、これをテストするクラスのテストケースがこんな感じだったりする。

```
    def test_search(self):
        db_object = Sihi2DB()
        
        db_object.run_mysqld()
        
        search = 'Enemy'

        query = f"SELECT `url`,`artist`,`mtitle`,`created` FROM mdb WHERE artist like '%{search}%' OR `mtitle` like '%{search}%' order by created"
        result = db_object.fetch_all(query)
        for row in result:
            print(row)
        
        db_object.db_close()
        
        db_object.shutdown_mysqld()
```

db_object を作っている Sihi2DB というクラスは mysql を使ってアレコレするための自作クラスで、そこに先のバッチファイルを呼び出す関数を追加した。run して fetch して close して shutdown する流れになっている。ちなみにこのコードは mdb という音楽データが入ったテーブルから Enemy という文字列に関係しそうなレコードを SELECT する感じなので、Arch Enemy の曲データが拾えたらok、みたいな。曲のタイトルに Enemy も入っていたら拾ってくるので余計なデータも出てきそうだけど。

ちなみに、実際にテストしたらこんな感じの結果になった。

```
('ebq', 'Arch Enemy', 'Nemesis', datetime.datetime(2014, 11, 26, 3, 22, 28))
('f1h', 'Arch Enemy', 'Eureka', datetime.datetime(2015, 1, 17, 20, 54, 15))
('f1i', 'Arch Enemy', 'War Eternal', datetime.datetime(2015, 1, 18, 12, 29, 56))
('f3p', 'Arch Enemy', 'Transmigration Macabre', datetime.datetime(2015, 3, 25, 13, 20, 39))
('g5i', 'Dream Theater', 'The Enemy Inside', datetime.datetime(2016, 5, 18, 3, 27, 54))
('k9h', 'Rush', 'The Enemy Within', datetime.datetime(2020, 9, 17, 20, 45, 37))
('mca', 'Arch Enemy', 'The Watcher', datetime.datetime(2022, 12, 10, 21, 5, 27))
('mcl', 'Arch Enemy', 'Sunset Over The Empire', datetime.datetime(2022, 12, 21, 17, 24, 9))
```

ん、open しなくていいのか?

という疑問が出た人は鋭い。実は、fetch を呼び出したときにまだ open していない時は勝手に open するようなコードになっている。

```
    def db_open(self):
        if self.cnx == None:
            config = self.get_config()
            self.cnx = mysql.connector.connect(**config)
            self.cursor = self.cnx.cursor(buffered=True)

～

    def fetch_all(self, query, params=None):
        self.db_open()  # 既に open していれば何もしないので、念のため呼んでおく
        self.cursor.execute(query, params)
        records = self.cursor.fetchall()
        return records

```
こういうズボラなコードはあまり、いや、全く推奨しないが、何でこういう構造にしたのか覚えてないのだ。こういうことをするのなら、run する時にも既に run していたら何もしない構造にするとか、その種の処理も追加して統一感を出せばいいような気がしてくるが…。

runしているかどうかも、何か実行してみて connect できなければとりあえず run してみるとか。

## 余談
### 出力
stdout, stderr の処理をしないと何かよからぬことが起こりそうな気がするが、こだわらない。必要になったら何か考える。

### port 指定

この処理は他に mysql を使う処理がないことを前提にしている。例えば Redmine が mysql を使っている時に勝手に shutdown してしまうと大変なことになる。

大変というほどでもないか。

複数の処理が mysqld を使うのなら、port を別に割り当てて、処理ごとに deamon process (?) を起動しておくべきだ。例えば、port を 13306 に割り当てて mysqld を起動したければ、次のような bat を作っておく。

```runmysql_13306.bat
cd \xampp\mysql
bin\mysqld --port=13306
```

shutdown の bat の port も、これに合わせておく。

```shutdownmysql_13306.bat
cd \xampp\mysql
bin\mysqladmin -u root -Pxxxxxxxx --port=13306 shutdown
```

もちろん、mysqld を分けるのなら data ディレクトリも分けないといけない。他の daemon が同じ data を勝手にアクセスしたらかなりややこしい話になりそう。

### start

Windows には start というコマンドがある。バッチファイルの中身を次のようにすれば、mysqld を起動してすぐに戻ってくる。

```runmysql_bg.bat
cd \xampp\mysql
start /b bin\mysqld
```

このようなバッチファイルを次のように Python から subprocess.run で呼び出せば、瞬時に戻ってくるので、run で待たされることはない。

```
subprocess.run(['runmysqld.bat'], shell=True)
```

これと、Popen を使った処理と、どちらがいいのかはよく分からない。