KnowHow

技術的なメモを中心にまとめます。
検索にて調べることができます。

[改良版7]asyncioを用いてサーバ側のコマンド結果をクライアントで取得するコード。

登録日 :2024/10/03 04:33
カテゴリ :Python基礎

client.pyとsumit.shを見直し改修しています。
verboseオプションを追加し、インフォメーションを表示するかどうかを選択できるように改修しました。また、client.py, tools, configはライブラリとして一つのフォルダにまとめる構成とします。

以下のようなファイル構成です。

server側

server.py
config/settings.py (設定ファイル)
log/server.log(ログファイル)

client側

submit.py (client.pyを実行するラッパースクリプト
checklog_lib
 ->__init__.py
 ->client.py(サーバと通信するメインプログラム)
 ->config/settings.py (設定ファイル)
 ->tools/utils_slurm.py(自作のライブラリ関数)

config/settings.py

import os
import sys

dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(dir_path)


"""
Share settings
"""
#server.pyを実行するサーバのIPアドレスは適切に変更する。以下はlocalhostの設定。
#ポート番号も、既存サービスと重複しないようなものを選定する。
SERVER_IP = '127.0.0.1'
SERVER_PORT = 8888

"""
For Server settings
"""
LOG_FILE = dir_path + '/log/server.log'
LIC_FILE = dir_path + '/log/server.log'
BASE_COMMAND = 'cat ' + LIC_FILE + ' | grep '
TIMEOUT = 10
STR_LEN_LIMIT = 10

server.py

サーバ側で起動しておくプログラム。APIとして、client.pyからの応答を受け取り結果を返答する。
インタプリタ(シバン)#!/usr/bin/python3 も書いておく。

#!/usr/bin/python3

import asyncio
import asyncio.subprocess
import shlex
import logging
import sys
import os
import datetime
import gc
import re

dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(dir_path)

from config import settings

#logging.basicConfig(filename=settings.LOG_FILE, level=logging.DEBUG)
logging.basicConfig(
        filename=settings.LOG_FILE,
        level=logging.INFO,
        format='%(asctime)s:%(name)s:%(levelname)s:%(message)s')
logger = logging.getLogger(__name__)
logger.debug({'add path': dir_path})

"""
Created by Me.
email:
ver: 1.0
date: 2024.10.01
"""


class FetchLogAPIServer(object):
    def __init__(self, _base_command, _strlen, _hostname_strlen, _timeout):
        self.lock = asyncio.Lock()
        self.base_command = _base_command
        self.strlen = _strlen
        self.timeout = _timeout
        self._hostname_len = _hostname_strlen

    @staticmethod
    def is_valid_string(text):
        pattern = r'^[a-zA-Z0-9-]+$'
        if re.match(pattern, text):
            return True
        else:
            return False

    async def run_command(self, reader, writer):
        dt_now = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
        try:
            data = await reader.read()
            client_addr = writer.get_extra_info('peername')
            client_ip = client_addr[0]
            client_port = client_addr[1]
            logger.debug({'time': dt_now, 'client_ip': client_ip, 'client_port': client_port})
            rev_msg = data.decode()
            logger.debug(rev_msg)
            name, num, hostname = rev_msg.split(',')
            print({
                'time': dt_now,
                'client_ip': client_ip,
                'name': name,
                'num': num,
                'hostname': hostname})
            logger.debug({
                'input name': name,
                'input num': num,
                'hostname': hostname})

            if not name.isalnum() or len(name) > int(self.strlen):
                logger.error({
                    'status': 'failed',
                    'client ip': client_ip,
                    '[ERROR]Bad name': name})
                raise ValueError(f"{name} is an Invalid or Too long input")

            if not num.isdigit():
                logger.error({
                    'status': 'failed',
                    'client ip': client_ip,
                    '[ERROR]Bad num': num})
                raise ValueError(f"{num} is not number.")

            if not self.is_valid_string(hostname) or len(hostname) > int(self._hostname_len):
                logger.error({
                    'status': 'failed',
                    'client ip': client_ip,
                    '[ERROR]Bad hostname': hostname})
                raise ValueError(f"{hostname} is an Invalid or Too long input.")

            full_command = self.base_command + shlex.quote(name)
            if hostname != 'NAN':
                full_command = full_command + ' | grep ' + shlex.quote(hostname)
            full_command = full_command + ' | tail -n ' + shlex.quote(num)
            logger.info({"Executing command": full_command})

            async with self.lock:
                proc = await asyncio.create_subprocess_exec(
                    'sh', '-c', full_command,
                    stdout=asyncio.subprocess.PIPE,
                    stderr=asyncio.subprocess.PIPE)
                try:
                    stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=self.timeout)
                    res = str(stdout.decode())
                    exitcode = await proc.wait()
                    if proc.returncode != 0:
                        error_message = f"Command failed with exit code {exitcode}: {stderr.decode()}"
                        logger.error({
                            'status': 'failed',
                            'client ip': client_ip,
                            '[ERROR]Command failed': full_command})
                        writer.write(error_message.encode())
                    else:
                        writer.write(res.encode())
                        logger.info({
                            'status': 'success',
                            'client_ip': client_ip,
                            'name': name,
                            'num': num,
                            'hostname': hostname})
                except asyncio.TimeoutError:
                    proc.kill()
                    logger.error({
                        'status': 'failed',
                        'client ip': client_ip,
                        "[ERROR]Command time out": full_command})
                    writer.write("Command timed out".encode())
        except Exception as e:
            logger.error({
                'status': 'failed',
                "[ERROR]Catch Exception": str(e)})
            writer.write(f"Error: {str(e)}".encode())
        finally:
            await writer.drain()
            writer.close()


