pwr_tray 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pwr_tray/IniTool.py +193 -0
- pwr_tray/SwayIdleMgr.py +139 -0
- pwr_tray/Utils.py +162 -0
- pwr_tray/YamlDump.py +163 -0
- pwr_tray/__init__.py +0 -0
- pwr_tray/main.py +974 -0
- pwr_tray/resources/FullSun-v03.svg +81 -0
- pwr_tray/resources/GoingDown-v03.svg +79 -0
- pwr_tray/resources/PlayingNow-v03.svg +63 -0
- pwr_tray/resources/RisingMoon-v03.svg +75 -0
- pwr_tray/resources/SetA/pwr-inh-v03.svg +1 -0
- pwr_tray/resources/SetA/pwr-no-sleep-v03.svg +1 -0
- pwr_tray/resources/SetA/pwr-uninh-v03.svg +1 -0
- pwr_tray/resources/SetB/LoBattery-v03.svg +698 -0
- pwr_tray/resources/SetB/LockOnlyMode-v03.svg +18 -0
- pwr_tray/resources/SetB/NormMode-v03.svg +645 -0
- pwr_tray/resources/SetC/New-LockOnly-v03.svg +13 -0
- pwr_tray/resources/SetC/New-LowBattery-v03.svg +13 -0
- pwr_tray/resources/SetC/New-NormMode-v03.svg +13 -0
- pwr_tray/resources/SetC/New-PresMode-v03.svg +13 -0
- pwr_tray/resources/SettingSun-v03.svg +71 -0
- pwr_tray/resources/StopSign-v03.svg +90 -0
- pwr_tray/resources/Unlocked-v03.svg +116 -0
- pwr_tray/resources/UnlockedMoon-v03.svg +121 -0
- pwr_tray/resources/__init__.py +0 -0
- pwr_tray/resources/lockpaper.png +0 -0
- pwr_tray-1.0.2.dist-info/METADATA +169 -0
- pwr_tray-1.0.2.dist-info/RECORD +31 -0
- pwr_tray-1.0.2.dist-info/WHEEL +4 -0
- pwr_tray-1.0.2.dist-info/entry_points.txt +3 -0
- pwr_tray-1.0.2.dist-info/licenses/LICENSE +21 -0
pwr_tray/IniTool.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
TBD
|
|
5
|
+
"""
|
|
6
|
+
# pylint: disable=invalid-name, broad-exception-caught
|
|
7
|
+
# pylint: disable=too-many-branches,too-many-statements
|
|
8
|
+
# pylint: disable=too-many-instance-attributes
|
|
9
|
+
# pylint: disable=consider-using-from-import
|
|
10
|
+
# pylint: disable=
|
|
11
|
+
# pylint: disable=
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import configparser
|
|
15
|
+
from types import SimpleNamespace
|
|
16
|
+
import copy
|
|
17
|
+
import json
|
|
18
|
+
from pwr_tray.Utils import prt
|
|
19
|
+
|
|
20
|
+
class IniTool:
|
|
21
|
+
""" Configued Params for this class"""
|
|
22
|
+
def __init__(self, paths_only=False):
|
|
23
|
+
self.defaults = {
|
|
24
|
+
'Settings': {
|
|
25
|
+
'i3lock_args': '-t -i ./lockpaper.png',
|
|
26
|
+
'swaylock_args': '-i ./lockpaper.png',
|
|
27
|
+
'debug_mode': False,
|
|
28
|
+
'power_down': False,
|
|
29
|
+
'turn_off_monitors': False,
|
|
30
|
+
'lock_min_list': '[15, 30]',
|
|
31
|
+
'sleep_min_list': '[5, 30]',
|
|
32
|
+
'lo_battery_pct': 10,
|
|
33
|
+
'gui_editor': 'geany',
|
|
34
|
+
# 'dim_pct_brightness': 100,
|
|
35
|
+
# 'dim_pct_lock_min': 100,
|
|
36
|
+
|
|
37
|
+
}, 'HiBattery': { #was OnBattery
|
|
38
|
+
'power_down': False,
|
|
39
|
+
'lock_min_list': '[10, 20]',
|
|
40
|
+
'sleep_min_list': '[1, 10]',
|
|
41
|
+
# 'dim_pct_brightness': 50,
|
|
42
|
+
# 'dim_pct_lock_min': 70,
|
|
43
|
+
|
|
44
|
+
}, 'LoBattery': {
|
|
45
|
+
'power_down': True,
|
|
46
|
+
'lock_min_list': '[1]',
|
|
47
|
+
'sleep_min_list': '[1]',
|
|
48
|
+
# 'dim_pct_brightness': 50,
|
|
49
|
+
# 'dim_pct_lock_min': 70,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
self.folder = os.path.expanduser("~/.config/pwr-tray")
|
|
53
|
+
self.ini_path = os.path.join(self.folder, "config.ini")
|
|
54
|
+
self.log_path = os.path.join(self.folder, "debug.log")
|
|
55
|
+
self.picks_path = os.path.join(self.folder, "picks.json")
|
|
56
|
+
self.config = configparser.ConfigParser()
|
|
57
|
+
self.last_mod_time = None
|
|
58
|
+
self.section_params = {'Settings': {}, 'HiBattery': {}, 'LoBattery': {}, }
|
|
59
|
+
self.params_by_selector = {}
|
|
60
|
+
if not paths_only:
|
|
61
|
+
self.ensure_ini_file()
|
|
62
|
+
os.chdir(self.folder)
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def get_selectors():
|
|
66
|
+
""" Returns the in right "order" """
|
|
67
|
+
return 'Settings HiBattery LoBattery'.split()
|
|
68
|
+
|
|
69
|
+
def the_default(self, selector, key):
|
|
70
|
+
""" return the default value given the selector and key """
|
|
71
|
+
return self.defaults[selector][key]
|
|
72
|
+
|
|
73
|
+
def get_current_vals(self, selector, list_name):
|
|
74
|
+
""" Expecting a list of two or more non-zero ints """
|
|
75
|
+
if selector in self.params_by_selector and hasattr(self.params_by_selector[selector], list_name):
|
|
76
|
+
vals = getattr(self.params_by_selector[selector], list_name)
|
|
77
|
+
if isinstance(vals, list) and len(vals) >= 2:
|
|
78
|
+
return vals
|
|
79
|
+
return self.the_default(selector, list_name) # should not get here
|
|
80
|
+
|
|
81
|
+
def get_rotated_vals(self, selector, list_name, first):
|
|
82
|
+
""" TBD """
|
|
83
|
+
vals = self.get_current_vals(selector, list_name)
|
|
84
|
+
if first in vals:
|
|
85
|
+
while vals[0] != first:
|
|
86
|
+
vals = vals[1:] + vals[:1]
|
|
87
|
+
setattr(self.params_by_selector[selector], list_name, vals)
|
|
88
|
+
return vals
|
|
89
|
+
|
|
90
|
+
def ensure_ini_file(self):
|
|
91
|
+
"""Check if the config file exists, create it if not."""
|
|
92
|
+
if not os.path.exists(self.folder):
|
|
93
|
+
os.makedirs(self.folder, exist_ok=True)
|
|
94
|
+
if not os.path.exists(self.ini_path):
|
|
95
|
+
self.config.read_dict(self.defaults)
|
|
96
|
+
with open(self.ini_path, 'w', encoding='utf-8') as configfile:
|
|
97
|
+
self.config.write(configfile)
|
|
98
|
+
|
|
99
|
+
def update_config(self):
|
|
100
|
+
""" Check if the file has been modified since the last read """
|
|
101
|
+
def to_array(val_str):
|
|
102
|
+
# Expecting string of form: "[1,2,...]" or just "20"
|
|
103
|
+
try:
|
|
104
|
+
vals = json.loads(val_str)
|
|
105
|
+
except Exception:
|
|
106
|
+
return None
|
|
107
|
+
if isinstance(vals, int):
|
|
108
|
+
vals = [vals]
|
|
109
|
+
if not isinstance(vals, list):
|
|
110
|
+
return None
|
|
111
|
+
rvs = []
|
|
112
|
+
for val in vals:
|
|
113
|
+
if isinstance(val, int) and val > 0:
|
|
114
|
+
rvs.append(val)
|
|
115
|
+
if not rvs:
|
|
116
|
+
return None
|
|
117
|
+
if len(rvs) == 1: # always want two
|
|
118
|
+
rvs.append(vals[0])
|
|
119
|
+
return rvs
|
|
120
|
+
|
|
121
|
+
current_mod_time = os.path.getmtime(self.ini_path)
|
|
122
|
+
if current_mod_time == self.last_mod_time:
|
|
123
|
+
return False # not updated
|
|
124
|
+
# Re-read the configuration file if it has changed
|
|
125
|
+
self.config.read(self.ini_path)
|
|
126
|
+
self.last_mod_time = current_mod_time
|
|
127
|
+
|
|
128
|
+
goldens = self.defaults['Settings']
|
|
129
|
+
running = goldens
|
|
130
|
+
all_params = {}
|
|
131
|
+
|
|
132
|
+
# Access the configuration values in order
|
|
133
|
+
prt('parsing config.ini...')
|
|
134
|
+
for selector in self.get_selectors():
|
|
135
|
+
all_params[selector] = params = copy.deepcopy(running)
|
|
136
|
+
if selector not in self.config:
|
|
137
|
+
all_params[selector] = SimpleNamespace(**params)
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
# iterate the candidates
|
|
141
|
+
candidates = dict(self.config[selector])
|
|
142
|
+
for key, value in candidates.items():
|
|
143
|
+
if key not in goldens:
|
|
144
|
+
prt(f'skip {selector}.{key}: {value!r} [unknown key]')
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
if key.endswith('_list'):
|
|
148
|
+
list_value = to_array(value)
|
|
149
|
+
if not value:
|
|
150
|
+
params[key] = self.the_default(selector, key)
|
|
151
|
+
prt(f'skip {selector}.{key}: {value!r} [bad list spec]')
|
|
152
|
+
else:
|
|
153
|
+
params[key] = list_value
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
if isinstance(goldens[key], bool):
|
|
157
|
+
if isinstance(value, str):
|
|
158
|
+
if value.lower() == 'true':
|
|
159
|
+
value = True
|
|
160
|
+
elif value.lower() == 'false':
|
|
161
|
+
value = False
|
|
162
|
+
if isinstance(value, bool):
|
|
163
|
+
params[key] = value
|
|
164
|
+
else:
|
|
165
|
+
params[key] = self.the_default(selector, key)
|
|
166
|
+
prt(f'skip {selector}.{key}: {value!r} [expecting bool]')
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
if isinstance(goldens[key], int):
|
|
170
|
+
try:
|
|
171
|
+
params[key] = int(value)
|
|
172
|
+
continue
|
|
173
|
+
except Exception:
|
|
174
|
+
params[key] = self.the_default(selector, key)
|
|
175
|
+
prt(f'skip {selector}.{key}: {value!r} [expecting int repr]')
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
if isinstance(goldens[key], str):
|
|
179
|
+
if isinstance(value, str):
|
|
180
|
+
params[key] = value
|
|
181
|
+
else:
|
|
182
|
+
params[key] = self.the_default(selector, key)
|
|
183
|
+
prt(f'skip {selector}.{key}: {value!r} [expecting string]')
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
assert False, f'unhandled goldens[{key}]: {value!r}'
|
|
187
|
+
all_params[selector] = SimpleNamespace(**params)
|
|
188
|
+
|
|
189
|
+
self.params_by_selector = all_params
|
|
190
|
+
|
|
191
|
+
prt('DONE parsing config.ini...')
|
|
192
|
+
|
|
193
|
+
return True # updated
|
pwr_tray/SwayIdleMgr.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Sway does not provide a way to get the idle time, and so the fundamental design
|
|
5
|
+
of pwr-tray using idle time does nto work. Instead, for sway, we must basically
|
|
6
|
+
set up swayidle to run our config, and if there are special actions like
|
|
7
|
+
blank now, we have to kill the running swayidle and start one that does what
|
|
8
|
+
we want.
|
|
9
|
+
"""
|
|
10
|
+
# pylint: disable=invalid-name,consider-using-with
|
|
11
|
+
import subprocess
|
|
12
|
+
import time
|
|
13
|
+
import os
|
|
14
|
+
import signal
|
|
15
|
+
from types import SimpleNamespace
|
|
16
|
+
from pwr_tray.Utils import prt
|
|
17
|
+
|
|
18
|
+
class SwayIdleManager:
|
|
19
|
+
""" Class to manage 'swayidle' """
|
|
20
|
+
def __init__(self, applet):
|
|
21
|
+
self.process = None
|
|
22
|
+
self.applet = applet
|
|
23
|
+
self.current_cmd = ''
|
|
24
|
+
# we construct the sway idle from these clauses which various
|
|
25
|
+
# substitutions.
|
|
26
|
+
self.clauses = SimpleNamespace(
|
|
27
|
+
leader="""exec swayidle""",
|
|
28
|
+
locker=""" timeout [lock_s] '[screenlock] [lockopts]'""",
|
|
29
|
+
blanker=""" timeout [blank_s] 'swaymsg "output * dpms off"'""",
|
|
30
|
+
sleeper=""" timeout [sleep_s] 'systemctl suspend'""",
|
|
31
|
+
# dimmer="""\\\n timeout [dim_s] 'brightnessctl set 50%'""", # perms?
|
|
32
|
+
before_sleep=""" before-sleep '[screenlock] [lockopts]'""",
|
|
33
|
+
after_resume=""" after-resume '[unblank]'""",
|
|
34
|
+
# + """ 'pgrep -x copyq || copyq --start-server hide;"""
|
|
35
|
+
# + """ pgrep -x nm-applet || nm-applet [undim][dpmsOn]'""",
|
|
36
|
+
# + """ pgrep -x nm-applet || nm-applet [unblank]'""",
|
|
37
|
+
# undim = """; brightnessctl set 100%""",
|
|
38
|
+
screenlock = """pkill swaylock ; exec swaylock --ignore-empty-password --show-failed-attempts""",
|
|
39
|
+
unblank='''; swaymsg "output * dpms on"''',
|
|
40
|
+
)
|
|
41
|
+
self.kill_other_swayidle()
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def kill_other_swayidle():
|
|
45
|
+
""" Kills any stray swayidles"""
|
|
46
|
+
try:
|
|
47
|
+
pids = subprocess.check_output(['pgrep', 'swayidle']).decode().split()
|
|
48
|
+
if pids:
|
|
49
|
+
subprocess.run(['pkill', 'swayidle'])
|
|
50
|
+
time.sleep(2) # Wait a moment to ensure processes are terminated
|
|
51
|
+
|
|
52
|
+
pids = subprocess.check_output(['pgrep', 'swayidle']).decode().split()
|
|
53
|
+
if pids:
|
|
54
|
+
prt(f"Force killing remaining swayidle processes... {pids}")
|
|
55
|
+
for pid in pids:
|
|
56
|
+
os.kill(int(pid), signal.SIGKILL)
|
|
57
|
+
return
|
|
58
|
+
except Exception:
|
|
59
|
+
return # none left
|
|
60
|
+
|
|
61
|
+
def build_cmd(self, mode=None):
|
|
62
|
+
""" Build the swayidle command line from the current statue. """
|
|
63
|
+
|
|
64
|
+
# lock_s, lockopts, sleep_s, blank_s, dim_s = None, '', None, None, None
|
|
65
|
+
lock_s, lockopts, sleep_s, blank_s = None, '', None, None
|
|
66
|
+
mode = mode if mode else self.applet.get_effective_mode()
|
|
67
|
+
til_sleep_s, sleeping = None, False
|
|
68
|
+
|
|
69
|
+
lockopts = self.applet.get_params().swaylock_args
|
|
70
|
+
quick = self.applet.quick
|
|
71
|
+
a_minute = 30 if quick else 60
|
|
72
|
+
|
|
73
|
+
if mode in ('LockOnly', 'SleepAfterLock'):
|
|
74
|
+
lock_s = self.applet.get_lock_min_list()[0] * a_minute
|
|
75
|
+
til_sleep_s = lock_s
|
|
76
|
+
if self.applet.get_params().turn_off_monitors:
|
|
77
|
+
blank_s = 20 + lock_s
|
|
78
|
+
if mode in ('SleepAfterLock', ):
|
|
79
|
+
sleep_s = self.applet.get_sleep_min_list()[0] * a_minute
|
|
80
|
+
til_sleep_s = sleep_s
|
|
81
|
+
sleeping = True
|
|
82
|
+
|
|
83
|
+
til_sleep_s, blanking = 0, False
|
|
84
|
+
cmd = self.clauses.leader
|
|
85
|
+
if isinstance(lock_s, (int,float)) and lock_s >= 0:
|
|
86
|
+
til_sleep_s += lock_s
|
|
87
|
+
cmd += self.clauses.locker.replace(
|
|
88
|
+
"[lock_s]", str(lock_s)).replace(
|
|
89
|
+
'[lockopts]', lockopts).replace(
|
|
90
|
+
'[screenlock]', self.clauses.screenlock)
|
|
91
|
+
if sleeping:
|
|
92
|
+
til_sleep_s += sleep_s
|
|
93
|
+
cmd += self.clauses.sleeper.replace("[sleep_s]", str(til_sleep_s))
|
|
94
|
+
if blank_s is not None:
|
|
95
|
+
blanking = True
|
|
96
|
+
cmd += self.clauses.blanker.replace('[blank_s]', str(blank_s))
|
|
97
|
+
# if isinstance(dim_s, (int,float)) and dim_s >= 0:
|
|
98
|
+
# dimming = True
|
|
99
|
+
# cmd += self.clauses.dimmer.replace('[dim_s]', str(dim_s))
|
|
100
|
+
cmd += self.clauses.before_sleep.replace(
|
|
101
|
+
"[sleep_s]", str(til_sleep_s)).replace(
|
|
102
|
+
'[lockopts]', lockopts).replace(
|
|
103
|
+
'[screenlock]', self.clauses.screenlock)
|
|
104
|
+
# cmd += self.clauses.after_resume.replace(
|
|
105
|
+
# "[undim]", self.clauses.undim if dimming else '')
|
|
106
|
+
if blanking:
|
|
107
|
+
cmd += self.clauses.after_resume.replace(
|
|
108
|
+
"[unblank]", self.clauses.unblank if blanking else '')
|
|
109
|
+
|
|
110
|
+
rv, self.current_cmd = bool(cmd != self.current_cmd), cmd
|
|
111
|
+
if rv:
|
|
112
|
+
prt('NEW-SWAYIDLE:', self.current_cmd)
|
|
113
|
+
|
|
114
|
+
return rv # whether updated
|
|
115
|
+
|
|
116
|
+
def start(self):
|
|
117
|
+
""" Build and start the command from the current state """
|
|
118
|
+
updated = self.build_cmd()
|
|
119
|
+
if self.process and updated:
|
|
120
|
+
self.stop()
|
|
121
|
+
if updated:
|
|
122
|
+
prt(f'SWAYIDLE: {self.current_cmd}')
|
|
123
|
+
|
|
124
|
+
if not self.process and self.current_cmd:
|
|
125
|
+
self.process = subprocess.Popen(self.current_cmd, shell=True)
|
|
126
|
+
|
|
127
|
+
return self.process
|
|
128
|
+
|
|
129
|
+
def stop(self):
|
|
130
|
+
""" Stop the current swayidle (normally to replace it) """
|
|
131
|
+
self.process.terminate()
|
|
132
|
+
self.process.wait()
|
|
133
|
+
self.process = None
|
|
134
|
+
|
|
135
|
+
def checkup(self):
|
|
136
|
+
""" Check whether swayidle is running, normally to restart it
|
|
137
|
+
with the current command. """
|
|
138
|
+
if self.process.poll() is not None:
|
|
139
|
+
self.start()
|
pwr_tray/Utils.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Utility functions
|
|
5
|
+
"""
|
|
6
|
+
# pylint: disable=invalid-name,broad-exception-caught
|
|
7
|
+
# pylint: disable=consider-using-with,global-statement
|
|
8
|
+
# pylint: disable=too-few-public-methods
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import stat
|
|
13
|
+
import time
|
|
14
|
+
import signal
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import inspect
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from io import StringIO
|
|
20
|
+
|
|
21
|
+
import importlib.resources as pkg_resources
|
|
22
|
+
# from pathlib import Path
|
|
23
|
+
|
|
24
|
+
prt_kb = 512
|
|
25
|
+
prt_path = ''
|
|
26
|
+
prt_to_init = True
|
|
27
|
+
|
|
28
|
+
def copy_to_folder(resource_name, dest):
|
|
29
|
+
""" Get the path of a resource """
|
|
30
|
+
with pkg_resources.path('pwr_tray.resources', resource_name) as file_path:
|
|
31
|
+
shutil.copy(file_path, os.path.join(dest, resource_name))
|
|
32
|
+
return file_path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def where(above=0):
|
|
36
|
+
"""Get the file and line of the caller. Arguments:
|
|
37
|
+
- above -- how many frames to go up (or down) from the reference
|
|
38
|
+
frame (which is 2 above). above=0 means the caller of the
|
|
39
|
+
caller of the function (which is the frame ofkjk interest usually).
|
|
40
|
+
Then keep going up until we get out of this file if possible
|
|
41
|
+
Returns:
|
|
42
|
+
[file:line]
|
|
43
|
+
"""
|
|
44
|
+
stack, frameNo = inspect.stack(), 2 + above
|
|
45
|
+
if frameNo < 0 or len(stack) < frameNo + 2:
|
|
46
|
+
return '[n/a]'
|
|
47
|
+
filename, line_number = stack[frameNo][1:3]
|
|
48
|
+
# lg.db(f'YES {os.path.basename(filename)}:{line_number}')
|
|
49
|
+
return f'[{filename.split("/")[-1]}:{line_number}]'
|
|
50
|
+
# return f'[:{line_number}]'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def prt(*args, **kwargs):
|
|
54
|
+
""" Our custom print routine ...
|
|
55
|
+
- use instead of print() to get time stamps.
|
|
56
|
+
- unless stdout is a tty, say for debugging, ~/.config/pwr-tray/debug.log is used for stdout
|
|
57
|
+
- if we create a log file, its size is limited to 512K and then it is truncated
|
|
58
|
+
"""
|
|
59
|
+
def check_stdout(use_stdout=None):
|
|
60
|
+
global prt_to_init
|
|
61
|
+
def is_tty():
|
|
62
|
+
try:
|
|
63
|
+
os.ttyname(sys.stdout.fileno())
|
|
64
|
+
return True
|
|
65
|
+
except Exception:
|
|
66
|
+
return False
|
|
67
|
+
def is_reg():
|
|
68
|
+
try:
|
|
69
|
+
return stat.S_ISREG(os.fstat(sys.stdout.fileno()).st_mode)
|
|
70
|
+
except Exception:
|
|
71
|
+
return False
|
|
72
|
+
def reopen():
|
|
73
|
+
global prt_to_init
|
|
74
|
+
sys.stdout = open(prt_path, "a+", encoding='utf-8')
|
|
75
|
+
sys.stderr = sys.stdout
|
|
76
|
+
os.dup2(sys.stdout.fileno(), 1)
|
|
77
|
+
os.dup2(sys.stderr.fileno(), 2)
|
|
78
|
+
prt_to_init = False
|
|
79
|
+
|
|
80
|
+
if prt_kb > 0 and prt_path: # non-positive disables stdout "tuning"
|
|
81
|
+
if use_stdout is False:
|
|
82
|
+
reopen()
|
|
83
|
+
elif prt_to_init and sys.stdout.closed: # Check if stdout is closed
|
|
84
|
+
reopen()
|
|
85
|
+
elif prt_to_init and not is_tty() and not is_reg():
|
|
86
|
+
reopen()
|
|
87
|
+
if os.fstat(sys.stdout.fileno()).st_size > prt_kb*1024:
|
|
88
|
+
shutil.move(prt_path, f'{prt_path}1')
|
|
89
|
+
reopen()
|
|
90
|
+
|
|
91
|
+
to_stdout = None
|
|
92
|
+
if 'to_stdout' in kwargs:
|
|
93
|
+
to_stdout = bool(kwargs['to_stdout'])
|
|
94
|
+
del kwargs['to_stdout']
|
|
95
|
+
check_stdout(to_stdout)
|
|
96
|
+
|
|
97
|
+
dt = datetime.now().strftime('%m-%d^%H:%M:%S')
|
|
98
|
+
s = StringIO()
|
|
99
|
+
print(dt, end=' ', file=s)
|
|
100
|
+
kwargs['end'] = ' '
|
|
101
|
+
kwargs['file'] = s
|
|
102
|
+
print(*args, **kwargs)
|
|
103
|
+
string = f'{s.getvalue()} {where()}'
|
|
104
|
+
print(string, flush=True)
|
|
105
|
+
|
|
106
|
+
def x_restart_self():
|
|
107
|
+
""" TBD """
|
|
108
|
+
# Retrieve the current script name and arguments
|
|
109
|
+
script_name = sys.argv[0]
|
|
110
|
+
script_args = sys.argv[1:]
|
|
111
|
+
|
|
112
|
+
# Use subprocess to run the script with the same arguments
|
|
113
|
+
subprocess.run([sys.executable, script_name] + script_args, check=True)
|
|
114
|
+
|
|
115
|
+
class PyKill:
|
|
116
|
+
"""Class to kill python scripts and explain how; avoids suicide. """
|
|
117
|
+
def __init__(self):
|
|
118
|
+
self.last_sig = {} # keyed by target
|
|
119
|
+
self.alive = {} # ps line of found targets
|
|
120
|
+
|
|
121
|
+
def _kill_script(self, targets, sig):
|
|
122
|
+
""" one iteration through very instance of the remaining processes """
|
|
123
|
+
def kill_pid(pid, sig):
|
|
124
|
+
# terminate the process
|
|
125
|
+
alive = True
|
|
126
|
+
try:
|
|
127
|
+
os.kill(pid, sig)
|
|
128
|
+
except OSError:
|
|
129
|
+
alive = False
|
|
130
|
+
return alive
|
|
131
|
+
self.alive = {} # reset each time thru
|
|
132
|
+
for line in os.popen("ps -eo pid,args"):
|
|
133
|
+
words = line.split()
|
|
134
|
+
if len(words) < 3:
|
|
135
|
+
continue
|
|
136
|
+
pid, cmd = int(words[0]), os.path.basename(words[1])
|
|
137
|
+
if pid == os.getpid():
|
|
138
|
+
continue # no suicides
|
|
139
|
+
script = os.path.basename(words[2])
|
|
140
|
+
for target in targets:
|
|
141
|
+
if cmd in ('python', 'python3') and script in (target, f'{target}.py'):
|
|
142
|
+
self.last_sig[target] = sig
|
|
143
|
+
if kill_pid(pid, sig): # returns True if alive
|
|
144
|
+
self.alive[target] = line
|
|
145
|
+
return self.alive
|
|
146
|
+
|
|
147
|
+
def kill_loop(self, targets):
|
|
148
|
+
"""Loops thru the remaining process until all or gone or we
|
|
149
|
+
become exhausted (e.g., unkillable processes)."""
|
|
150
|
+
targets = targets if isinstance(targets, (list, tuple)) else [targets]
|
|
151
|
+
for sig in [signal.SIGTERM, signal.SIGTERM, signal.SIGTERM,
|
|
152
|
+
signal.SIGKILL, signal.SIGKILL,]:
|
|
153
|
+
if not self._kill_script(targets, sig):
|
|
154
|
+
break
|
|
155
|
+
time.sleep(1)
|
|
156
|
+
for target, line in self.alive.items():
|
|
157
|
+
prt(f'ALERT: running: {line} [sig={self.last_sig[target]}]')
|
|
158
|
+
for target in targets:
|
|
159
|
+
if target in self.alive:
|
|
160
|
+
continue
|
|
161
|
+
prt(f'INFO: gone: {target} [sig={self.last_sig.get(target, None)}]')
|
|
162
|
+
return not bool(self.alive)
|
pwr_tray/YamlDump.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Dump (almost) any object in YAML format.
|
|
4
|
+
Supports mixing flow / non-flow nodes to handles cases where non-flow makes the
|
|
5
|
+
dump have an absurd line count with many trite lines.
|
|
6
|
+
|
|
7
|
+
This *may* require customization if types are used for which this does not work;
|
|
8
|
+
search for 'YourClassThatDoesNotWork' to see where to customize your outlier cases.
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
# pylint: disable=invalid-name
|
|
12
|
+
|
|
13
|
+
import threading
|
|
14
|
+
import textwrap
|
|
15
|
+
from io import StringIO
|
|
16
|
+
from collections import OrderedDict
|
|
17
|
+
from ruamel.yaml import YAML, comments as yaml_comments
|
|
18
|
+
from LibGen.CustLogger import CustLogger as lg
|
|
19
|
+
# import ToolBase as tb
|
|
20
|
+
|
|
21
|
+
#yaml = YAML(typ='safe') # NOTE: cannot have 'safe' and mixed block/flow style
|
|
22
|
+
yaml = YAML()
|
|
23
|
+
yaml.default_flow_style = False
|
|
24
|
+
|
|
25
|
+
flow = threading.local()
|
|
26
|
+
flow.flow_nodes = None
|
|
27
|
+
|
|
28
|
+
def yamlize(obj):
|
|
29
|
+
"""Make a complex object (e.g., class objects) more capable of
|
|
30
|
+
being dumped in yaml by simplifying the types where possible."""
|
|
31
|
+
# pylint: disable=too-many-return-statements
|
|
32
|
+
if isinstance(obj, list):
|
|
33
|
+
rv = []
|
|
34
|
+
for value in obj:
|
|
35
|
+
rv.append(yamlize(value))
|
|
36
|
+
return rv
|
|
37
|
+
|
|
38
|
+
if isinstance(obj, dict):
|
|
39
|
+
rv = OrderedDict()
|
|
40
|
+
for key, value in obj.items():
|
|
41
|
+
rv[key] = yamlize(value)
|
|
42
|
+
return rv
|
|
43
|
+
|
|
44
|
+
# this handles some crazy types like:
|
|
45
|
+
# rumel.yaml.scalarfloat.ScalarFloat
|
|
46
|
+
# when the object was converted from yaml
|
|
47
|
+
if isinstance(obj, bool):
|
|
48
|
+
return bool(obj)
|
|
49
|
+
if isinstance(obj, int):
|
|
50
|
+
return int(obj)
|
|
51
|
+
if isinstance(obj, float):
|
|
52
|
+
return float(obj)
|
|
53
|
+
# NOTE: if there are objects not handled, then fix here
|
|
54
|
+
# if isinstance(obj, YourClassThatDoesNotWork):
|
|
55
|
+
# return str(obj) # or whatever type if str is not appropriate
|
|
56
|
+
|
|
57
|
+
# convert a potentially recursive object into a dictionary
|
|
58
|
+
# - ignore variables that start with '_'
|
|
59
|
+
if not hasattr(obj, '__dict__'):
|
|
60
|
+
return obj
|
|
61
|
+
raw_rv = vars(obj)
|
|
62
|
+
rv = {}
|
|
63
|
+
for key, val in raw_rv.items():
|
|
64
|
+
if key.startswith('_'):
|
|
65
|
+
continue
|
|
66
|
+
rv[key] = yamlize(val)
|
|
67
|
+
return rv
|
|
68
|
+
|
|
69
|
+
def set_flow(obj, flow_nodes=None):
|
|
70
|
+
"""Set flow style for certain nodes...
|
|
71
|
+
Ref: https://stackoverflow.com/questions/63364894/
|
|
72
|
+
how-to-dump-only-lists-with-flow-style-with-pyyaml-or-ruamel-yaml
|
|
73
|
+
"""
|
|
74
|
+
obj = yamlize(obj)
|
|
75
|
+
if flow_nodes:
|
|
76
|
+
flow.flow_nodes = flow_nodes
|
|
77
|
+
set_flow_recursive(obj)
|
|
78
|
+
return obj
|
|
79
|
+
|
|
80
|
+
def set_flow_recursive(obj):
|
|
81
|
+
"""TBD"""
|
|
82
|
+
if isinstance(obj, dict):
|
|
83
|
+
# print('dict...')
|
|
84
|
+
for key, value in obj.items():
|
|
85
|
+
# print('key...', key)
|
|
86
|
+
if key in flow.flow_nodes:
|
|
87
|
+
if isinstance(value, dict):
|
|
88
|
+
# print('...', key, '/map')
|
|
89
|
+
value = yaml_comments.CommentedMap(value)
|
|
90
|
+
value.fa.set_flow_style()
|
|
91
|
+
obj[key] = value
|
|
92
|
+
elif isinstance(value, list):
|
|
93
|
+
# print('...', key, '/list')
|
|
94
|
+
value = yaml_comments.CommentedSeq(value)
|
|
95
|
+
value.fa.set_flow_style()
|
|
96
|
+
obj[key] = value
|
|
97
|
+
else:
|
|
98
|
+
set_flow_recursive(value)
|
|
99
|
+
elif isinstance(obj, list):
|
|
100
|
+
# print('list...')
|
|
101
|
+
for value in obj:
|
|
102
|
+
set_flow_recursive(value)
|
|
103
|
+
|
|
104
|
+
def yaml_to_file(obj, fileh, flow_nodes=None):
|
|
105
|
+
"""Do a yaml-to-file conversion of an object (w/o indent)."""
|
|
106
|
+
obj = set_flow(obj, flow_nodes)
|
|
107
|
+
yaml.dump(obj, fileh)
|
|
108
|
+
|
|
109
|
+
def yaml_str(obj, indent=8, width=70, flow_nodes=None):
|
|
110
|
+
"""Do a yaml-to-string conversion of an object with indent by default."""
|
|
111
|
+
outs = StringIO()
|
|
112
|
+
obj = set_flow(obj, flow_nodes)
|
|
113
|
+
old_width, yaml.width = yaml.width, width
|
|
114
|
+
yaml.dump(obj, outs)
|
|
115
|
+
yaml.width = old_width
|
|
116
|
+
return textwrap.indent(outs.getvalue(), prefix=' '*indent)
|
|
117
|
+
|
|
118
|
+
def yaml_dump(obj, indent=8, flow_nodes=None):
|
|
119
|
+
"""Do a yaml dump of an object to stdout with indent by default1."""
|
|
120
|
+
lg.pr(yaml_str(obj, indent=indent, flow_nodes=flow_nodes).rstrip())
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def runner(argv):
|
|
124
|
+
"""Several simple tests for YamlDump including exercising flow_nodes."""
|
|
125
|
+
# pylint: disable=broad-except, import-outside-toplevel, using-constant-test
|
|
126
|
+
def tester_func():
|
|
127
|
+
"""TBD"""
|
|
128
|
+
import sys
|
|
129
|
+
import copy
|
|
130
|
+
|
|
131
|
+
scrum_orig = {'flow_sec': 55, 'hash': 'cb217daa4aedf6ad8483a9333f6cc114f413cc7b',
|
|
132
|
+
'name': 'Eche Palante - Refle', 'progress': 100, 'ratio': 0.8502833247184753,
|
|
133
|
+
'samples': [[10.9, 10, 2.02], [15.7, 23, 1.14], [18.1, 32, 1.06],
|
|
134
|
+
[20.5, 42, 0.91], [22.9, 50, 0.93], [30.1, 65, 0.98], [34.9, 75, 0.91],
|
|
135
|
+
[39.7, 83, 0.88], [44.5, 90, 0.89], [54.1, 100, 0.85]],
|
|
136
|
+
'scrum_form_sec': 8, 'scrum_sz': 12, 'time': 1621850682.8617778,
|
|
137
|
+
'tor_sz': 249262127, 'up_client': 'qBittorrent/4.3.5', 'up_sec': 0}
|
|
138
|
+
|
|
139
|
+
if True:
|
|
140
|
+
scrum = copy.deepcopy(scrum_orig)
|
|
141
|
+
print('\n\n===== TEST 1:', 'yaml.dump(scrum, sys.stdout)')
|
|
142
|
+
yaml.dump(scrum, sys.stdout)
|
|
143
|
+
|
|
144
|
+
if True:
|
|
145
|
+
scrum = copy.deepcopy(scrum_orig)
|
|
146
|
+
print('\n\n===== TEST 2:', 'surgical CommentedSeq')
|
|
147
|
+
obj = yaml_comments.CommentedSeq(scrum['samples'])
|
|
148
|
+
obj.fa.set_flow_style()
|
|
149
|
+
scrum['samples'] = obj
|
|
150
|
+
yaml.dump(scrum, sys.stdout)
|
|
151
|
+
|
|
152
|
+
if True:
|
|
153
|
+
scrum = copy.deepcopy(scrum_orig)
|
|
154
|
+
print('\n\n===== TEST 3:', 'designed API')
|
|
155
|
+
yaml_dump(scrum, flow_nodes=('samples',))
|
|
156
|
+
|
|
157
|
+
import argparse
|
|
158
|
+
parser = argparse.ArgumentParser()
|
|
159
|
+
parser.add_argument('-V', '--log-level', choices=lg.choices,
|
|
160
|
+
default='INFO', help='set logging/verbosity level [dflt=INFO]')
|
|
161
|
+
opts = parser.parse_args(argv)
|
|
162
|
+
lg.setup(level=opts.log_level)
|
|
163
|
+
tester_func()
|
pwr_tray/__init__.py
ADDED
|
File without changes
|