#!/usr/bin/env python3

# Send SIGWINCH (i.e. graceful-stop) to all `apache2`-processes run by
# "www-data" that have more than LIMIT_IN_BYTES resident memory allocated.

# Copyright (C) 2025  Johannes Truschnigg <johannes@truschnigg.info>
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <https://www.gnu.org/licenses/>.

# This script should work with both python2.7 and python3.5+

APACHE_USER = 'www-data'
PROC_EXE_MATCH = 'apache2'
LIMIT_IN_BYTES = 1024*1024*256
LOOP_DELAY_SECS = 5


from __future__ import print_function
import glob
import os
import pwd
import resource
import signal
import time
import sys


PAGE_SIZE = resource.getpagesize()
apache_uid = pwd.getpwnam(APACHE_USER).pw_uid
while True:
    proc_dirs = glob.glob('/proc/[0-9]*')
    for d in proc_dirs:
        pid = int(d.split("/")[-1])
        try:
            st = os.stat(d)
            if st.st_uid == apache_uid:
                cmdlinefile = os.path.join(d, "cmdline")
                try:
                    with open(cmdlinefile, "r") as c:
                        cmdline = str(c.read()).split('\x00')
                        if PROC_EXE_MATCH in cmdline[0]:
                            statmfile = os.path.join(d, "statm")
                            with open(statmfile, "r") as s:
                                memkeys = ['size', 'resident', 'shared', 'text', '__lib', 'data', '__dirty']
                                meminfo = dict(zip(memkeys, [int(p) * int(PAGE_SIZE) for p in s.read().split()]))
                                #print(pid, meminfo['resident'])
                                if meminfo['resident'] > LIMIT_IN_BYTES:
                                    os.kill(pid, signal.SIGWINCH)
                                    print("Sent SIGWINCH to PID %d ('%s', %dM RSS)" % ( pid, cmdline[0], meminfo['resident'] / (1024*1024)))
                                    sys.stdout.flush()
                except IOError as e:
                    # This is another possible result of the race conditon
                    # described below.
                    continue
        except OSError as e:
            # If we end up here, it's most likely that the PID associated with
            # the procfs directory has terminated in the meantime. There's no
            # easy solution to prevent this race, but since we're not
            # interested in non-existing processes anyway, we might as well
            # just not care.

            #print(e)
            continue
    time.sleep(LOOP_DELAY_SECS)