if __name__ == '__main__':
    ip = settings.SERVER_IP
    port = settings.SERVER_PORT
    base_command = settings.BASE_COMMAND
    timeout = settings.TIMEOUT
    strlen = settings.STR_LEN_LIMIT
    hostname_strlen = 20

    loop = asyncio.get_event_loop()
    counter_sever = FetchLogAPIServer(
            base_command, strlen, hostname_strlen, timeout)
    coro = asyncio.start_server(counter_sever.run_command,
                                ip, port, loop=loop)

    server = loop.run_until_complete(coro)
    print('server {}'.format(server.sockets[0].getsockname()))
    try:
        loop.run_forever()
    except KeyboardInterrupt:
        pass

    server.close()
    loop.run_until_complete(server.wait_closed())
    loop.close()

    gc.collect()

client.py

server.pyと通信をする。server.pyに信号を送り、server.pyから結果を取得するプログラム。

#!/usr/bin/python3

import os
import sys
import asyncio
import gc
import datetime
import logging
import json
from optparse import OptionParser

dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(dir_path)

from config import settings
from tools import utils_slurm

logging.basicConfig(
        stream=sys.stdout,
        level=logging.INFO,
        format='%(asctime)s:%(name)s:%(levelname)s:%(message)s')
logger = logging.getLogger(__name__)

"""
Created by Me.
email:
ver: 1.0
date: 2024.10.03
"""


class AwaitableClass(object):
    def __init__(self, arg, _loop):
        self.name = str(arg[0])
        self.num = str(arg[1])
        self.hostname = str(arg[2])
        self._send_msg = send_msg = self.name + "," + self.num + "," + self.hostname
        self.loop = _loop
        self._ip = settings.SERVER_IP
        self._port = settings.SERVER_PORT

    def __await__(self):
        async def request_server():
            try:
                reader, writer = await asyncio.open_connection(
                    self._ip, self._port)
                writer.write(self._send_msg.encode())
                writer.write_eof()
                data = await reader.read()
                data = data.decode()
                return data
            except Exception as e:
                _messages = "The server-side service may be down, so please contact support."
                logger.error({"ERROR:__AwaitableClass__": _messages})
        return request_server().__await__()


async def main(name, _loop):
    print('='*100)
    print('---> check ansys log, please wait...\n')
    result = await AwaitableClass(name, _loop)
    print(result)
    print('---> finish')


