nohang/nohang

1501 lines
53 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# Nohang - The No Hang Daemon for Linux
##########################################################################
# импорты
import os
from operator import itemgetter
from time import sleep
import datetime
from argparse import ArgumentParser
import time
##########################################################################
# задание констант
version = 'upstream'
sig_dict = {9: 'SIGKILL', 15: 'SIGTERM'}
# директория, в которой запущен скрипт
cd = os.getcwd()
# где искать конфиг, если не указан через опцию -c/--config
default_configs = (
cd + '/nohang.conf',
'/etc/nohang/nohang.conf'
)
# универсальное сообщение при инвалидном конфиге
conf_err_mess = '\nSet up the path to the valid config file with -c/--confi' \
'g option!\nExit'
# означает, что при задани zram disksize = 10000M доступная память
# уменьшится на 42M
# найден экспериментально, требует уточнения с разными ядрами и архитектурами
# на небольших дисксайзах (до гигабайта) может быть больше, до 0.0045
# создатель модуля zram утверждает, что zram_disksize_factor доожен быть 0.001
# ("zram uses about 0.1% of the size of the disk"
# - https://www.kernel.org/doc/Documentation/blockdev/zram.txt),
# но это утверждение противоречит опытным данным
zram_disksize_factor = 0.0042
##########################################################################
# задание функций
def string_to_float_convert_test(string):
try:
return float(string)
except ValueError:
return None
def string_to_int_convert_test(string):
try:
return int(string)
except ValueError:
return None
# извлечение праметра из словаря конфига, возврат str
def conf_parse_string(param):
if param in config_dict:
return config_dict[param].strip()
else:
print('{} not in config\nExit'.format(param))
exit()
# извлечение праметра из словаря конфига, возврат bool
def conf_parse_bool(param):
if param in config_dict:
param_str = config_dict[param]
if param_str == 'True':
return True
elif param_str == 'False':
return False
else:
print('Invalid {} value {} (shou' \
'ld be True or False)\nExit'.format(param, param_str))
exit()
else:
print('{} not in config\nExit'.format(param))
exit()
def func_decrease_oom_score_adj(oom_score_adj_max):
# цикл для наполнения oom_list
for i in os.listdir('/proc'):
# пропускаем элементы, не состоящие только из цифр
if i.isdigit() is not True:
continue
try:
oom_score_adj = int(rline1('/proc/' + i + '/oom_score_adj'))
if oom_score_adj > oom_score_adj_max:
write('/proc/' + i + '/oom_score_adj',
str(oom_score_adj_max) + '\n')
except FileNotFoundError:
pass
except ProcessLookupError:
pass
# чтение первой строки файла
def rline1(path):
with open(path) as f:
for line in f:
return line[:-1]
# запись в файл
def write(path, string):
with open(path, 'w') as f:
f.write(string)
def append_log(string):
try:
with open(logfile, 'a') as f:
f.write(string + '\n')
except PermissionError:
print(
' Cannot append log {}: PermissionError'.format(
logfile))
except IsADirectoryError:
print(
' Cannot append log {}: IsADirectoryError'.format(
logfile))
def kib_to_mib(num):
return round(num / 1024.0)
def percent(num):
return round(num * 100, 1)
def just_percent_mem(num):
return str(round(num * 100, 1)).rjust(4, ' ')
def just_percent_swap(num):
return str(round(num * 100, 1)).rjust(5, ' ')
# K -> M, выравнивание по правому краю
def human(num, lenth):
return str(round(num / 1024)).rjust(lenth, ' ')
# возвращает disksize и mem_used_total по zram id
def zram_stat(zram_id):
try:
disksize = rline1('/sys/block/' + zram_id + '/disksize')
except FileNotFoundError:
return '0', '0'
if disksize == ['0\n']:
return '0', '0'
try:
mm_stat = rline1('/sys/block/' + zram_id + '/mm_stat').split(' ')
mm_stat_list = []
for i in mm_stat:
if i != '':
mm_stat_list.append(i)
mem_used_total = mm_stat_list[2]
except FileNotFoundError:
mem_used_total = rline1('/sys/block/' + zram_id + '/mem_used_total')
return disksize, mem_used_total # BYTES, str
# имя через пид
def pid_to_name(pid):
try:
with open('/proc/' + pid + '/status') as f:
for line in f:
return line[:-1].split('\t')[1]
except FileNotFoundError:
return '<unknown>'
except ProcessLookupError:
return '<unknown>'
def send_notify_warn():
# текст отправляемого уведомления
if mem_used_zram > 0:
info = '"<i>MemAvailable:</i> <b>{} MiB</b>\n<i>SwapFree:</i> <b>{} MiB</b>\n<i>MemUsedZram:</i> <b>{} MiB</b>" &'.format(
kib_to_mib(mem_available),
kib_to_mib(swap_free),
kib_to_mib(mem_used_zram))
elif swap_free > 0:
info = '"<i>MemAvailable:</i> <b>{} MiB</b>\n<i>SwapFree:</i>' \
' <b>{} MiB</b>" &'.format(
kib_to_mib(mem_available),
kib_to_mib(swap_free))
else:
info = '"<i>MemAvailable:</i> <b>{} MiB</b>, <b>{} %</b>" &'.format(
kib_to_mib(mem_available),
round(mem_available / mem_total * 100, 1))
if root:
# отправляем уведомления всем залогиненным
for uid in os.listdir('/run/user'):
root_notify_command = 'sudo -u {} DISPLAY={} notify-s' \
'end {} "Warning: Low Memory" '.format(
users_dict[uid], root_display, notify_options)
os.system(root_notify_command + info)
else:
# отправляем уведомление пользователю, который запустил nohang
user_notify_command = 'notify-send {} "Warning: Low Memory" '.format(
notify_options)
os.system(user_notify_command + info)
def send_notify(signal, name, pid, oom_score, vm_rss, vm_swap):
# текст отправляемого уведомления
info = '"<u>Nohang</u> sent <u>{}</u> \nto the process <b>{}</b> \n<i>P' \
'id:</i> <b>{}</b> \n<i>Badness:</i> <b>{}</b> \n<i>VmRSS:</i> <b' \
'>{} MiB</b> \n<i>VmSwap:</i> <b>{} MiB</b>" &'.format(
sig_dict[signal], name, pid, oom_score, vm_rss, vm_swap)
if root:
# отправляем уведомление всем залогиненным пользователям
for uid in os.listdir('/run/user'):
root_notify_command = 'sudo -u {} DISPLAY={} notify-send {} "Pr' \
'eventing OOM" '.format(
users_dict[uid], root_display, notify_options)
os.system(root_notify_command + info)
else:
# отправляем уведомление пользователю, который запустил nohang
user_notify_command = 'notify-send {} "Preventing OOM" '.format(
notify_options)
os.system(user_notify_command + info)
def send_notify_black(signal, name, pid):
# текст отправляемого уведомления
info = '"<u>Nohang</u> sent <u>{}</u>\nto blacklisted proce' \
'ss <b>{}</b>, <i>Pid</i> <b>{}</b>" &'.format(
sig_dict[signal], name, pid)
if root:
# отправляем уведомление всем залогиненным пользователям
for uid in os.listdir('/run/user'):
root_notify_command = 'sudo -u {} DISPLAY={} notify-send {} "Pr' \
'eventing OOM" '.format(
users_dict[uid], root_display, notify_options)
os.system(root_notify_command + info)
else:
# отправляем уведомление пользователю, который запустил nohang
user_notify_command = 'notify-send {} "Preventing OOM" '.format(
notify_options)
os.system(user_notify_command + info)
def sleep_after_send_signal(signal):
if signal is 9:
if print_sleep_periods:
print(' sleep', min_delay_after_sigkill)
sleep(min_delay_after_sigterm)
else:
if print_sleep_periods:
print(' sleep', min_delay_after_sigterm)
sleep(min_delay_after_sigterm)
def find_victim_and_send_signal(signal):
time_now_and_mem_info = datetime.datetime.today().strftime(
'\n%Y-%m-%d %a %H:%M:%S\n{}'.format(mem_info))
print(time_now_and_mem_info)
if logging:
append_log(time_now_and_mem_info)
# выставляем потолок для oom_score_adj всех процессов
if decrease_oom_score_adj and root:
func_decrease_oom_score_adj(oom_score_adj_max)
# получаем список процессов ((pid, badness))
oom_list = []
if use_regex_lists:
# pid list, rename var! blacklisted_pids
oom_blacklist_regex = []
for pid in os.listdir('/proc'):
if pid.isdigit() is not True:
continue
try:
# пошла итерация для каждого существующего процесса
oom_score = int(rline1('/proc/' + pid + '/oom_score'))
name = pid_to_name(pid)
# если имя в белом списке,то пропускаем
res = fullmatch(whitelist_regex, name)
if res is not None:
print(' {} (Pid: {}) matches with whitelist_regex'.format(name, pid)),
continue
# если имя в черном списке - добавляем Pid в список для убийства
# ДОБАВЛЯТЬ И ИМЯ СРАЗУ
# ВАРИАНТ - СРАЗУ УБИВАТЬ
res = fullmatch(blacklist_regex, name)
if res is not None:
oom_blacklist_regex.append(pid)
print(' {} (Pid: {}) matches with blacklist_regex'.format(name, pid)),
res = fullmatch(avoidlist_regex, name)
if res is not None:
# тут уже получаем badness
oom_score = int(oom_score / avoidlist_factor)
print(' {} (Pid: {}, Badness {}) matches with avoidlist_regex'.format(name, pid, oom_score)),
res = fullmatch(preferlist_regex, name)
if res is not None:
oom_score = int((oom_score + 1) * preferlist_factor)
print(' {} (Pid: {}, Badness {}) matches with preferlist_regex'.format(name, pid, oom_score)),
except FileNotFoundError:
oom_score = 0
except ProcessLookupError:
oom_score = 0
oom_list.append((pid, oom_score))
# если найден хоть один в черном списке - нет смысла сравнивать с остальными
if oom_blacklist_regex != []:
for pid in oom_blacklist_regex:
name = pid_to_name(pid)
print(' Preventing OOM: trying to send the {} signal to blacklisted process {}, Pid: {}'.format(
sig_dict[signal], name, pid))
try:
os.kill(int(pid), signal)
print(' Success')
if desktop_notifications:
send_notify_black(signal, name, pid)
except FileNotFoundError:
print(' No such process')
except ProcessLookupError:
print(' No such process')
except PermissionError:
print(' Operation not permitted')
# после отправки сигнала процессам из черного списка поспать и выйти из функции
sleep_after_send_signal(signal)
return 0
else:
# not use regex
for i in os.listdir('/proc'):
if i[0].isdecimal() is not True:
continue
try:
oom_score = int(rline1('/proc/' + i + '/oom_score'))
except FileNotFoundError:
oom_score = 0
except ProcessLookupError:
oom_score = 0
oom_list.append((i, oom_score))
# получаем отсортированный по oom_score (по badness!) список пар (pid, oom_score)
pid_tuple_list = sorted(oom_list, key=itemgetter(1), reverse=True)[0]
# получаем максимальный oom_score
oom_score = pid_tuple_list[1]
if oom_score >= oom_score_min:
# пытаемся отправить сигнал найденной жертве
pid = pid_tuple_list[0]
name = pid_to_name(pid)
# находим VmRSS и VmSwap процесса, которому попытаемся послать сигнал
try:
with open('/proc/' + pid + '/status') as f:
for n, line in enumerate(f):
if n is vm_rss_index:
vm_rss = kib_to_mib(int(
line.split('\t')[1][:-4]))
continue
if n is vm_swap_index:
vm_swap = kib_to_mib(int(
line.split('\t')[1][:-4]))
break
except FileNotFoundError:
vm_rss = 0
vm_swap = 0
except ProcessLookupError:
vm_rss = 0
vm_swap = 0
except IndexError:
vm_rss = 0
vm_swap = 0
if name in etc_dict and signal is 15:
command = etc_dict[name]
etc_info = ' Process {} is a victim,\n Pid: {}, Badness: {}, VmRSS: {} MiB, VmSwap: {} MiB\n Execute command: {}'.format(name, pid, oom_score, vm_rss, vm_swap, command)
print(etc_info)
os.system(etc_dict[name])
append_log(etc_info)
else:
try_to_send = ' Preventing OOM: trying to send the {} signal to {},\n Pid: {}, Badness: {}, VmRSS: {} MiB, VmSwap: {} MiB'.format(sig_dict[signal], name, pid, oom_score, vm_rss, vm_swap)
print(try_to_send)
try:
os.kill(int(pid), signal)
send_result = ' Success'
if desktop_notifications:
send_notify(signal, name, pid, oom_score, vm_rss, vm_swap)
except FileNotFoundError:
send_result = ' No such process'
except ProcessLookupError:
send_result = ' No such process'
print(send_result)
if logging:
append_log(try_to_send + '\n' + send_result)
else:
badness_is_too_small = ' oom_score {} < oom_score_min {}'.format(
oom_score, oom_score_min)
print(badness_is_too_small)
if logging:
append_log(badness_is_too_small)
sleep_after_send_signal(signal)
def sleep_after_check_mem():
# задание периода сна в зависимости от рейтов и уровней доступной памяти
t_mem = mem_available / rate_mem
t_swap = swap_free / rate_swap
t_zram = (mem_total - mem_used_zram) / rate_zram
t_mem_swap = t_mem + t_swap
t_mem_zram = t_mem + t_zram
if t_mem_swap <= t_mem_zram:
t = t_mem_swap
else:
t = t_mem_zram
try:
if print_sleep_periods:
print('sleep', round(t, 2), ' (t_mem={}, t_swap={}, t_zram={})'.format(
round(t_mem, 2), round(t_swap, 2), round(t_zram, 2)))
sleep(t)
except KeyboardInterrupt:
exit()
##########################################################################
# поиск позиций и mem_total
with open('/proc/meminfo') as file:
mem_list = file.readlines()
mem_list_names = []
for s in mem_list:
mem_list_names.append(s.split(':')[0])
if mem_list_names[2] != 'MemAvailable':
print('Your Linux kernel is too old, 3.14+ requie\nExit')
exit()
swap_total_index = mem_list_names.index('SwapTotal')
swap_free_index = swap_total_index + 1
mem_total = int(mem_list[0].split(':')[1].strip(' kB\n'))
with open('/proc/self/status') as file:
status_list = file.readlines()
# список имен из /proc/*/status для дальнейшего поиска позиций VmRSS and VmSwap
status_names = []
for s in status_list:
status_names.append(s.split(':')[0])
vm_rss_index = status_names.index('VmRSS')
vm_swap_index = status_names.index('VmSwap')
##########################################################################
# получение пути к конфигу
# парсинг аргументов командной строки
parser = ArgumentParser()
parser.add_argument(
'-c',
'--config',
help="""path to the config file, default values:
./nohang.conf, /etc/nohang/nohang.conf""",
default=None,
type=str
)
args = parser.parse_args()
arg_config = args.config
if arg_config is None:
config = None
for i in default_configs:
if os.path.exists(i):
config = i
break
if config is None:
print('По дефолтным путям конфиг не найден\n', conf_err_mess)
exit()
else:
if os.path.exists(arg_config):
config = arg_config
else:
print("File {} doesn't exists{}".format(arg_config, conf_err_mess))
exit()
print('The path to the config file to be used:')
print(config)
##########################################################################
# парсинг конфига с получением словаря параметров
# conf_parameters_dict
# conf_restart_dict
try:
with open(config) as f:
# словарь с параметрами конфига
config_dict = dict()
# словарь с именами и командами для параметра execute_the_command
etc_dict = dict()
for line in f:
a = line.startswith('#')
b = line.startswith('\n')
c = line.startswith('\t')
d = line.startswith(' ')
etc = line.startswith('**')
if not a and not b and not c and not d and not etc:
a = line.split('=')
config_dict[a[0].strip()] = a[1].strip()
if etc:
a = line[2:].split('::')
etc_name = a[0].strip()
etc_command = a[1].strip()
if len(etc_name) > 15:
print('инвалид конфиг, длина имени процесса не должна превышать 15 символов\nExit')
exit()
etc_dict[etc_name] = etc_command
except PermissionError:
print('PermissionError', conf_err_mess)
exit()
except UnicodeDecodeError:
print('UnicodeDecodeError', conf_err_mess)
exit()
except IsADirectoryError:
print('IsADirectoryError', conf_err_mess)
exit()
except IndexError:
print('IndexError', conf_err_mess)
exit()
##########################################################################
# извлечение параметров из словаря
# проверка наличия всех необходимых параметров
# валидация всех параметров
print_config = conf_parse_bool('print_config')
print_mem_check_results = conf_parse_bool('print_mem_check_results')
print_sleep_periods = conf_parse_bool('print_sleep_periods')
mlockall = conf_parse_bool('mlockall')
if 'self_nice' in config_dict:
self_nice = string_to_int_convert_test(config_dict['self_nice'])
if self_nice is None:
print('Invalid self_nice value, not integer\nExit')
exit()
if self_nice < -20 or self_nice > 19:
print('Недопустимое значение self_nice\nExit')
exit()
else:
print('self_nice not in config\nExit')
exit()
if 'self_oom_score_adj' in config_dict:
self_oom_score_adj = string_to_int_convert_test(
config_dict['self_oom_score_adj'])
if self_oom_score_adj is None:
print('Invalid self_oom_score_adj value, not integer\nExit')
exit()
if self_oom_score_adj < -1000 or self_oom_score_adj > 1000:
print('Недопустимое значение self_oom_score_adj\nExit')
exit()
else:
print('self_oom_score_adj not in config\nExit')
exit()
if 'rate_mem' in config_dict:
rate_mem = string_to_float_convert_test(config_dict['rate_mem'])
if rate_mem is None:
print('Invalid rate_mem value, not float\nExit')
exit()
if rate_mem <= 0:
print('rate_mem должен быть положительным\nExit')
exit()
else:
print('rate_mem not in config\nExit')
exit()
if 'rate_swap' in config_dict:
rate_swap = string_to_float_convert_test(config_dict['rate_swap'])
if rate_swap is None:
print('Invalid rate_swap value, not float\nExit')
exit()
if rate_swap <= 0:
print('rate_swap должен быть положительным\nExit')
exit()
else:
print('rate_swap not in config\nExit')
exit()
if 'rate_zram' in config_dict:
rate_zram = string_to_float_convert_test(config_dict['rate_zram'])
if rate_zram is None:
print('Invalid rate_zram value, not float\nExit')
exit()
if rate_zram <= 0:
print('rate_zram должен быть положительным\nExit')
exit()
else:
print('rate_zram not in config\nExit')
exit()
if 'mem_min_sigterm' in config_dict:
mem_min_sigterm = config_dict['mem_min_sigterm']
if mem_min_sigterm.endswith('%'):
# отбрасываем процент, получаем число
mem_min_sigterm_percent = mem_min_sigterm[:-1].strip()
# далее флоат тест
mem_min_sigterm_percent = string_to_float_convert_test(mem_min_sigterm_percent)
if mem_min_sigterm_percent is None:
print('Invalid mem_min_sigterm value, not float\nExit')
exit()
# окончательная валидация
if mem_min_sigterm_percent < 0 or mem_min_sigterm_percent > 100:
print('mem_min_sigterm, выраженный в процентах, должен быть быть в диапазоне [0; 100]\nExit')
exit()
# mem_min_sigterm_percent это теперь чистое валидное флоат число процентов, можно переводить в кб
mem_min_sigterm_kb = mem_min_sigterm_percent / 100 * mem_total
mem_min_sigterm_mb = round(mem_min_sigterm_kb / 1024)
elif mem_min_sigterm.endswith('M'):
mem_min_sigterm_mb = string_to_float_convert_test(mem_min_sigterm[:-1].strip())
if mem_min_sigterm_mb is None:
print('Invalid mem_min_sigterm value, not float\nExit')
exit()
mem_min_sigterm_kb = mem_min_sigterm_mb * 1024
if mem_min_sigterm_kb > mem_total:
print('mem_min_sigterm value не должен быть больше MemTotal ({} MiB)\nExit'.format(round(mem_total / 1024)))
exit()
mem_min_sigterm_percent = mem_min_sigterm_kb / mem_total * 100
else:
print('Конфиг инвалид, для mem_min_sigterm неверно указаны единицы измерения\nExit')
exit()
else:
print('mem_min_sigterm not in config\nExit')
exit()
if 'mem_min_sigkill' in config_dict:
mem_min_sigkill = config_dict['mem_min_sigkill']
if mem_min_sigkill.endswith('%'):
# отбрасываем процент, получаем число
mem_min_sigkill_percent = mem_min_sigkill[:-1].strip()
# далее флоат тест
mem_min_sigkill_percent = string_to_float_convert_test(mem_min_sigkill_percent)
if mem_min_sigkill_percent is None:
print('Invalid mem_min_sigkill value, not float\nExit')
exit()
# окончательная валидация
if mem_min_sigkill_percent < 0 or mem_min_sigkill_percent > 100:
print('mem_min_sigkill, выраженный в процентах, должен быть быть в диапазоне [0; 100]\nExit')
exit()
# mem_min_sigterm_percent это теперь чистое валидное флоат число процентов, можно переводить в кб
mem_min_sigkill_kb = mem_min_sigkill_percent / 100 * mem_total
mem_min_sigkill_mb = round(mem_min_sigkill_kb / 1024)
elif mem_min_sigkill.endswith('M'):
mem_min_sigkill_mb = string_to_float_convert_test(mem_min_sigkill[:-1].strip())
if mem_min_sigkill_mb is None:
print('Invalid mem_min_sigkill value, not float\nExit')
exit()
mem_min_sigkill_kb = mem_min_sigkill_mb * 1024
if mem_min_sigkill_kb > mem_total:
print('mem_min_sigkill value не должен быть больше MemTotal ({} MiB)\nExit'.format(round(mem_total / 1024)))
exit()
mem_min_sigkill_percent = mem_min_sigkill_kb / mem_total * 100
else:
print('Конфиг инвалид, для mem_min_sigkill неверно указаны единицы измерения\nExit')
exit()
else:
print('mem_min_sigkill not in config\nExit')
exit()
# НУЖНА ВАЛИДАЦИЯ НА МЕСТЕ!
if 'swap_min_sigterm' in config_dict:
swap_min_sigterm = config_dict['swap_min_sigterm']
else:
print('swap_min_sigterm not in config\nExit')
exit()
# НУЖНА ВАЛИДАЦИЯ НА МЕСТЕ!
if 'swap_min_sigkill' in config_dict:
swap_min_sigkill = config_dict['swap_min_sigkill']
else:
print('swap_min_sigkill not in config\nExit')
exit()
if 'zram_max_sigterm' in config_dict:
zram_max_sigterm = config_dict['zram_max_sigterm']
if zram_max_sigterm.endswith('%'):
# отбрасываем процент, получаем число
zram_max_sigterm_percent = zram_max_sigterm[:-1].strip()
# далее флоат тест
zram_max_sigterm_percent = string_to_float_convert_test(zram_max_sigterm_percent)
if zram_max_sigterm_percent is None:
print('Invalid zram_max_sigterm value, not float\nExit')
exit()
# окончательная валидация
if zram_max_sigterm_percent < 0 or zram_max_sigterm_percent > 100:
print('zram_max_sigterm, выраженный в процентах, должен быть быть в диапазоне [0; 100]\nExit')
exit()
# zram_max_sigterm_percent это теперь чистое валидное флоат число процентов, можно переводить в кб
zram_max_sigterm_kb = zram_max_sigterm_percent / 100 * mem_total
zram_max_sigterm_mb = round(zram_max_sigterm_kb / 1024)
elif zram_max_sigterm.endswith('M'):
zram_max_sigterm_mb = string_to_float_convert_test(zram_max_sigterm[:-1].strip())
if zram_max_sigterm_mb is None:
print('Invalid zram_max_sigterm value, not float\nExit')
exit()
zram_max_sigterm_kb = zram_max_sigterm_mb * 1024
if zram_max_sigterm_kb > mem_total:
print('zram_max_sigterm value не должен быть больше MemTotal ({} MiB)\nExit'.format(round(mem_total / 1024)))
exit()
zram_max_sigterm_percent = zram_max_sigterm_kb / mem_total * 100
else:
print('Конфиг инвалид, для zram_max_sigterm неверно указаны единицы измерения\nExit')
exit()
else:
print('zram_max_sigterm not in config\nExit')
exit()
if 'zram_max_sigkill' in config_dict:
zram_max_sigkill = config_dict['zram_max_sigkill']
if zram_max_sigkill.endswith('%'):
# отбрасываем процент, получаем число
zram_max_sigkill_percent = zram_max_sigkill[:-1].strip()
# далее флоат тест
zram_max_sigkill_percent = string_to_float_convert_test(zram_max_sigkill_percent)
if zram_max_sigkill_percent is None:
print('Invalid zram_max_sigkill value, not float\nExit')
exit()
# окончательная валидация
if zram_max_sigkill_percent < 0 or zram_max_sigkill_percent > 100:
print('zram_max_sigkill, выраженный в процентах, должен быть быть в диапазоне [0; 100]\nExit')
exit()
# zram_max_sigkill_percent это теперь чистое валидное флоат число процентов, можно переводить в кб
zram_max_sigkill_kb = zram_max_sigkill_percent / 100 * mem_total
zram_max_sigkill_mb = round(zram_max_sigkill_kb / 1024)
elif zram_max_sigkill.endswith('M'):
zram_max_sigkill_mb = string_to_float_convert_test(zram_max_sigkill[:-1].strip())
if zram_max_sigkill_mb is None:
print('Invalid zram_max_sigkill value, not float\nExit')
exit()
zram_max_sigkill_kb = zram_max_sigkill_mb * 1024
if zram_max_sigkill_kb > mem_total:
print('zram_max_sigkill value не должен быть больше MemTotal ({} MiB)\nExit'.format(round(mem_total / 1024)))
exit()
zram_max_sigkill_percent = zram_max_sigkill_kb / mem_total * 100
else:
print('Конфиг инвалид, для zram_max_sigkill неверно указаны единицы измерения\nExit')
exit()
else:
print('zram_max_sigkill not in config\nExit')
exit()
if 'min_delay_after_sigterm' in config_dict:
min_delay_after_sigterm = string_to_float_convert_test(
config_dict['min_delay_after_sigterm'])
if min_delay_after_sigterm is None:
print('Invalid min_delay_after_sigterm value, not float\nExit')
exit()
if min_delay_after_sigterm < 0:
print('min_delay_after_sigterm должен быть неотрицательным\nExit')
exit()
else:
print('min_delay_after_sigterm not in config\nExit')
exit()
if 'min_delay_after_sigkill' in config_dict:
min_delay_after_sigkill = string_to_float_convert_test(
config_dict['min_delay_after_sigkill'])
if min_delay_after_sigkill is None:
print('Invalid min_delay_after_sigkill value, not float\nExit')
exit()
if min_delay_after_sigkill < 0:
print('min_delay_after_sigkill должен быть неотрицательным\nExit')
exit()
else:
print('min_delay_after_sigkill not in config\nExit')
exit()
if 'oom_score_min' in config_dict:
oom_score_min = string_to_int_convert_test(
config_dict['oom_score_min'])
if oom_score_min is None:
print('Invalid oom_score_min value, not integer\nExit')
exit()
if oom_score_min < 0 or oom_score_min > 1000:
print('Недопустимое значение oom_score_min\nExit')
exit()
else:
print('oom_score_min not in config\nExit')
exit()
decrease_oom_score_adj = conf_parse_bool('decrease_oom_score_adj')
if 'oom_score_adj_max' in config_dict:
oom_score_adj_max = string_to_int_convert_test(
config_dict['oom_score_adj_max'])
if oom_score_adj_max is None:
print('Invalid oom_score_adj_max value, not integer\nExit')
exit()
if oom_score_adj_max < 0 or oom_score_adj_max > 1000:
print('Недопустимое значение oom_score_adj_max\nExit')
exit()
else:
print('oom_score_adj_max not in config\nExit')
exit()
if 'desktop_notifications' in config_dict:
desktop_notifications = config_dict['desktop_notifications']
if desktop_notifications == 'True':
desktop_notifications = True
users_dict = dict()
with open('/etc/passwd') as f:
for line in f:
line_list = line.split(':')
username = line_list[0]
uid = line_list[2]
users_dict[uid] = username
elif desktop_notifications == 'False':
desktop_notifications = False
else:
print('Invalid desktop_notifications value {} (shoul' \
'd be True or False)\nExit'.format(
desktop_notifications))
exit()
else:
print('desktop_notifications not in config\nExit')
exit()
notify_options = conf_parse_string('notify_options')
root_display = conf_parse_string('root_display')
use_regex_lists = conf_parse_bool('use_regex_lists')
if use_regex_lists:
from re import fullmatch
whitelist_regex = conf_parse_string('whitelist_regex')
blacklist_regex = conf_parse_string('blacklist_regex')
preferlist_regex = conf_parse_string('preferlist_regex')
if 'preferlist_factor' in config_dict:
preferlist_factor = string_to_float_convert_test(config_dict['preferlist_factor'])
if preferlist_factor is None:
print('Invalid preferlist_factor value, not float\nExit')
exit()
if preferlist_factor < 1 and preferlist_factor > 1000:
print('preferlist_factor должен быть в диапазоне [1; 1000]\nExit')
exit()
else:
print('preferlist_factor not in config\nExit')
exit()
avoidlist_regex = conf_parse_string('avoidlist_regex')
if 'avoidlist_factor' in config_dict:
avoidlist_factor = string_to_float_convert_test(config_dict['avoidlist_factor'])
if avoidlist_factor is None:
print('Invalid avoidlist_factor value, not float\nExit')
exit()
if avoidlist_factor < 1 and avoidlist_factor > 1000:
print('avoidlist_factor должен быть в диапазоне [1; 1000]\nExit')
exit()
else:
print('avoidlist_factor not in config\nExit')
exit()
logging = conf_parse_bool('logging')
logfile = conf_parse_string('logfile')
low_memory_warnings = conf_parse_bool('low_memory_warnings')
if 'min_time_between_warnings' in config_dict:
min_time_between_warnings = string_to_float_convert_test(
config_dict['min_time_between_warnings'])
if min_time_between_warnings is None:
print('Invalid min_time_between_warnings value, not float\nExit')
exit()
if min_time_between_warnings < 1 or min_time_between_warnings > 300:
print('Недопустимое значение min_time_between_warnings, должно быть в диапазоне [1; 300]\nExit')
exit()
else:
print('min_time_between_warnings not in config\nExit')
exit()
if 'mem_min_warnings' in config_dict:
mem_min_warnings = config_dict['mem_min_warnings']
if mem_min_warnings.endswith('%'):
# отбрасываем процент, получаем число
mem_min_warnings_percent = mem_min_warnings[:-1].strip()
# далее флоат тест
mem_min_warnings_percent = string_to_float_convert_test(mem_min_warnings_percent)
if mem_min_warnings_percent is None:
print('Invalid mem_min_warnings value, not float\nExit')
exit()
# окончательная валидация
if mem_min_warnings_percent < 0 or mem_min_warnings_percent > 100:
print('mem_min_warnings, выраженный в процентах, должен быть быть в диапазоне [0; 100]\nExit')
exit()
# mem_min_warnings_percent это теперь чистое валидное флоат число процентов, можно переводить в кб
mem_min_warnings_kb = mem_min_warnings_percent / 100 * mem_total
mem_min_warnings_mb = round(mem_min_warnings_kb / 1024)
elif mem_min_warnings.endswith('M'):
mem_min_warnings_mb = string_to_float_convert_test(mem_min_warnings[:-1].strip())
if mem_min_warnings_mb is None:
print('Invalid mem_min_warnings value, not float\nExit')
exit()
mem_min_warnings_kb = mem_min_warnings_mb * 1024
if mem_min_warnings_kb > mem_total:
print('mem_min_warnings value не должен быть больше MemTotal ({} MiB)\nExit'.format(round(mem_total / 1024)))
exit()
mem_min_warnings_percent = mem_min_warnings_kb / mem_total * 100
else:
print('Конфиг инвалид, для mem_min_warnings неверно указаны единицы измерения\nExit')
exit()
else:
print('mem_min_warnings not in config\nExit')
exit()
# НА МЕСТЕ!!!
if 'swap_min_warnings' in config_dict:
swap_min_warnings = config_dict['swap_min_warnings']
else:
print('swap_min_warnings not in config\nExit')
exit()
if 'zram_max_warnings' in config_dict:
zram_max_warnings = config_dict['zram_max_warnings']
if zram_max_warnings.endswith('%'):
# отбрасываем процент, получаем число
zram_max_warnings_percent = zram_max_warnings[:-1].strip()
# далее флоат тест
zram_max_warnings_percent = string_to_float_convert_test(zram_max_warnings_percent)
if zram_max_warnings_percent is None:
print('Invalid zram_max_warnings value, not float\nExit')
exit()
# окончательная валидация
if zram_max_warnings_percent < 0 or zram_max_warnings_percent > 100:
print('zram_max_warnings, выраженный в процентах, должен быть быть в диапазоне [0; 100]\nExit')
exit()
# zram_max_warnings_percent это теперь чистое валидное флоат число процентов, можно переводить в кб
zram_max_warnings_kb = zram_max_warnings_percent / 100 * mem_total
zram_max_warnings_mb = round(zram_max_warnings_kb / 1024)
elif zram_max_warnings.endswith('M'):
zram_max_warnings_mb = string_to_float_convert_test(zram_max_warnings[:-1].strip())
if zram_max_warnings_mb is None:
print('Invalid zram_max_warnings value, not float\nExit')
exit()
zram_max_warnings_kb = zram_max_warnings_mb * 1024
if zram_max_warnings_kb > mem_total:
print('zram_max_warnings value не должен быть больше MemTotal ({} MiB)\nExit'.format(round(mem_total / 1024)))
exit()
zram_max_warnings_percent = zram_max_warnings_kb / mem_total * 100
else:
print('Конфиг инвалид, для zram_max_warnings неверно указаны единицы измерения\nExit')
exit()
else:
print('zram_max_warnings not in config\nExit')
exit()
execute_the_command = conf_parse_bool('execute_the_command')
##########################################################################
# получение уровней в кибибайтах
# возвращает число килобайт при задании в конфиге абсолютного значения,
# или кортеж с числом процентов
def sig_level_to_kb_swap(string):
if string.endswith('%'):
return float(string[:-1].strip()), True
elif string.endswith('M'):
return float(string[:-1].strip()) * 1024
else:
print('Конфиг инвалид, где-то неверно указаны единицы измерения\nExit')
exit()
# получаем число килобайт или кортеж с процентами
swap_min_sigterm_swap = sig_level_to_kb_swap(swap_min_sigterm)
swap_min_sigkill_swap = sig_level_to_kb_swap(swap_min_sigkill)
swap_min_warnings_swap = sig_level_to_kb_swap(swap_min_warnings)
if isinstance(swap_min_sigterm_swap, tuple):
swap_term_is_percent = True
swap_min_sigterm_percent = swap_min_sigterm_swap[0]
else:
swap_term_is_percent = False
swap_min_sigterm_kb = swap_min_sigterm_swap
if isinstance(swap_min_sigkill_swap, tuple):
swap_kill_is_percent = True
swap_min_sigkill_percent = swap_min_sigkill_swap[0]
else:
swap_kill_is_percent = False
swap_min_sigkill_kb = swap_min_sigkill_swap
if isinstance(swap_min_warnings_swap, tuple):
swap_warn_is_percent = True
swap_min_warnings_percent = swap_min_warnings_swap[0]
else:
swap_warn_is_percent = False
swap_min_warnings_kb = swap_min_warnings_swap
##########################################################################
# самозащита
# повышаем приоритет
try:
os.nice(self_nice)
self_nice_result = 'OK'
except PermissionError:
self_nice_result = 'Fail'
pass
# возможность запрета самоубийства
try:
with open('/proc/self/oom_score_adj', 'w') as file:
file.write('{}\n'.format(self_oom_score_adj))
self_oom_score_adj_result = 'OK'
except PermissionError:
pass
self_oom_score_adj_result = 'Fail'
except OSError:
self_oom_score_adj_result = 'Fail'
pass
# запрет своппинга процесса
if mlockall:
from ctypes import CDLL
result = CDLL('libc.so.6', use_errno=True).mlockall(3)
if result is 0:
mla_res = 'OK'
else:
mla_res = 'Fail'
else:
mla_res = ''
if os.geteuid() == 0:
root = True
decrease_res = 'OK'
else:
root = False
decrease_res = 'Impossible'
##########################################################################
# печать конфига
if print_config:
print('\nI. STANDARD OUTPUT VERBOSITY')
print('print_config: {}'.format(print_config))
print('print_mem_check_results: {}'.format(print_mem_check_results))
print('print_sleep_periods: {}'.format(print_sleep_periods))
print('\nII. SELF-DEFENSE')
print('mlockall: {} ({})'.format(mlockall, mla_res))
print('self_nice: {} ({})'.format(
self_nice, self_nice_result
))
print('self_oom_score_adj: {} ({})'.format(
self_oom_score_adj, self_oom_score_adj_result
))
print('\nIII. MONITORING INTENSITY')
print('rate_mem: {}'.format(rate_mem))
print('rate_swap: {}'.format(rate_swap))
print('rate_zram: {}'.format(rate_zram))
print('\nIV. THRESHOLDS FOR SENDING SIGNALS')
print('mem_min_sigterm: {} MiB, {} %'.format(
round(mem_min_sigterm_mb), round(mem_min_sigterm_percent, 1)))
print('mem_min_sigkill: {} MiB, {} %'.format(
round(mem_min_sigkill_mb), round(mem_min_sigkill_percent, 1)))
print('swap_min_sigterm: {}'.format(swap_min_sigterm))
print('swap_min_sigkill: {}'.format(swap_min_sigkill))
print('zram_max_sigterm: {} MiB, {} %'.format(
round(zram_max_sigterm_mb), round(zram_max_sigterm_percent, 1)))
print('zram_max_sigkill: {} MiB, {} %'.format(
round(zram_max_sigkill_mb), round(zram_max_sigkill_percent, 1)))
print('\nV. PREVENTION OF KILLING INNOCENT VICTIMS')
print('min_delay_after_sigterm: {}'.format(min_delay_after_sigterm))
print('min_delay_after_sigkill: {}'.format(min_delay_after_sigkill))
print('oom_score_min: {}'.format(oom_score_min))
# False (OK) - OK не нужен когда фолс
print('decrease_oom_score_adj: {} ({})'.format(
decrease_oom_score_adj, decrease_res
))
if decrease_oom_score_adj:
print('oom_score_adj_max: {}'.format(oom_score_adj_max))
print('\nVI. DESKTOP NOTIFICATIONS')
print('desktop_notifications: {}'.format(desktop_notifications))
if desktop_notifications:
print('notify_options: {}'.format(notify_options))
print('root_display: {}'.format(root_display))
print('\nVII. BLACK, WHITE, AVOID AND PREFER LISTS')
print('use_regex_lists: {}'.format(use_regex_lists))
if use_regex_lists:
print('whitelist_regex: {}'.format(whitelist_regex))
print('blacklist_regex: {}'.format(blacklist_regex))
print('preferlist_regex: {}'.format(preferlist_regex))
print('preferlist_factor: {}'.format(preferlist_factor))
print('avoidlist_regex: {}'.format(avoidlist_regex))
print('avoidlist_factor: {}'.format(avoidlist_factor))
print('\nVIII. LOGGING')
print('logging: {}'.format(logging))
if logging:
print('logfile: {}'.format(logfile))
print('\nIX. LOW MEMORY WARNINGS')
print('low_memory_warnings: {}'.format(low_memory_warnings))
if low_memory_warnings:
print('min_time_between_warnings: {}'.format(min_time_between_warnings))
print('mem_min_warnings: {} MiB, {} %'.format(
round(mem_min_warnings_mb), round(mem_min_warnings_percent, 1)))
print('swap_min_warnings: {}'.format(swap_min_warnings))
print('zram_max_warnings: {} MiB, {} %'.format(
round(zram_max_warnings_mb), round(zram_max_warnings_percent, 1)))
print('\nX. EXECUTE THE COMMAND INSTEAD OF SENDING THE SIGTERM SIGNAL')
print('execute_the_command: {}'.format(execute_the_command))
if execute_the_command:
print('\nPROCESS NAME COMMAND TO EXECUTE')
for key in etc_dict:
print('{} {}'.format(key.ljust(15), etc_dict[key]))
##########################################################################
# для рассчета ширины столбцов при печати mem и zram
mem_len = len(str(round(mem_total / 1024.0)))
rate_mem = rate_mem * 1048576
rate_swap = rate_swap * 1048576
rate_zram = rate_zram * 1048576
warn_time_now = 0
warn_time_delta = 1000
warn_timer = 0
print('\nStart monitoring...')
##########################################################################
# цикл проверки уровней доступной памяти
while True:
# находим mem_available, swap_total, swap_free
with open('/proc/meminfo') as f:
for n, line in enumerate(f):
if n is 2:
mem_available = int(line.split(':')[1].strip(' kB\n'))
continue
if n is swap_total_index:
swap_total = int(line.split(':')[1].strip(' kB\n'))
continue
if n is swap_free_index:
swap_free = int(line.split(':')[1].strip(' kB\n'))
break
# если swap_min_sigkill задан в процентах
if swap_kill_is_percent:
swap_min_sigkill_kb = swap_total * swap_min_sigkill_percent / 100.0
if swap_term_is_percent:
swap_min_sigterm_kb = swap_total * swap_min_sigterm_percent / 100.0
if swap_warn_is_percent:
swap_min_warnings_kb = swap_total * swap_min_warnings_percent / 100.0
# находим MemUsedZram
disksize_sum = 0
mem_used_total_sum = 0
for dev in os.listdir('/sys/block'):
if dev.startswith('zram'):
stat = zram_stat(dev)
disksize_sum += int(stat[0])
mem_used_total_sum += int(stat[1])
mem_used_zram = (
mem_used_total_sum + disksize_sum * zram_disksize_factor
) / 1024.0
if print_mem_check_results:
# для рассчета ширины столбца свопа
swap_len = len(str(round(swap_total / 1024.0)))
# печать размеров доступной памяти
if swap_total == 0 and mem_used_zram == 0:
print('MemAvail: {} M, {} %'.format(
human(mem_available, mem_len),
just_percent_mem(mem_available / mem_total)))
elif swap_total > 0 and mem_used_zram == 0:
print('MemAvail: {} M, {} % | SwapFree: {} M, {} %'.format(
human(mem_available, mem_len),
just_percent_mem(mem_available / mem_total),
human(swap_free, swap_len),
just_percent_swap(swap_free / (swap_total + 0.1))))
else:
print('MemAvail: {} M, {} % | SwapFree: {} M, {} % | Mem' \
'UsedZram: {} M, {} %'.format(
human(mem_available, mem_len),
just_percent_mem(mem_available / mem_total),
human(swap_free, swap_len),
just_percent_swap(swap_free / (swap_total + 0.1)),
human(mem_used_zram, mem_len),
just_percent_mem(mem_used_zram / mem_total)))
# если swap_min_sigkill задан в абсолютной величине и Swap_total = 0
if swap_total > swap_min_sigkill_kb:
swap_sigkill_pc = percent(swap_min_sigkill_kb / (swap_total + 0.1))
else:
swap_sigkill_pc = '-'
if swap_total > swap_min_sigterm_kb:
swap_sigterm_pc = percent(swap_min_sigterm_kb / (swap_total + 0.1))
else:
# СТОИТ ПЕЧАТАТЬ СВОП ТОЛЬКО ПРИ SwapTotal > 0
swap_sigterm_pc = '-'
# проверка превышения порогов
# порог превышен - пытаемся предотвратить OOM
# пороги не превышены - спим
# MEM SWAP KILL
if mem_available <= mem_min_sigkill_kb and swap_free <= swap_min_sigkill_kb:
mem_info = ' MemAvailable ({} MiB, {} %) < mem_min_sigkill ({} MiB, {} %)\n Swa' \
'pFree ({} MiB, {} %) < swap_min_sigkill ({} MiB, {} %)'.format(
kib_to_mib(mem_available),
percent(mem_available / mem_total),
kib_to_mib(mem_min_sigkill_kb),
percent(mem_min_sigkill_kb / mem_total),
kib_to_mib(swap_free),
percent(swap_free / (swap_total + 0.1)),
kib_to_mib(swap_min_sigkill_kb),
swap_sigkill_pc)
find_victim_and_send_signal(9)
# ZRAM KILL
elif mem_used_zram >= zram_max_sigkill_kb:
mem_info = ' MemUsedZram ({} MiB, {} %) > zram_max_sigkill ({} MiB, {} %)'.format(
kib_to_mib(mem_used_zram),
percent(mem_used_zram / mem_total),
kib_to_mib(zram_max_sigkill_kb),
percent(zram_max_sigkill_kb / mem_total))
find_victim_and_send_signal(9)
# MEM SWAP TERM
elif mem_available <= mem_min_sigterm_kb and swap_free <= swap_min_sigterm_kb:
mem_info = ' MemAvailable ({} MiB, {} %) < mem_min_sigterm ({} MiB, {} %)\n Sw' \
'apFree ({} MiB, {} %) < swap_min_sigterm ({} MiB, {} %)'.format(
kib_to_mib(mem_available),
percent(mem_available / mem_total),
kib_to_mib(mem_min_sigterm_kb),
#percent(mem_min_sigterm_kb / mem_total),
# ОКРУГЛЯТЬ НА МЕСТЕ ВЫШЕ
round(mem_min_sigterm_percent, 1),
kib_to_mib(swap_free),
percent(swap_free / (swap_total + 0.1)),
kib_to_mib(swap_min_sigterm_kb),
swap_sigterm_pc)
find_victim_and_send_signal(15)
# ZRAM TERM
elif mem_used_zram >= zram_max_sigterm_kb:
mem_info = ' MemUsedZram ({} MiB, {} %) > zram_max_sigter' \
'm ({} M, {} %)'.format(
kib_to_mib(mem_used_zram),
percent(mem_used_zram / mem_total),
kib_to_mib(zram_max_sigterm_kb),
percent(zram_max_sigterm_kb / mem_total))
find_victim_and_send_signal(15)
# LOW MEMORY WARNINGS
elif low_memory_warnings and desktop_notifications:
if mem_available < mem_min_warnings_kb and swap_free < swap_min_warnings_kb + 0.1 or mem_used_zram > zram_max_warnings_kb:
warn_time_delta = time.time() - warn_time_now
warn_time_now = time.time()
warn_timer += warn_time_delta
if warn_timer > min_time_between_warnings:
send_notify_warn()
warn_timer = 0
sleep_after_check_mem()
# SLEEP BETWEEN MEM CHECKS
else:
sleep_after_check_mem()