diff --git a/README.md b/README.md
index 6ebe85f..7c89023 100644
--- a/README.md
+++ b/README.md
@@ -107,6 +107,19 @@ $ sudo systemctl start nohang
$ sudo systemctl enable nohang
```
+## Command line options
+
+```
+./nohang -h
+usage: nohang [-h] [-c CONFIG]
+
+optional arguments:
+ -h, --help show this help message and exit
+ -c CONFIG, --config CONFIG
+ path to the config file, default values:
+ ./nohang.conf, /etc/nohang/nohang.conf
+```
+
## How to configure nohang
The program can be configured by editing the [config file](https://github.com/hakavlad/nohang/blob/master/nohang.conf). The configuration includes the following sections:
@@ -205,13 +218,13 @@ Please create [issues](https://github.com/hakavlad/nohang/issues). Use cases, fe
- [x] Handle all timeouts when notify-send starts
- [x] Fix conf parsing: use of `line.partition('=')` instead of `line.split('=')`
- [x] Add `oom-sort`
- - [x] Reduce memory usage (remove `import argparse`)
+ - [x] Reduce memory usage and startup time (using `sys.argv` instead of `argparse`)
- [x] Remove CLI options (need to add it again via `sys.argv`)
- [x] Remove self-defense options from config, use systemd unit scheduling instead
- [x] Add the ability to send any signal instead of SIGTERM for processes with certain names
- [x] Handle `UnicodeDecodeError` if victim name consists of many unicode characters
- [x] Fix `mlockall()` using `MCL_ONFAULT` and lock all memory by default
- - [ ] Add `PSI` support (using `/proc/pressure/memory`, need Linux 4.20+)
+ - [x] Add initial support for `PSI` (using `/proc/pressure/memory`, need Linux 4.20+)
- [ ] Redesign of the config
- [ ] Decrease CPU usage: ignore `zram` by default
- [ ] Improve user input validation
@@ -221,5 +234,4 @@ Please create [issues](https://github.com/hakavlad/nohang/issues). Use cases, fe
- [x] Fix: replace `re.fullmatch()` by `re.search()`
- [ ] Validation RE patterns at startup
-- [v0.1](https://github.com/hakavlad/nohang/releases/tag/v0.1), 2018-11-23
- - 1st release
+- [v0.1](https://github.com/hakavlad/nohang/releases/tag/v0.1), 2018-11-23: Initial release
diff --git a/nohang b/nohang
index 3e42090..ccfd00b 100755
--- a/nohang
+++ b/nohang
@@ -4,9 +4,48 @@ import os
from ctypes import CDLL
from time import sleep, time
from operator import itemgetter
-from sys import stdout
+from sys import stdout, stderr, argv, exit
from signal import SIGKILL, SIGTERM
+
+help_mess = """usage: nohang [-h] [-c CONFIG]
+
+optional arguments:
+ -h, --help show this help message and exit
+ -c CONFIG, --config CONFIG
+ path to the config file, default values:
+ ./nohang.conf, /etc/nohang/nohang.conf"""
+
+
+if len(argv) == 1:
+ if os.path.exists('./nohang.conf'):
+ config = cd = os.getcwd() + '/nohang.conf'
+ else:
+ config = '/etc/nohang/nohang.conf'
+
+elif len(argv) == 2:
+ if argv[1] == '--help' or argv[1] == '-h':
+ print(help_mess)
+ exit(1)
+ else:
+ print('Invalid CLI input')
+ exit(1)
+
+elif len(argv) > 3:
+ print('Invalid CLI input')
+ exit(1)
+
+else:
+ if argv[1] == '--config' or argv[1] == '-c':
+ config = argv[2]
+ else:
+ print('Invalid option: {}'.format(argv[1]))
+ exit(1)
+
+
+conf_err_mess = 'Invalid config. Exit.'
+
+
start_time = time()
sig_dict = {SIGKILL: 'SIGKILL',
@@ -40,6 +79,7 @@ HR = '~' * 79
# todo: make config option
print_total_stat = True
+
##########################################################################
# define functions
@@ -194,7 +234,7 @@ def conf_parse_string(param):
else:
print('All the necessary parameters must be in the config')
print('There is no "{}" parameter in the config'.format(param))
- exit()
+ exit(1)
def conf_parse_bool(param):
@@ -211,14 +251,14 @@ def conf_parse_bool(param):
elif param_str == 'False':
return False
else:
- print('Invalid value of the "{}" parameter.'.format(param_str))
+ print('Invalid value of the "{}" parameter.'.format(param))
print('Valid values are True and False.')
print('Exit')
- exit()
+ exit(1)
else:
print('All the necessary parameters must be in the config')
- print('There is no "{}" parameter in the config'.format(param_str))
- exit()
+ print('There is no "{}" parameter in the config'.format(param))
+ exit(1)
def rline1(path):
@@ -325,6 +365,9 @@ def pid_to_uid(pid):
return f_list[uid_index].split('\t')[2]
+
+
+
def notify_send_wait(title, body):
'''GUI notifications with UID != 0'''
with Popen(['notify-send', '--icon=dialog-warning', title, body]) as proc:
@@ -338,8 +381,6 @@ def notify_send_wait(title, body):
def notify_helper(title, body):
'''GUI notification with UID = 0'''
- # os.system(notify_helper_path + ' foo bar &')
-
with Popen([notify_helper_path, title, body]) as proc:
try:
proc.wait(timeout=wait_time)
@@ -350,12 +391,16 @@ def notify_helper(title, body):
title, body))
+
+
+
def send_notify_warn():
"""
Look for process with maximum 'badness' and warn user with notification.
(implement Low memory warnings)
"""
+ '''
# find process with max badness
fat_tuple = fattest()
pid = fat_tuple[0]
@@ -376,7 +421,14 @@ def send_notify_warn():
# title = 'Low memory: {}'.format(low_mem_percent)
title = 'Low memory'
+ '''
+
+
+
+
+
+ '''
body = 'Next victim: {}[{}]'.format(
name.replace(
# symbol '&' can break notifications in some themes,
@@ -384,6 +436,13 @@ def send_notify_warn():
'&', '*'),
pid
)
+ '''
+
+
+ '''
+ body = 'MemAvail: {}%\nSwapFree: {}%'.format(
+ round(mem_available / mem_total * 100),
+ round(swap_free / (swap_total + 0.1) * 100))
if root: # If nohang was started by root
# send notification to all active users with special script
@@ -391,6 +450,22 @@ def send_notify_warn():
else: # Or by regular user
# send notification to user that runs this nohang
notify_send_wait(title, body)
+ '''
+
+
+ b = """{} 'Low memory' 'MemAvail: {}%\nSwapFree: {}%' &""".format(
+ notify_helper_path,
+ round(mem_available / mem_total * 100),
+ round(swap_free / (swap_total + 0.1) * 100)
+ )
+
+ t0 = time()
+ os.system(b)
+ t1 = time()
+ print('t:', t1 - t0)
+
+
+
def send_notify(signal, name, pid):
@@ -402,13 +477,14 @@ def send_notify(signal, name, pid):
pid: str process pid
"""
title = 'Freeze prevention'
- body = '{} {}[{}]'.format(
+ body = '{} [{}] {}'.format(
notify_sig_dict[signal],
+ pid,
name.replace(
# symbol '&' can break notifications in some themes,
# therefore it is replaced by '*'
- '&', '*'),
- pid
+ '&', '*'
+ )
)
if root:
# send notification to all active users with notify-send
@@ -899,7 +975,7 @@ def sleep_after_check_mem():
stdout.flush()
sleep(t)
except KeyboardInterrupt:
- exit()
+ exit(1)
def calculate_percent(arg_key):
@@ -922,12 +998,12 @@ def calculate_percent(arg_key):
mem_min_percent = string_to_float_convert_test(mem_min_percent)
if mem_min_percent is None:
print('Invalid {} value, not float\nExit'.format(arg_key))
- exit()
+ exit(1)
# Final validations...
if mem_min_percent < 0 or mem_min_percent > 100:
print(
'{}, as percents value, out of range [0; 100]\nExit'.format(arg_key))
- exit()
+ exit(1)
# mem_min_sigterm_percent is clean and valid float percentage. Can
# translate into Kb
@@ -938,14 +1014,14 @@ def calculate_percent(arg_key):
mem_min_mb = string_to_float_convert_test(mem_min[:-1].strip())
if mem_min_mb is None:
print('Invalid {} value, not float\nExit'.format(arg_key))
- exit()
+ exit(1)
mem_min_kb = mem_min_mb * 1024
if mem_min_kb > mem_total:
print(
'{} value can not be greater then MemTotal ({} MiB)\nExit'.format(
arg_key, round(
mem_total / 1024)))
- exit()
+ exit(1)
mem_min_percent = mem_min_kb / mem_total * 100
else:
@@ -982,7 +1058,7 @@ for s in mem_list:
if mem_list_names[2] != 'MemAvailable':
print('Your Linux kernel is too old, Linux 3.14+ requied\nExit')
- exit()
+ exit(1)
swap_total_index = mem_list_names.index('SwapTotal')
swap_free_index = swap_total_index + 1
@@ -1003,6 +1079,8 @@ vm_size_index = status_names.index('VmSize')
vm_rss_index = status_names.index('VmRSS')
vm_swap_index = status_names.index('VmSwap')
uid_index = status_names.index('Uid')
+state_index = status_names.index('State')
+
try:
anon_index = status_names.index('RssAnon')
@@ -1023,7 +1101,7 @@ cd = os.getcwd()
'''
-config = '/etc/nohang/nohang.conf'
+#config = '/etc/nohang/nohang.conf'
# config = 'nohang.conf'
@@ -1075,7 +1153,7 @@ try:
if len(etc_name) > 15:
print('Invalid config, the length of the process '
'name must not exceed 15 characters\nExit')
- exit()
+ exit(1)
etc_dict[etc_name] = etc_command
# NEED VALIDATION!
@@ -1095,16 +1173,21 @@ try:
except PermissionError:
print('PermissionError', conf_err_mess)
- exit()
+ exit(1)
except UnicodeDecodeError:
print('UnicodeDecodeError', conf_err_mess)
- exit()
+ exit(1)
except IsADirectoryError:
print('IsADirectoryError', conf_err_mess)
- exit()
+ exit(1)
except IndexError:
print('IndexError', conf_err_mess)
- exit()
+ exit(1)
+except FileNotFoundError:
+ print('FileNotFoundError', conf_err_mess)
+ exit(1)
+
+
# print(processname_re_list)
# print(cmdline_re_list)
@@ -1158,53 +1241,53 @@ 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()
+ exit(1)
if rate_mem <= 0:
print('rate_mem MUST be > 0\nExit')
- exit()
+ exit(1)
else:
print('rate_mem not in config\nExit')
- exit()
+ exit(1)
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()
+ exit(1)
if rate_swap <= 0:
print('rate_swap MUST be > 0\nExit')
- exit()
+ exit(1)
else:
print('rate_swap not in config\nExit')
- exit()
+ exit(1)
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()
+ exit(1)
if rate_zram <= 0:
print('rate_zram MUST be > 0\nExit')
- exit()
+ exit(1)
else:
print('rate_zram not in config\nExit')
- exit()
+ exit(1)
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()
+ exit(1)
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()
+ exit(1)
if 'min_delay_after_sigterm' in config_dict:
@@ -1212,13 +1295,13 @@ if 'min_delay_after_sigterm' in config_dict:
config_dict['min_delay_after_sigterm'])
if min_delay_after_sigterm is None:
print('Invalid min_delay_after_sigterm value, not float\nExit')
- exit()
+ exit(1)
if min_delay_after_sigterm < 0:
print('min_delay_after_sigterm must be positiv\nExit')
- exit()
+ exit(1)
else:
print('min_delay_after_sigterm not in config\nExit')
- exit()
+ exit(1)
if 'min_delay_after_sigkill' in config_dict:
@@ -1226,13 +1309,13 @@ if 'min_delay_after_sigkill' in config_dict:
config_dict['min_delay_after_sigkill'])
if min_delay_after_sigkill is None:
print('Invalid min_delay_after_sigkill value, not float\nExit')
- exit()
+ exit(1)
if min_delay_after_sigkill < 0:
print('min_delay_after_sigkill must be positive\nExit')
- exit()
+ exit(1)
else:
print('min_delay_after_sigkill not in config\nExit')
- exit()
+ exit(1)
if 'psi_avg10_sleep_time' in config_dict:
@@ -1240,13 +1323,13 @@ if 'psi_avg10_sleep_time' in config_dict:
config_dict['psi_avg10_sleep_time'])
if psi_avg10_sleep_time is None:
print('Invalid psi_avg10_sleep_time value, not float\nExit')
- exit()
+ exit(1)
if psi_avg10_sleep_time < 0:
print('psi_avg10_sleep_time must be positive\nExit')
- exit()
+ exit(1)
else:
print('psi_avg10_sleep_time not in config\nExit')
- exit()
+ exit(1)
if 'sigkill_psi_avg10' in config_dict:
@@ -1254,13 +1337,13 @@ if 'sigkill_psi_avg10' in config_dict:
config_dict['sigkill_psi_avg10'])
if sigkill_psi_avg10 is None:
print('Invalid sigkill_psi_avg10 value, not float\nExit')
- exit()
+ exit(1)
if sigkill_psi_avg10 < 0 or sigkill_psi_avg10 > 100:
print('sigkill_psi_avg10 must be in the range [0; 100]\nExit')
- exit()
+ exit(1)
else:
print('sigkill_psi_avg10 not in config\nExit')
- exit()
+ exit(1)
if 'sigterm_psi_avg10' in config_dict:
@@ -1268,13 +1351,13 @@ if 'sigterm_psi_avg10' in config_dict:
config_dict['sigterm_psi_avg10'])
if sigterm_psi_avg10 is None:
print('Invalid sigterm_psi_avg10 value, not float\nExit')
- exit()
+ exit(1)
if sigterm_psi_avg10 < 0 or sigterm_psi_avg10 > 100:
print('sigterm_psi_avg10 must be in the range [0; 100]\nExit')
- exit()
+ exit(1)
else:
print('sigterm_psi_avg10 not in config\nExit')
- exit()
+ exit(1)
if 'min_badness' in config_dict:
@@ -1282,13 +1365,13 @@ if 'min_badness' in config_dict:
config_dict['min_badness'])
if min_badness is None:
print('Invalid min_badness value, not integer\nExit')
- exit()
+ exit(1)
if min_badness < 0 or min_badness > 1000:
print('Invalud min_badness value\nExit')
- exit()
+ exit(1)
else:
print('min_badness not in config\nExit')
- exit()
+ exit(1)
if 'oom_score_adj_max' in config_dict:
@@ -1296,13 +1379,13 @@ if 'oom_score_adj_max' in config_dict:
config_dict['oom_score_adj_max'])
if oom_score_adj_max is None:
print('Invalid oom_score_adj_max value, not integer\nExit')
- exit()
+ exit(1)
if oom_score_adj_max < 0 or oom_score_adj_max > 1000:
print('Invalid oom_score_adj_max value\nExit')
- exit()
+ exit(1)
else:
print('oom_score_adj_max not in config\nExit')
- exit()
+ exit(1)
if 'min_time_between_warnings' in config_dict:
@@ -1310,20 +1393,20 @@ if 'min_time_between_warnings' in config_dict:
config_dict['min_time_between_warnings'])
if min_time_between_warnings is None:
print('Invalid min_time_between_warnings value, not float\nExit')
- exit()
+ exit(1)
if min_time_between_warnings < 1 or min_time_between_warnings > 300:
print('min_time_between_warnings value out of range [1; 300]\nExit')
- exit()
+ exit(1)
else:
print('min_time_between_warnings not in config\nExit')
- exit()
+ exit(1)
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()
+ exit(1)
##########################################################################
@@ -1344,12 +1427,12 @@ def get_swap_threshold_tuple(string):
valid = string_to_float_convert_test(string[:-1])
if valid is None:
print('somewhere swap unit is not float_%')
- exit()
+ exit(1)
value = float(string[:-1].strip())
if value < 0 or value > 100:
print('invalid value, must be from the range[0; 100] %')
- exit()
+ exit(1)
return value, True
@@ -1357,18 +1440,18 @@ def get_swap_threshold_tuple(string):
valid = string_to_float_convert_test(string[:-1])
if valid is None:
print('somewhere swap unit is not float_M')
- exit()
+ exit(1)
value = float(string[:-1].strip()) * 1024
if value < 0:
print('invalid unit in config (negative value)')
- exit()
+ exit(1)
return value, False
else:
print('Invalid config file. There are invalid units somewhere\nExit')
- exit()
+ exit(1)
swap_min_sigterm_tuple = get_swap_threshold_tuple(swap_min_sigterm)
diff --git a/nohang.service b/nohang.service
index 0a59b8d..bf645fa 100644
--- a/nohang.service
+++ b/nohang.service
@@ -4,7 +4,7 @@ After=sysinit.target
Documentation=man:nohang(1) https://github.com/hakavlad/nohang
[Service]
-ExecStart=/usr/sbin/nohang
+ExecStart=/usr/sbin/nohang --config /etc/nohang/nohang.conf
Slice=nohang.slice
Restart=always
ProtectSystem=strict