def fetch_input_data():
    usage = 'usage: %prog -u <username> -n <number> -j <Slurm job ID> -v'
    parser = OptionParser(usage=usage)

    parser.add_option('-u', '--user', action='store', type='string',
                      dest='name', help='user name')
    parser.add_option('-n', '--num', action='store', type='int',
                      dest='num', help='tail number')
    parser.add_option('-j', '--jobid', action='store', type='int',
                      dest='jobid', help='slurm job id')
    parser.add_option('-v', '--verbose', action='store_true',
                      dest='verbose', default=False,
                      help='verbose slurm information')

    options, args = parser.parse_args()

    username = str(options.name)
    nums = int(options.num)
    jobid = int(options.jobid)
    verbose = options.verbose
    if username is None or username == "None":
        raise Exception("user name is required.")

    logger.debug({'username': username})
    logger.debug({'nums': nums})
    logger.debug({'jobID': jobid})
    logger.debug({'verbose': verbose})

    return username, nums, jobid, verbose


if __name__ == '__main__':

    username = None
    nums = None
    jobid = None
    hostname = "NAN"
    slurm_user = None

    dt_now = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
    print(f'check start :{dt_now}')

    try:
        username, nums, jobid, verbose = fetch_input_data()
        logger.debug({
            'usename': username,
            'length': nums,
            'jobid': jobid,
            'verbose': verbose,
            })

        if jobid > 0:
            dataset = utils_slurm.fetch_slurm_job_info(int(jobid))
            hostname = dataset['HOSTNAME']
            slurm_user = dataset['User']
            logger.debug({'Slurm user name': slurm_user})
            if verbose:
                print('='*100)
                print('* slurm job information *')
                print(json.dumps(dataset, indent=2))

        logger.debug({'your account name': username})
        logger.debug({'hostname': hostname})

        if slurm_user is not None and slurm_user != username:
            messages = f" Slurm user name({slurm_user}) is not match your account name({username})."
            messages = messages + " Please check job id."
            raise Exception(messages)

        loop = asyncio.get_event_loop()
        loop.run_until_complete(asyncio.wait([
            main([username, nums, hostname], loop),
        ]))
        loop.close()
    except Exception as e:
        logger.error(str(e))

    gc.collect()

tools/utils_slurm.py

自作のライブラリ関数。client.pyにて読み込む。
(補足)sacctでは、次の日付を指定しないと最新データが取得できないことに注意が必要だった。

#!/usr/bin/python3

import subprocess
from subprocess import PIPE
import logging
import time
import datetime
import os
import sys
import gc
import re
import json

dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(dir_path)

handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')
handler.setFormatter(formatter)

logger = logging.getLogger(__name__)
#logger.setLevel(logging.DEBUG)
logger.setLevel(logging.INFO)
logger.addHandler(handler)


logger.debug({'add path': dir_path})

"""
Created by Me.
email:
ver: 1.0
date: 2024.10.01
"""


class FetchSlurmJobInformation(object):
    def __init__(self, _jobid):
        self._starttime = '2022-01-01'
        self._endtime = self._fetch_tomorrow()
        self._jobid = _jobid
        self._sacct_info = None
        self.sacct_data = dict()

        logger.debug(self._endtime)
        logger.debug(self._jobid)
        logger.debug({"now": self._endtime})

    @staticmethod
    def _fetch_tomorrow():
        today = datetime.datetime.now()
        tomorrow = today + datetime.timedelta(days=1)
        return tomorrow.date()

    def _submit_sacct(self):

        logger.debug(str(self._jobid))
        _command = '\sacct -j ' + str(self._jobid)
        _command = _command + ' --starttime=' + str(self._starttime)
        _command = _command + ' --endtime=' + str(self._endtime)
        _command = _command + ' --format JobID%10,end%16,JobName%20,User%8,NodeList%30,state -X'
        logger.debug({
            'action': "_submit_acct",
            'command': _command})
        saccts = subprocess.run(
                _command,
                shell=True,
                stdout=PIPE,
                universal_newlines=True).stdout.split('\n')

        if len(saccts) < 4:
            logger.error({'action': 'sacct','status': 'failed'})
            raise Exception('There is no sacct data.')

        item_name = []
        for item in saccts[0].split():
            item_name.append(item)

        for idx,item in enumerate(saccts[2].split()):
            #print(idx, item_name[idx], "".join(item.split()))
            logger.debug({
                "action" : "_submit_sacct",
                "idx":idx,
                "item_name":item_name[idx],
                "value": "".join(item.split())})
            self.sacct_data[item_name[idx]] = "".join(item.split())
        logger.debug(self.sacct_data)

    def _pickup_headnode(self):

        nodelist = self.sacct_data['NodeList']
        #test data
        #nodelist = "h11n[001,005-010]"
        #nodelist = "r4s-h17235n[001,005-010]"
        #nodelist = "r4s-h17235n014"
        #nodelist = "h11n001"
        #

        items = nodelist.split('[')
        logger.debug({
            "action":"_picup_headnode",
            "items":items,
            "len(items)":len(items)})

        if len(items)<2:
            temp = nodelist.split(',')
            if len(temp)<2:
                self.sacct_data['HOSTNAME'] = nodelist
            else:
                logger.debug({'action':'pickup_headnode','value': temp})
                self.sacct_data['HOSTNAME'] = temp[0]
        else:
            indexes = re.split(r'[\[,\]-]', items[1])
            self.sacct_data['HOSTNAME'] = items[0] + indexes[0]
        logger.debug(self.sacct_data['HOSTNAME'])

    def run(self):

        self._submit_sacct()
        self._pickup_headnode()

    def __str__(self):
        #return '\n'.join(f'{k}: {v}' for k, v in self.sacct_data.items())
        return json.dumps(self.sacct_data, indent=2)


def fetch_slurm_job_info(jobid):

    result = None
    try:
        logger.debug({"job id": jobid})
        slurm_info = FetchSlurmJobInformation(jobid)
        slurm_info.run()
        #logger.debug(slurm_info)
        result = slurm_info.sacct_data
    except Exception as e:
        logger.error(str(e))

    finally:
        return result


if __name__ == '__main__':
    timeout = 10
    dt_now = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')

    for jobid in range(0, 14):
        print(jobid)
        res = fetch_slurm_job_info(jobid)
        if res is None:
            print('result : No data')
        else:
            print(json.dumps(res, indent=2))

submit.sh

client.pyを実行するためのラッパースクリプト
実行ユーザ名を取得して自動で引数を与えます。ラッパーでも入力引数のチェックをしています。

#!/bin/bash

#
#Created by Me
#date 2024.10.3
#ver 1.0
#

SCRIPT_DIR='/home/app/asyncio_server/client/checklog_lib'
SCRIPT_NAME='client.py'
DEFAULT_NUM=20
DEBUG=false
VERBOSE=false

function echo_usage() {
    echo "Usage  : $0 [-n Length] [-j JobID] [-v]"
    echo "Options:"
    echo "  -h    show this help message and exit"
    echo "  -n    specify showing row number"
    echo "  -j    specify slurm job id"
    echo "  -v    show details of slurm job information"
    echo ""
    echo "Example: $0 -n 5 -j 12345 -v"

}

function echo_message(){
    echo "------------------------------------------------------------------"
    echo "Description:"
    echo " If no SLURM job ID is specified using the -j option,"
    echo " the most recent log will be displayed."
    echo " If a SLURM job ID is specified, the results will be filtered "
    echo " by the hostname used by that particular job."
    echo " By default, ${DEFAULT_NUM} lines are displayed."
    echo " The number of lines displayed can be changed with the -n option."
    echo ""
    echo_usage
    echo "------------------------------------------------------------------"
}

function is_number() {
    local re='^[0-9]+$'
    if [[ $1 =~ $re ]]; then
        return 0  # 数値である
    else
        return 1  # 数値でない
    fi
}


NUM=${DEFAULT_NUM}
JOB_ID=0

while getopts ":n:j:hv" opt; do
  case $opt in
    n)
      NUM="$OPTARG"
      ;;
    j)
      JOB_ID="$OPTARG"
      ;;
    v)
      VERBOSE=true
      ;;
    h)
      echo_message
      exit 1
      ;;
    \?)
      echo "Invalid option: -$OPTARG" >&2
      exit 1
      ;;
    :)
      echo "Option -$OPTARG requires an argument." >&2
      exit 1
      ;;
  esac
done

if is_number $NUM; then
    #echo "$NUM は数値です"
    echo "Length:  $NUM"
else
    echo "[ERROR]: Length = $NUM"
    echo "[ERROR]: '-n' argument  must be a number."
    echo_usage
    exit 1
fi

if is_number $JOB_ID; then
    #echo "$NUM は数値です"
    echo "Job ID:  $JOB_ID"
else
    echo "[ERROR]: Job ID = $JOB_ID"
    echo "[ERROR]: '-j' argument  must be a number."
    echo_usage
    exit 1
fi


if ${VERBOSE}; then
    SCRIPT='python3 '${SCRIPT_DIR}'/'${SCRIPT_NAME}' -u '${USER}' -n '${NUM}' -j '${JOB_ID}' -v'
else
    SCRIPT='python3 '${SCRIPT_DIR}'/'${SCRIPT_NAME}' -u '${USER}' -n '${NUM}' -j '${JOB_ID}
fi

if $DEBUG;
then
  echo ${SCRIPT}
fi

eval ${SCRIPT}

-hオプションにてヘルプから、使用方法を確認することができる。

[root@headnode client]# ./submit.sh -h
------------------------------------------------------------------
Description:
 If no SLURM job ID is specified using the -j option,
 the most recent log will be displayed.
 If a SLURM job ID is specified, the results will be filtered
 by the hostname used by that particular job.
 By default, 20 lines are displayed.
 The number of lines displayed can be changed with the -n option.

Usage  : /usr/bin/mychecklog [-n Length] [-j JobID] [-v]
Options:
  -h    show this help message and exit
  -n    specify showing row number
  -j    specify slurm job id
  -v    show details of slurm job information

Example: /usr/bin/mychecklog -n 5 -j 12345 -v
------------------------------------------------------------------
[root@headnode client]#

プログラムをサービス化する

RockyLinuxサーバで、server.pyをサービス化する方法です。
まずサービスファイルを作成します。
ExecStartで指定するserver.pyのパスは適切に修正してください。
「fetchlogserver.service」

[root@headnode system]# cat fetchlogserver.service
[Unit]
Description=My Python App
After=network.target
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
ExecStart=/usr/bin/python3 /home/app/asyncio_server/server/server.py
Restart=always
User=root
Group=root
RestartSec=1s

[Install]
WantedBy=multi-user.target
[root@headnode system]#

サービスファイルの内容は以下の通り

[Unit] セクション:
  Description: サービスの説明("My Python App")
  After: ネットワークが利用可能になった後に起動
  StartLimitIntervalSec: 再起動の制限時間(5分)
  StartLimitBurst:再起動の試行回数(5回)
[Service] セクション :
  ExecStart: 実行するコマンド(Python3で特定のスクリプトを実行)
  Restart: 常に再起動
  User: rootユーザーとして実行
  Group: rootグループとして実行
  RestartSec: 再起動間隔(1秒)

サービスファイルを、/etc/systemd/systemに配置します。

サービスファイルをリロードして起動し、ステータスを確認します。

systemctl daemon-reload
systemctl start fetchlogserver.service
systemctl status fetchlogserver.service

もしfailedとなった場合は、ログを見て適切に対処しましょう。

journalctl -u fetchlogserver.service

問題なければ、サービスの自動起動も有効にしておきます。

systemctl enable fetchlogserver.service

firewalldが有効の場合は、以下の手順でポートを解放する

firewall-cmd --get-active-zones
sudo firewall-cmd --zone=public --add-port=8888/tcp
sudo firewall-cmd --zone=public --add-port=8888/tcp --permanent
sudo firewall-cmd --reload
sudo firewall-cmd --zone=public --list-ports
[root@headnode temp]# firewall-cmd --get-active-zones
libvirt
  interfaces: virbr0
public
  interfaces: enp0s8
[root@headnode temp]# sudo firewall-cmd --zone=public --add-port=8888/tcp
success
[root@headnode temp]# sudo firewall-cmd --zone=public --add-port=8888/tcp --permanent
success
[root@headnode temp]# sudo firewall-cmd --reload
success
[root@headnode temp]# sudo firewall-cmd --zone=public --list-ports
6817/tcp 6818/tcp 6819/tcp 7817/tcp 8888/tcp
[root@headnode temp]# vi firewalld
[root@headnode temp]#

(補足)
サービスファイルはシンボリックリンクでもよい。
アプリケーションと一緒にサービスファイルを保管しておくことで、メンテナンスや管理がしやすいと思われる。※シンボリックリンクではうまくいかないこともあることが分かった。(SELinuxが有効の場合。この時は直接ファイルを配置する必要がある)
例)

ln -s /home/app/asyncio_server/server/config/service/fetchlogserver.service /etc/systemd/system/fetchlogserver.service
[root@nis1 system]# ll
lrwxrwxrwx. 1 root root   35  8  3 19:36  display-manager.service -> /usr/lib/systemd/system/gdm.service
lrwxrwxrwx. 1 root root   69  9 22 08:37  fetchlogserver.service -> /home/app/asyncio_server/server/config/service/fetchlogserver.service

Appendix 1 クライアントからの実行例

クライアントから、submit.shプログラムを実行すると以下のようになる。
新しいバージョンは、行数とSLURMのジョブIDを指定することが可能。

[root@compute01 client]# mychecklog -n 1 -j 1
Length:  1
Job ID:  1
check start :2024/10/01 04:18:39
====================================================================================================
* slurm job information *
{
  "JobID": "1",
  "End": "2024-03-16T12:19",
  "JobName": "test",
  "User": "root",
  "NodeList": "headnode",
  "State": "COMPLETED",
  "HOSTNAME": "headnode"
}
2024-10-01 04:18:39,635:__main__:INFO:{'Slurm user name': 'root'}
2024-10-01 04:18:39,635:__main__:INFO:{'your account name': 'root'}
2024-10-01 04:18:39,635:__main__:INFO:{'hostname': 'headnode'}
====================================================================================================
---> check ansys log, please wait...

2024-10-01 04:18:39,637:__main__:INFO:{'Executing command': 'cat /home/app/asyncio_server/server/log/server.log | grep root | grep headnode | tail -n 1'}

---> finish
[root@compute01 client]#

サーバ側のログにも記録されている。

[root@headnode server]# cat log/server.log | tail -n 1
2024-10-01 04:18:39,655:__main__:INFO:{'status': 'success', 'client_ip': '192.168.56.112', 'name': 'root', 'num': '1', 'hostname': 'headnode'}
[root@headnode server]#

SLURMの実行ユーザ名と不一致の場合は、以下のように処理を終了する。

[root@compute01 client]# mychecklog -n 1 -j 10
Length:  1
Job ID:  10
check start :2024/10/01 04:20:42
====================================================================================================
* slurm job information *
{
  "JobID": "10",
  "End": "2024-04-07T08:42",
  "JobName": "run_test2",
  "User": "nobuyuki",
  "NodeList": "node[1-2]",
  "State": "COMPLETED",
  "HOSTNAME": "node1"
}
2024-10-01 04:20:42,989:__main__:INFO:{'Slurm user name': 'nobuyuki'}
2024-10-01 04:20:42,989:__main__:INFO:{'your account name': 'root'}
2024-10-01 04:20:42,989:__main__:INFO:{'hostname': 'node1'}
2024-10-01 04:20:42,989:__main__:ERROR: Slurm user name(nobuyuki) is not match your account name(root). Please check job id.
[root@compute01 client]#

簡単に実行できるように、/usr/binなどにシンボリックリンクを作成するとよい。ラッパーのsubmit.shにシンボリックを作成することに注意。

ln -s /home/app/asyncio_server/client/submit.sh /usr/bin/mychecklog

Appendix 2 サーバ側のプログラム変更や、設定ファイルsettings.pyを変更したときの反映方法

プログラムを修正したり、settings.pyの変更を行った場合は、修正内容を反映するためにサービスを再起動する。

[root@nis1 server]# systemctl restart fetchlogserver.service
[root@nis1 server]# systemctl status fetchlogserver.service
 fetchlogserver.service - My Python App
   Loaded: loaded (/etc/systemd/system/fetchlogserver.service; disabled; vendor preset: disabled)
   Active: active (running) since Sat 2024-09-21 09:25:00 JST; 6s ago
 Main PID: 114474 (python3)
    Tasks: 1 (limit: 11004)
   Memory: 9.3M
   CGroup: /system.slice/fetchlogserver.service
           └─114474 /usr/bin/python3 /home/app/asyncio_server/server/server.py

 9 21 09:25:00 nis1 systemd[1]: fetchlogserver.service: Succeeded.
 9 21 09:25:00 nis1 systemd[1]: Stopped My Python App.
 9 21 09:25:00 nis1 systemd[1]: Started My Python App.
[root@nis1 server]# 

Appendix3 起動停止が適切に行われているか。

systemdで起動停止したサービスについて、psコマンドにてゾンビなどが残っていないかを確認する。

[root@nis1 log]# systemctl status fetchlogserver.service
 fetchlogserver.service - My Python App
   Loaded: loaded (/etc/systemd/system/fetchlogserver.service; disabled; vendor preset>
   Active: active (running) since Sat 2024-09-21 09:25:00 JST; 10h ago
 Main PID: 114474 (python3)
    Tasks: 1 (limit: 11004)
   Memory: 9.3M
   CGroup: /system.slice/fetchlogserver.service
           mq114474 /usr/bin/python3 /home/app/asyncio_server/server/server.py

 9 21 09:25:00 nis1 systemd[1]: fetchlogserver.service: Succeeded.
 9 21 09:25:00 nis1 systemd[1]: Stopped My Python App.
 9 21 09:25:00 nis1 systemd[1]: Started My Python App.
[root@nis1 log]#
[root@nis1 log]# ps aux | grep python3
root      134368  0.2  1.0 303176 18268 ?        Ss   07:04   0:00 /usr/bin/python3 /home/app/asyncio_server/server/server.py
root      134381  0.0  0.0 221944  1180 pts/2    R+   07:04   0:00 grep --color=auto python3
[root@nis1 log]#

停止してみよう

[root@nis1 log]# systemctl stop fetchlogserver.service
[root@nis1 log]# systemctl status fetchlogserver.service
 fetchlogserver.service - My Python App
   Loaded: loaded (/etc/systemd/system/fetchlogserver.service; disabled; vendor preset>
   Active: inactive (dead)

 9 22 06:57:59 nis1 systemd[1]: fetchlogserver.service: Succeeded.
 9 22 06:57:59 nis1 systemd[1]: Stopped My Python App.
 9 22 06:58:46 nis1 systemd[1]: Started My Python App.
 9 22 07:04:07 nis1 systemd[1]: Stopping My Python App...
 9 22 07:04:07 nis1 systemd[1]: fetchlogserver.service: Succeeded.
 9 22 07:04:07 nis1 systemd[1]: Stopped My Python App.
 9 22 07:04:22 nis1 systemd[1]: Started My Python App.
 9 22 07:05:17 nis1 systemd[1]: Stopping My Python App...
 9 22 07:05:17 nis1 systemd[1]: fetchlogserver.service: Succeeded.
 9 22 07:05:17 nis1 systemd[1]: Stopped My Python App.
[root@nis1 log]# ps aux | grep python3
root      134402  0.0  0.0 221944  1100 pts/2    R+   07:05   0:00 grep --color=auto python3
[root@nis1 log]#

python3で起動していたserver.pyのプロセスが消えていることを確認できた。