4142 lines
92 KiB
Python
4142 lines
92 KiB
Python
#!/usr/bin/env python3
|
|
import asyncio
|
|
import argparse
|
|
import enum
|
|
import threading
|
|
import shutil
|
|
import collections
|
|
import datetime
|
|
import functools
|
|
import io
|
|
import json
|
|
import pathlib
|
|
import logging
|
|
import optparse
|
|
import os
|
|
import pprint
|
|
import re
|
|
import select
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import traceback
|
|
|
|
from typing import (
|
|
Literal,
|
|
Optional,
|
|
TypedDict,
|
|
Callable,
|
|
Generator,
|
|
TypeAlias,
|
|
Any,
|
|
cast,
|
|
)
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def custom_notify(
|
|
title: Optional[str] = None,
|
|
msg: Optional[str] = None,
|
|
timeout: Optional[int] = None,
|
|
) -> None:
|
|
if timeout is None:
|
|
timeout = 5
|
|
|
|
timeout2 = int(timeout * 1000)
|
|
|
|
assert isinstance(timeout2, int) and timeout2 >= 500
|
|
|
|
if title is None:
|
|
title = 'commands'
|
|
|
|
assert isinstance(title, str) and len(title) > 0
|
|
assert isinstance(msg, str) and len(msg) > 0
|
|
|
|
if sys.platform == 'darwin':
|
|
osascript_translate = functools.partial(
|
|
custom_translate,
|
|
check=lambda a, b: not re.compile(r'^[a-zA-Z0-9\<\>\/\(\)\s\.\,\:]*$').match(b) is None,
|
|
)
|
|
|
|
subprocess.check_call(
|
|
[
|
|
'osascript',
|
|
'-e',
|
|
'display notification "%s" with title "%s"'
|
|
% (
|
|
osascript_translate(msg),
|
|
osascript_translate(title),
|
|
),
|
|
]
|
|
)
|
|
else:
|
|
subprocess.check_call(['notify-send', '-t', '%d' % timeout2, title, msg[-128:]])
|
|
|
|
|
|
class intercept_output_t:
|
|
class line_res_t(TypedDict):
|
|
aggregated: bool
|
|
line: bytes
|
|
|
|
class realtime_res_t(TypedDict):
|
|
aggregated: bool
|
|
data: bytes
|
|
|
|
class aggregated_res_t(TypedDict):
|
|
aggregated: bool
|
|
data: bytes
|
|
returncode: Optional[int]
|
|
|
|
res_t: TypeAlias = line_res_t | realtime_res_t | aggregated_res_t
|
|
|
|
|
|
def intercept_output(
|
|
current_subprocess: subprocess.Popen[bytes],
|
|
return_aggregated: Optional[bool] = None,
|
|
transform_callback: Optional[Callable[[bytes], Optional[bytes]]] = None,
|
|
real_time: Optional[bool] = None,
|
|
timeout: Optional[float] = None,
|
|
need_lines: Optional[bool] = None,
|
|
) -> Generator[
|
|
intercept_output_t.res_t,
|
|
None,
|
|
None,
|
|
]:
|
|
if real_time is None:
|
|
real_time = False
|
|
|
|
start_timestamp = datetime.datetime.now()
|
|
|
|
if not return_aggregated:
|
|
return_aggregated = False
|
|
|
|
t1 = select.poll()
|
|
|
|
assert not current_subprocess.stdout is None
|
|
|
|
assert isinstance(current_subprocess.stdout, io.BufferedReader)
|
|
|
|
t1.register(current_subprocess.stdout, select.POLLIN)
|
|
# print([current_subprocess, current_subprocess.poll()])
|
|
output: list[bytes] = []
|
|
buffer: collections.deque[bytes] = collections.deque()
|
|
buffer_lines: collections.deque[bytes] = collections.deque()
|
|
|
|
last_data = None
|
|
|
|
while not (not current_subprocess.poll() is None and not last_data is None):
|
|
if not timeout is None and (datetime.datetime.now() - start_timestamp).total_seconds() > timeout:
|
|
break
|
|
|
|
t2 = t1.poll(100)
|
|
if (
|
|
len(t2) == 1
|
|
and (t2[0][1] & select.POLLIN) > 0
|
|
and not (isinstance(last_data, bytes) and len(last_data) == 0)
|
|
or not current_subprocess.poll() is None
|
|
):
|
|
t3 = current_subprocess.stdout.peek()
|
|
|
|
t4: bytes = current_subprocess.stdout.read(len(t3))
|
|
assert isinstance(t4, bytes)
|
|
|
|
last_data = t3
|
|
output.append(t3)
|
|
if need_lines:
|
|
buffer.append(t3)
|
|
|
|
if need_lines:
|
|
if b'\n' in t3:
|
|
t3_pos = t3.rfind(b'\n')
|
|
buffer_lines.extend(
|
|
[
|
|
o + b'\n'
|
|
for o in b''.join(
|
|
list(buffer)[:-1] + [t3[:t3_pos]],
|
|
).splitlines()
|
|
]
|
|
)
|
|
buffer.clear()
|
|
buffer.append(t3[t3_pos + 1 :])
|
|
while len(buffer_lines) > 0:
|
|
yield intercept_output_t.line_res_t(
|
|
aggregated=False,
|
|
line=buffer_lines.popleft(),
|
|
)
|
|
|
|
else:
|
|
yield dict(
|
|
data=t3,
|
|
aggregated=False,
|
|
)
|
|
t6 = t3
|
|
if not transform_callback is None:
|
|
t5 = transform_callback(t3)
|
|
if not t5 is None:
|
|
t6 = t5
|
|
|
|
if len(t6) > 0:
|
|
os.write(sys.stdout.fileno(), t6)
|
|
elif real_time:
|
|
yield dict(
|
|
data=b'',
|
|
aggregated=False,
|
|
)
|
|
|
|
if return_aggregated:
|
|
yield dict(
|
|
data=b''.join(output),
|
|
aggregated=True,
|
|
returncode=current_subprocess.poll(),
|
|
)
|
|
|
|
|
|
def player_metadata() -> Optional[str]:
|
|
for k in range(20):
|
|
try:
|
|
metadata = {k: subprocess.check_output(['playerctl', 'metadata', k]).decode('utf-8').strip() for k in ['artist', 'title']}
|
|
return '%s - %s' % (metadata['artist'], metadata['title'])
|
|
time.sleep(1.0)
|
|
except Exception:
|
|
continue
|
|
|
|
return None
|
|
|
|
|
|
class memory_stats_t:
|
|
class res_t(TypedDict):
|
|
mem_total: int
|
|
mem_used: int
|
|
|
|
|
|
def memory_stats() -> memory_stats_t.res_t:
|
|
if sys.platform == 'linux':
|
|
with io.BytesIO(subprocess.check_output('free', shell=True)) as f:
|
|
t1 = f.read().decode('utf-8').splitlines()
|
|
mem_total = int(t1[1].strip().split()[1])
|
|
mem_used = int(t1[1].strip().split()[2]) + int(t1[1].strip().split()[4])
|
|
|
|
return dict(
|
|
mem_total=mem_total,
|
|
mem_used=mem_used,
|
|
)
|
|
elif sys.platform == 'darwin':
|
|
sysctl_value = lambda name, custom_cast=int: custom_cast(
|
|
subprocess.check_output(
|
|
'sysctl -a | grep %s' % name,
|
|
shell=True,
|
|
)
|
|
.decode('utf-8')
|
|
.split(':')[1]
|
|
)
|
|
|
|
vm_pagesize = sysctl_value('vm.pagesize')
|
|
mem_total = sysctl_value('hw.memsize')
|
|
|
|
t1 = subprocess.check_output('vm_stat').decode('utf-8')
|
|
t2 = [o.split(':') for o in t1.splitlines() if ':' in o]
|
|
t3 = {
|
|
o[0].replace(' ', '_').replace('-', '_').lower(): int(o[1].strip().rstrip('.'))
|
|
for o in t2
|
|
if len(o) == 2
|
|
and len(o[0]) > 0
|
|
and not re.compile(r'^\s*\d+\.\s*$').match(o[1]) is None
|
|
and not re.compile(r'^[a-zA-Z0-9\_\-\s]+$').match(o[0]) is None
|
|
}
|
|
mem_used = (t3['pages_active'] + t3['pages_wired_down']) * vm_pagesize
|
|
|
|
return dict(
|
|
mem_total=mem_total / 1024,
|
|
mem_used=mem_used / 1024,
|
|
)
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
|
|
def chrome(argv: list[str]) -> int:
|
|
assert isinstance(argv, list) and all([isinstance(o, str) for o in argv])
|
|
parser = optparse.OptionParser()
|
|
parser.add_option(
|
|
'--user_data_dir',
|
|
dest='user_data_dir',
|
|
default=None,
|
|
type=str,
|
|
)
|
|
|
|
options, args = parser.parse_args(argv)
|
|
|
|
if options.user_data_dir is None:
|
|
options.user_data_dir = os.path.join(
|
|
os.environ['HOME'],
|
|
'.config',
|
|
'google-chrome',
|
|
)
|
|
|
|
# assert os.path.exists(options.user_data_dir)
|
|
|
|
if sys.platform == 'linux':
|
|
return subprocess.check_call(
|
|
[
|
|
'google-chrome-stable',
|
|
'--enable-features=useOzonePlatform',
|
|
'--ozone-platform=wayland',
|
|
'--process-per-site',
|
|
'--user-data-dir=%s' % options.user_data_dir,
|
|
*args,
|
|
]
|
|
)
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
|
|
def raise_not_implemented() -> None:
|
|
raise NotImplementedError
|
|
|
|
|
|
def eternal_oom(argv: list[str]) -> None:
|
|
import signal
|
|
import re
|
|
import time
|
|
import pprint
|
|
|
|
assert isinstance(argv, list) and all([isinstance(o, str) for o in argv])
|
|
parser = optparse.OptionParser()
|
|
parser.add_option(
|
|
'--cpu_wait',
|
|
dest='cpu_wait',
|
|
default=None,
|
|
type=float,
|
|
)
|
|
parser.add_option(
|
|
'--mean_size',
|
|
dest='mean_size',
|
|
default=None,
|
|
type=int,
|
|
)
|
|
parser.add_option(
|
|
'--one_shot_clean',
|
|
dest='one_shot_clean',
|
|
action='store_true',
|
|
default=None,
|
|
)
|
|
parser.add_option(
|
|
'--cpu',
|
|
dest='cpu_json',
|
|
type=str,
|
|
default=None,
|
|
)
|
|
parser.add_option(
|
|
'--one_shot_app',
|
|
dest='one_shot_app',
|
|
default=[],
|
|
action='append',
|
|
)
|
|
parser.add_option(
|
|
'--period',
|
|
dest='period',
|
|
default=None,
|
|
type=float,
|
|
)
|
|
parser.add_option(
|
|
'--memory_limit',
|
|
dest='memory_limit',
|
|
default=None,
|
|
type=float,
|
|
)
|
|
parser.add_option(
|
|
'--cpu_limit',
|
|
dest='cpu_limit',
|
|
default=None,
|
|
type=float,
|
|
)
|
|
parser.add_option(
|
|
'--debug',
|
|
dest='debug',
|
|
action='store_true',
|
|
default=False,
|
|
)
|
|
options, args = parser.parse_args(argv)
|
|
|
|
if not options.cpu_json is None:
|
|
options.cpu = json.loads(options.cpu_json)
|
|
else:
|
|
options.cpu = True
|
|
|
|
self_pid = os.getpid()
|
|
|
|
if isinstance(options.one_shot_clean, bool) and options.one_shot_clean:
|
|
if len(options.one_shot_app) == 0:
|
|
options.one_shot_app = ['chrome', 'teams']
|
|
|
|
config = dict(
|
|
chrome=(r'chrome.*type=renderer', r'^.*--extension-process.*$'),
|
|
teams=(r'teams.*type=renderer', None),
|
|
)
|
|
|
|
for app in options.one_shot_app:
|
|
p = config[app]
|
|
|
|
try:
|
|
t1 = subprocess.check_output(['pgrep', '-a', '-f', p[0]]).decode('utf-8')
|
|
except Exception:
|
|
continue
|
|
t2 = t1.splitlines()
|
|
if not p[1] is None:
|
|
t3 = [o for o in t2 if re.compile(p[1]).match(o) is None]
|
|
else:
|
|
t3 = t2
|
|
t4 = [int(o.split()[0]) for o in t3]
|
|
|
|
for pid in t4:
|
|
if pid == self_pid:
|
|
raise NotImplementedError
|
|
|
|
os.kill(pid, signal.SIGTERM)
|
|
|
|
logging.info(
|
|
json.dumps(
|
|
dict(
|
|
apps=options.one_shot_app,
|
|
count=len(t4),
|
|
processes=[o.split()[:3] for o in t3],
|
|
)
|
|
)
|
|
)
|
|
|
|
if len(t4) > 0:
|
|
print('\n'.join([str(o.split()[:3]) for o in t3]))
|
|
return
|
|
|
|
cpu_count = os.cpu_count()
|
|
assert isinstance(cpu_count, int)
|
|
|
|
if options.period is None:
|
|
options.period = 1
|
|
if options.memory_limit is None:
|
|
options.memory_limit = 3 * 1024 * 1024
|
|
if options.cpu_limit is None:
|
|
options.cpu_limit = 0.6 * cpu_count
|
|
if options.cpu_wait is None:
|
|
options.cpu_wait = 10
|
|
if options.mean_size is None:
|
|
options.mean_size = 30
|
|
|
|
if isinstance(options.memory_limit, float):
|
|
options.memory_limit = int(options.memory_limit)
|
|
|
|
assert isinstance(options.memory_limit, int) and options.memory_limit < memory_stats()['mem_total'] * 0.95 and options.memory_limit > 512 * 1024
|
|
|
|
assert isinstance(options.cpu_limit, float) and options.cpu_limit > 0.2 * cpu_count and options.cpu_limit < cpu_count * 0.95
|
|
|
|
assert options.period >= 1
|
|
|
|
assert options.cpu_wait >= 10
|
|
assert options.mean_size >= 16
|
|
|
|
def pandas_data_frame(
|
|
lines: list[str],
|
|
groups_regex: re.Pattern[str],
|
|
header_regex: re.Pattern[str],
|
|
extra_columns: dict[str, Callable[[dict[str, str]], Any]],
|
|
) -> dict[str, list[Any]]:
|
|
header_match = re.compile(header_regex).search(lines[0])
|
|
assert not header_match is None
|
|
header = header_match.groups()
|
|
rows = []
|
|
for line in lines[1:]:
|
|
row_match = re.compile(groups_regex).search(line)
|
|
assert not row_match is None
|
|
rows.append(row_match.groups())
|
|
|
|
columns: dict[str, list[Any]] = {column: [] for column in header}
|
|
|
|
for row in rows:
|
|
for value, column in zip(row, header):
|
|
columns[column].append(value)
|
|
for column, transformation in extra_columns.items():
|
|
columns[column] = [transformation({k: v[index] for k, v in columns.items()}) for index in range(len(rows))]
|
|
|
|
return columns
|
|
|
|
def pandas_merge(
|
|
left: dict[str, list[Any]],
|
|
right: dict[str, list[Any]],
|
|
on: str,
|
|
) -> dict[str, list[Any]]:
|
|
index: dict[str, dict[Any, list[int]]] = {}
|
|
|
|
input_data_frames: list[tuple[str, dict[str, list[Any]]]] = [
|
|
('left', left),
|
|
('right', right),
|
|
]
|
|
for index_name, data_frame in input_data_frames:
|
|
current_index: dict[Any, list[int]] = {}
|
|
for row_index, value in enumerate(data_frame[on]):
|
|
if not value in current_index:
|
|
current_index[value] = []
|
|
current_index[value].append(row_index)
|
|
|
|
index[index_name] = current_index
|
|
|
|
class MergedDataFrame(TypedDict):
|
|
header: list[str]
|
|
columns: dict[str, list[Any]]
|
|
|
|
merged_data_frame: MergedDataFrame = dict(
|
|
header=[column + '_x' for column in left] + [column + '_y' for column in right],
|
|
columns={},
|
|
)
|
|
|
|
for column in merged_data_frame['header']:
|
|
merged_data_frame['columns'][column] = []
|
|
|
|
common_values: set[Any] = {left_value for left_value in index['left'] if left_value in index['right']}
|
|
|
|
class RowMatch(TypedDict):
|
|
left_row_index: int
|
|
right_row_index: int
|
|
|
|
common_rows: list[RowMatch] = sorted(
|
|
[
|
|
dict(
|
|
left_row_index=index['left'][value][0],
|
|
right_row_index=index['right'][value][0],
|
|
)
|
|
for value in common_values
|
|
],
|
|
key=lambda x: x['left_row_index'],
|
|
)
|
|
for common_row in common_rows:
|
|
row = sum(
|
|
[
|
|
[
|
|
values[
|
|
common_row[
|
|
cast(
|
|
Literal['left_row_index' | 'right_row_index'],
|
|
'left_row_index' if index_name == 'left' else 'right_row_index' if index_name == 'right' else raise_not_implemented(),
|
|
)
|
|
]
|
|
]
|
|
for column, values in data_frame.items()
|
|
]
|
|
for index_name, data_frame in input_data_frames
|
|
],
|
|
[],
|
|
)
|
|
for column, value in zip(merged_data_frame['header'], row):
|
|
merged_data_frame['columns'][column].append(value)
|
|
|
|
return merged_data_frame['columns']
|
|
|
|
def pandas_sort_values(data_frame, by, ascending):
|
|
assert len(by) == 1
|
|
assert ascending is False
|
|
t1 = [
|
|
o['row_index']
|
|
for o in sorted([dict(row_index=row_index, value=value) for row_index, value in enumerate(data_frame[by[0]])], key=lambda x: x['value'])[::-1]
|
|
]
|
|
return {column: [values[row_index] for row_index in t1] for column, values in data_frame.items()}
|
|
|
|
def pandas_filter_values(data_frame, condition):
|
|
shape = [
|
|
len(data_frame),
|
|
]
|
|
if shape[0] > 0:
|
|
shape.append(len(list(data_frame.values())[0]))
|
|
t1 = [row_index for row_index in range(shape[1]) if condition({column: values[row_index] for column, values in data_frame.items()})]
|
|
return {column: [values[row_index] for row_index in t1] for column, values in data_frame.items()}
|
|
|
|
def pandas_row(data_frame, row_index):
|
|
return {column: values[row_index] for column, values in data_frame.items()}
|
|
|
|
def pandas_shape(data_frame):
|
|
columns_count = len(data_frame)
|
|
if columns_count > 0:
|
|
rows_count = len(data_frame[next(iter(data_frame.keys()))])
|
|
else:
|
|
rows_count = 0
|
|
|
|
return [
|
|
columns_count,
|
|
rows_count,
|
|
]
|
|
|
|
def ps_regex(groups_cnt):
|
|
assert groups_cnt >= 1
|
|
return ''.join(
|
|
[
|
|
r'^\s*',
|
|
r'([^\s]+)\s+' * (groups_cnt - 1),
|
|
r'([^\s]+)\s*$',
|
|
]
|
|
)
|
|
|
|
def oom_get_processes(
|
|
extra_filter=None,
|
|
):
|
|
with io.BytesIO(subprocess.check_output('ps -e -o pid,rss,user,%cpu', shell=True)) as f:
|
|
t1 = pandas_data_frame(
|
|
f.read().decode('utf-8').splitlines(),
|
|
ps_regex(4),
|
|
ps_regex(4),
|
|
dict(
|
|
PID=lambda row: int(row['PID']),
|
|
RSS=lambda row: int(row['RSS']),
|
|
CPU=lambda row: float(row['%CPU']),
|
|
),
|
|
)
|
|
del t1['%CPU']
|
|
assert set(t1.keys()) == set(['PID', 'RSS', 'USER', 'CPU'])
|
|
|
|
t5 = subprocess.check_output('ps -e -o pid,args', shell=True).decode('utf-8').splitlines()
|
|
t6 = pandas_data_frame(
|
|
t5,
|
|
r'^\s*(\d+)\s(.*)$',
|
|
r'^\s+(\w+)\s+(\w+)\s*$',
|
|
dict(PID=lambda row: int(row['PID'])),
|
|
)
|
|
|
|
if not 'COMMAND' in t6:
|
|
if sys.platform == 'darwin' and 'ARGS' in t6:
|
|
t6['COMMAND'] = t6['ARGS']
|
|
del t6['ARGS']
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
assert set(t6.keys()) == set(['PID', 'COMMAND'])
|
|
t11 = pandas_merge(t1, t6, on='PID')
|
|
if extra_filter is None:
|
|
extra_filter = lambda *args: True
|
|
|
|
t7 = pandas_filter_values(t11, lambda row: row['PID_x'] != self_pid and not 'freelancer' in row['COMMAND_y'] and extra_filter(row))
|
|
|
|
t8 = pandas_sort_values(t7, by=['RSS_x'], ascending=False)
|
|
t9 = pandas_sort_values(t7, by=['CPU_x'], ascending=False)
|
|
t10 = sum(t9['CPU_x'], 0.0) / 100
|
|
if options.debug:
|
|
pprint.pprint([t9['CPU_x'][:10], t10 * 100])
|
|
|
|
return dict(
|
|
by_mem=t8,
|
|
by_cpu=t9,
|
|
total_cpu=t10,
|
|
)
|
|
|
|
def oom_display_rows(current_dataframe):
|
|
print(
|
|
'\n'.join(
|
|
[
|
|
(
|
|
lambda row: '% 8d\t% 6.3f GiB\t% 5.2f %%\t% 10s\t%s'
|
|
% (
|
|
row['PID_x'],
|
|
row['RSS_x'] / 1024 / 1024,
|
|
row['CPU_x'],
|
|
row['USER_x'],
|
|
row['COMMAND_y'],
|
|
)
|
|
)(pandas_row(current_dataframe, k))
|
|
for k in range(
|
|
0,
|
|
min(
|
|
5,
|
|
pandas_shape(current_dataframe)[1],
|
|
),
|
|
)
|
|
]
|
|
)
|
|
)
|
|
|
|
def oom_kill(pid):
|
|
assert isinstance(pid, int)
|
|
|
|
try:
|
|
logging.info(
|
|
'%s oom_kill, pid %d'
|
|
% (
|
|
datetime.datetime.now().isoformat(),
|
|
pid,
|
|
)
|
|
)
|
|
os.kill(pid, signal.SIGKILL)
|
|
except Exception:
|
|
logging.error(traceback.format_exc())
|
|
custom_notify(msg='oom_kill, failed to kill pid %d' % pid)
|
|
|
|
def oom_status():
|
|
print(
|
|
'\r%s %6.2f / %.2f %%, %6.2f / %.2f GiB'
|
|
% (
|
|
datetime.datetime.now().isoformat(),
|
|
oom_mean_cpu() / os.cpu_count() * 100,
|
|
options.cpu_limit / os.cpu_count() * 100,
|
|
memory_stats()['mem_used'] / 1024 / 1024,
|
|
options.memory_limit / 1024 / 1024,
|
|
),
|
|
end='',
|
|
)
|
|
|
|
def first_check():
|
|
current_memory_stats = memory_stats()
|
|
|
|
t11 = oom_get_processes()
|
|
t8 = t11['by_mem']
|
|
|
|
if current_memory_stats['mem_used'] > options.memory_limit:
|
|
oom_display_rows(t8)
|
|
|
|
if t11['total_cpu'] > options.cpu_limit:
|
|
oom_display_rows(t11['by_cpu'])
|
|
|
|
free_before_oom = options.memory_limit - current_memory_stats['mem_used']
|
|
|
|
print(
|
|
'available %5.2f %% out of %5.2f %% of cpu limit before OOC'
|
|
% (
|
|
(options.cpu_limit - t11['total_cpu']) * 100 / os.cpu_count(),
|
|
options.cpu_limit * 100 / os.cpu_count(),
|
|
)
|
|
)
|
|
|
|
print(
|
|
'%5.2f GiB [%5.2f %%] out of %5.2f GiB of free memory before OOM'
|
|
% (
|
|
free_before_oom / 1024 / 1024,
|
|
free_before_oom / options.memory_limit * 100,
|
|
options.memory_limit / 1024 / 1024,
|
|
)
|
|
)
|
|
|
|
del t8
|
|
del t11
|
|
|
|
print('press Enter to start monitoring: ...', end='')
|
|
input()
|
|
print('\nstarted...')
|
|
|
|
first_check()
|
|
|
|
last_total_cpu = []
|
|
|
|
last_cpu_high = None
|
|
|
|
def oom_add_cpu(total_cpu):
|
|
if options.debug:
|
|
pprint.pprint([total_cpu, last_total_cpu])
|
|
last_total_cpu.append(total_cpu)
|
|
if len(last_total_cpu) > options.mean_size:
|
|
del last_total_cpu[-options.mean_size :]
|
|
|
|
def oom_mean_cpu():
|
|
return sum(last_total_cpu) / (len(last_total_cpu) + 1e-8)
|
|
|
|
def oom_cpu_high(cpu_limit=None):
|
|
if cpu_limit is None:
|
|
cpu_limit = options.cpu_limit
|
|
|
|
nonlocal last_cpu_high
|
|
|
|
if oom_mean_cpu() > cpu_limit:
|
|
if last_cpu_high is None:
|
|
last_cpu_high = datetime.datetime.now().timestamp()
|
|
|
|
if datetime.datetime.now().timestamp() - last_cpu_high > options.cpu_wait:
|
|
last_cpu_high = None
|
|
del last_total_cpu[:]
|
|
return True
|
|
|
|
return False
|
|
|
|
mem_used = None
|
|
mem_stat = None
|
|
|
|
def oom_mem_high(memory_limit=None):
|
|
nonlocal mem_used
|
|
|
|
if memory_limit is None:
|
|
memory_limit = options.memory_limit
|
|
|
|
return mem_used > memory_limit
|
|
|
|
while True:
|
|
mem_stat = memory_stats()
|
|
mem_used = mem_stat['mem_used']
|
|
|
|
if options.memory_limit < mem_stat['mem_total'] and not oom_mem_high(mem_stat['mem_total'] - (mem_stat['mem_total'] - options.memory_limit) / 2):
|
|
extra_filters = lambda row: ('chrome' in row['COMMAND_y'] and '--type=renderer' in row['COMMAND_y'] or not 'chrome' in row['COMMAND_y'])
|
|
else:
|
|
extra_filters = None
|
|
|
|
t11 = oom_get_processes(extra_filters)
|
|
|
|
oom_add_cpu(t11['total_cpu'])
|
|
|
|
t8 = t11['by_mem']
|
|
|
|
t9 = t8
|
|
t4 = lambda: oom_kill(t9['PID_x'][0])
|
|
|
|
oom_status()
|
|
|
|
if oom_mem_high():
|
|
print('\n', end='')
|
|
pprint.pprint(
|
|
[
|
|
'Killing [OOM]',
|
|
pandas_row(t9, 0),
|
|
mem_used,
|
|
]
|
|
)
|
|
t4()
|
|
|
|
if options.cpu and oom_cpu_high():
|
|
oom_display_rows(t11['by_cpu'])
|
|
print('\n', end='')
|
|
pprint.pprint(
|
|
[
|
|
'Killing [CPU]',
|
|
pandas_row(t11['by_cpu'], 0),
|
|
[options.cpu_limit, oom_mean_cpu(), t11['total_cpu']],
|
|
]
|
|
)
|
|
oom_kill(t11['by_cpu']['PID_x'][0])
|
|
time.sleep(options.period)
|
|
|
|
|
|
def resilient_vlc(stream=None):
|
|
if stream is None:
|
|
streams_path = os.path.join(os.environ['CACHE_PATH'], 'resilient-vlc-streams.json')
|
|
|
|
if os.path.exists(streams_path):
|
|
with io.open(streams_path, 'r') as f:
|
|
stream = json.load(f)
|
|
else:
|
|
raise RuntimeError('not found, %s' % streams_path)
|
|
|
|
if isinstance(stream, str):
|
|
stream = [stream]
|
|
|
|
if len(stream) == 0:
|
|
raise RuntimeError('no streams')
|
|
|
|
import subprocess
|
|
import time
|
|
|
|
while True:
|
|
print('new start')
|
|
with subprocess.Popen(
|
|
[
|
|
'cvlc',
|
|
'--verbose',
|
|
'2',
|
|
*stream,
|
|
],
|
|
stderr=subprocess.PIPE,
|
|
) as p:
|
|
while p.returncode is None:
|
|
t1 = p.stderr.readline().decode('utf-8')
|
|
if len(t1) > 0:
|
|
print(t1)
|
|
if not all(
|
|
[
|
|
o in t1
|
|
for o in [
|
|
'prefetch stream error',
|
|
'terror',
|
|
'main interface error',
|
|
]
|
|
]
|
|
) and any([o in t1 for o in ['pulse audio output debug: underflow']]):
|
|
print('shit')
|
|
p.kill()
|
|
while True:
|
|
try:
|
|
t2 = p.wait(timeout=1)
|
|
print(t2)
|
|
break
|
|
except Exception:
|
|
print('shit')
|
|
pass
|
|
time.sleep(1.0)
|
|
|
|
|
|
def sway_sock(
|
|
wait: bool = False,
|
|
) -> Optional[str]:
|
|
while True:
|
|
import glob
|
|
|
|
uid = os.stat(os.environ['HOME']).st_uid
|
|
t1 = glob.glob(
|
|
os.path.join(
|
|
'/run',
|
|
'user',
|
|
'%d' % uid,
|
|
'sway-ipc.%d*.sock' % uid,
|
|
)
|
|
)
|
|
t2 = [os.stat(o).st_mtime for o in t1]
|
|
sorted_entries = sorted(enumerate(t1), key=lambda x: t2[x[0]])
|
|
if len(sorted_entries) > 0:
|
|
t3 = sorted_entries[-1][0]
|
|
return t1[t3]
|
|
else:
|
|
if wait:
|
|
time.sleep(0.1)
|
|
continue
|
|
else:
|
|
return None
|
|
|
|
|
|
def eternal_firefox(
|
|
tabs=None,
|
|
profile=None,
|
|
group_name=None,
|
|
window_position=None,
|
|
debug=None,
|
|
):
|
|
import os
|
|
import datetime
|
|
import pprint
|
|
import subprocess
|
|
import time
|
|
|
|
if debug is None:
|
|
debug = False
|
|
if tabs is None:
|
|
raise RuntimeError('no tabs provided')
|
|
if profile is None:
|
|
raise RuntimeError('no profile provided')
|
|
if group_name is None:
|
|
raise RuntimeError('no group provided')
|
|
if window_position is None:
|
|
# window_position = '1,600,0,600,540'
|
|
raise RuntimeError('no window-position provided')
|
|
while True:
|
|
os.system(r"""date""")
|
|
with subprocess.Popen(
|
|
[
|
|
'firefox',
|
|
'-P',
|
|
profile,
|
|
*tabs,
|
|
]
|
|
) as p:
|
|
try:
|
|
if debug:
|
|
assert subprocess.check_call(['notify-send', '%s:Starting' % group_name]) == 0
|
|
|
|
# t3 = ''
|
|
for k in range(300):
|
|
t1 = subprocess.check_output(
|
|
r"""
|
|
swaymsg -t get_tree | jq -r '..|try select(.pid== %d)'
|
|
"""
|
|
% p.pid,
|
|
shell=True,
|
|
).decode('utf-8')
|
|
if len(t1) > 10:
|
|
break
|
|
# time.sleep(0.1)
|
|
# t1 = subprocess.check_output(['wmctrl', '-p', '-l']).decode('utf-8')
|
|
# t4 = [o for o in t1.splitlines() if str(p.pid) in o]
|
|
# if len(t4) == 1:
|
|
# t3 = t4[0]
|
|
# break
|
|
|
|
# if t3 == '':
|
|
# raise RuntimeError
|
|
|
|
# t2 = t3.split()[0]
|
|
# assert os.system('wmctrl -i -r %s -e %s' % (t2, window_position)) == 0
|
|
# assert os.system('wmctrl -i -r %s -b add,below' % t2) == 0
|
|
def reposition():
|
|
t1 = (
|
|
lambda s: s.replace('{{PID}}', str(p.pid))
|
|
.replace('{{X}}', str(window_position[1]))
|
|
.replace('{{Y}}', str(window_position[2]))
|
|
.replace('{{W}}', str(window_position[3]))
|
|
.replace('{{H}}', str(window_position[4]))
|
|
.replace('{{WORKSPACE}}', str(window_position[0]))
|
|
)
|
|
|
|
assert (
|
|
os.system(
|
|
t1(r"""
|
|
swaymsg '[pid="{{PID}}"] move window to workspace {{WORKSPACE}}'
|
|
""")
|
|
)
|
|
== 0
|
|
)
|
|
|
|
if window_position[1] != '' and window_position[2] != '':
|
|
assert (
|
|
os.system(
|
|
t1(r"""
|
|
swaymsg '[pid="{{PID}}"] floating enable' \
|
|
swaymsg '[pid="{{PID}}"] resize set width {{W}}px height {{H}}px' && \
|
|
swaymsg '[pid="{{PID}}"] move absolute position {{X}}px {{Y}}px'
|
|
""")
|
|
)
|
|
== 0
|
|
)
|
|
else:
|
|
assert (
|
|
os.system(
|
|
t1(r"""
|
|
swaymsg '[pid="{{PID}}"] floating disable'
|
|
""")
|
|
)
|
|
== 0
|
|
)
|
|
|
|
if False:
|
|
for tab in tabs[1:]:
|
|
time.sleep(10)
|
|
assert (
|
|
subprocess.check_call(
|
|
[
|
|
'firefox',
|
|
'-P',
|
|
profile,
|
|
'--new-tab',
|
|
tab,
|
|
]
|
|
)
|
|
== 0
|
|
)
|
|
|
|
reposition()
|
|
|
|
if debug:
|
|
assert subprocess.check_call(['notify-send', '%s:Started' % group_name]) == 0
|
|
|
|
start = datetime.datetime.now()
|
|
is_to_restart = lambda: (datetime.datetime.now() - start).total_seconds() >= 900 * 4
|
|
polling_count = 0
|
|
|
|
while not is_to_restart():
|
|
if polling_count == 0:
|
|
reposition()
|
|
|
|
if not p.poll() is None:
|
|
break
|
|
time.sleep(10)
|
|
polling_count += 1
|
|
|
|
if debug:
|
|
assert subprocess.check_call(['notify-send', '%s:Closing' % group_name]) == 0
|
|
|
|
# assert os.system('wmctrl -i -c %s' % t2) == 0
|
|
assert (
|
|
os.system(
|
|
r"""
|
|
swaymsg '[pid="%d"] kill'
|
|
"""
|
|
% (p.pid,)
|
|
)
|
|
== 0
|
|
)
|
|
|
|
except KeyboardInterrupt:
|
|
assert (
|
|
os.system(
|
|
r"""
|
|
swaymsg '[pid="%d"] kill'
|
|
"""
|
|
% (p.pid,)
|
|
)
|
|
== 0
|
|
)
|
|
break
|
|
except Exception:
|
|
import traceback
|
|
import pprint
|
|
|
|
pprint.pprint(traceback.format_exc())
|
|
finally:
|
|
try:
|
|
p.wait(20)
|
|
except subprocess.TimeoutExpired:
|
|
pprint.pprint([p.pid, '20 seconds timeout', 'kill'])
|
|
p.kill()
|
|
if debug:
|
|
assert subprocess.check_call(['notify-send', '%s:Closed' % group_name]) == 0
|
|
|
|
|
|
def resilient_ethernet(ip_addr, ethernet_device):
|
|
subprocess.check_call(
|
|
r"""
|
|
sudo sh -c '\
|
|
while true; \
|
|
do ping -c 3 -w 3 -W 1 {{IP_ADDR}} || (\
|
|
ip link set {{ETHERNET_DEVICE}} down; \
|
|
ip link set {{ETHERNET_DEVICE}} up; \
|
|
sleep 4; true;\
|
|
); \
|
|
sleep 10; clear; date; \
|
|
done'
|
|
""".replace('{{IP_ADDR}}', ip_addr).replace('{{ETHERNET_DEVICE}}}', ethernet_device),
|
|
shell=True,
|
|
)
|
|
|
|
|
|
def http_server(argv):
|
|
from .commands_typed import os as commands_os
|
|
|
|
assert isinstance(argv, list) and all([isinstance(o, str) for o in argv])
|
|
parser = optparse.OptionParser()
|
|
parser.add_option(
|
|
'--public',
|
|
dest='public',
|
|
action='store_true',
|
|
default=False,
|
|
)
|
|
parser.add_option(
|
|
'--force',
|
|
dest='force',
|
|
action='store_true',
|
|
default=False,
|
|
)
|
|
parser.add_option(
|
|
'--token',
|
|
dest='token',
|
|
type=str,
|
|
default=None,
|
|
)
|
|
parser.add_option(
|
|
'--port',
|
|
dest='port',
|
|
type='int',
|
|
default=80,
|
|
)
|
|
parser.add_option(
|
|
'--no_docker',
|
|
dest='docker',
|
|
action='store_false',
|
|
default=None,
|
|
)
|
|
parser.add_option(
|
|
'-H',
|
|
'--header',
|
|
dest='response_headers',
|
|
type='str',
|
|
action='append',
|
|
default=[],
|
|
)
|
|
parser.add_option(
|
|
'--host',
|
|
dest='host',
|
|
type='str',
|
|
default='127.0.0.1',
|
|
)
|
|
parser.add_option(
|
|
'--prefix',
|
|
dest='prefix',
|
|
type='str',
|
|
default=None,
|
|
)
|
|
options, args = parser.parse_args(argv)
|
|
|
|
assert options.port >= 1
|
|
|
|
try:
|
|
if not options.docker and options.host == '0.0.0.0':
|
|
found: bool = False
|
|
for o in commands_os.interfaces_index():
|
|
if found:
|
|
break
|
|
for o2 in o.addr_info:
|
|
if o2.family == 'inet' and o2.local != '127.0.0.1':
|
|
options.host = o2.local
|
|
logger.info(
|
|
dict(
|
|
host=options.host,
|
|
msg='found',
|
|
)
|
|
)
|
|
found = True
|
|
break
|
|
|
|
assert not socket.inet_aton(options.host) is None
|
|
# subprocess.check_call([
|
|
# 'ping', '-w', '1',
|
|
# options.host
|
|
# ])
|
|
assert options.host in sum([[o2.local for o2 in o.addr_info] for o in commands_os.interfaces_index()], [])
|
|
except Exception:
|
|
raise RuntimeError('invalid ip address %s' % options.host)
|
|
|
|
if options.docker is None:
|
|
options.docker = True
|
|
|
|
index_section = 'autoindex on;'
|
|
|
|
if options.public:
|
|
options.token = 'public'
|
|
|
|
location_section = 'location / {%s}' % index_section
|
|
else:
|
|
if options.token is None:
|
|
options.token = os.urandom(16).hex()
|
|
|
|
if options.docker:
|
|
DATA_DIR = '/usr/share/nginx'
|
|
APP_DIR = '/app'
|
|
CONF_DIR = '/etc/nginx'
|
|
else:
|
|
DATA_DIR = '/opt/nginx/%s/data/' % options.token
|
|
CONF_DIR = '/opt/nginx/%s/conf/' % options.token
|
|
APP_DIR = os.path.abspath(os.path.curdir)
|
|
|
|
if not options.public:
|
|
if not options.prefix is None:
|
|
path = options.prefix + options.token
|
|
else:
|
|
path = options.token
|
|
|
|
logger.info(
|
|
'access url is http://%s:%d/%s/'
|
|
% (
|
|
options.host,
|
|
options.port,
|
|
path,
|
|
)
|
|
)
|
|
|
|
assert all([not re.compile(r'^[A-Za-z-]+ [a-z0-9A-Z-\.]+$').match(o) is None for o in options.response_headers])
|
|
|
|
location_section = ('location / {deny all;}location /%s/ {alias %s/;%s%s}') % (
|
|
path,
|
|
APP_DIR,
|
|
'\n'.join(['add_header %s;' % o for o in options.response_headers]),
|
|
index_section,
|
|
)
|
|
|
|
if options.docker:
|
|
subprocess.check_call(
|
|
r"""
|
|
sudo docker run \
|
|
-p %s:%d:80 \
|
|
-u root \
|
|
-it --entrypoint=/bin/bash \
|
|
-v $PWD:%s:ro \
|
|
--log-driver none \
|
|
nginx:latest \
|
|
-c 'echo "server{listen 80; charset UTF-8; root /app; %s}" > /etc/nginx/conf.d/default.conf; nginx -g "daemon off;"'
|
|
"""
|
|
% (
|
|
options.host,
|
|
options.port,
|
|
APP_DIR,
|
|
location_section,
|
|
),
|
|
shell=True,
|
|
)
|
|
else:
|
|
if os.path.exists(CONF_DIR):
|
|
assert options.force
|
|
shutil.rmtree(CONF_DIR)
|
|
|
|
if os.path.exists(DATA_DIR):
|
|
assert options.force
|
|
shutil.rmtree(DATA_DIR)
|
|
|
|
os.makedirs(CONF_DIR, exist_ok=True)
|
|
os.makedirs(
|
|
os.path.join(
|
|
CONF_DIR,
|
|
'conf.d',
|
|
),
|
|
exist_ok=True,
|
|
)
|
|
os.makedirs(DATA_DIR, exist_ok=True)
|
|
|
|
with io.open(os.path.join(CONF_DIR, 'nginx.conf'), 'w') as f:
|
|
f.write(
|
|
r"""
|
|
|
|
events {
|
|
multi_accept on;
|
|
worker_connections 32;
|
|
}
|
|
|
|
http {
|
|
include /etc/nginx/mime.types;
|
|
default_type application/octet-stream;
|
|
expires off;
|
|
open_file_cache off;
|
|
|
|
log_format main
|
|
'[$time_local][$remote_addr, $http_x_forwarded_for, $t1, $server_name]'
|
|
'[$request_length,$bytes_sent,$request_time]'
|
|
'[$status][$request]'
|
|
'[$http_user_agent][$http_referer]';
|
|
|
|
access_log /dev/null combined;
|
|
access_log /dev/stderr main;
|
|
|
|
include %s/conf.d/*.conf;
|
|
|
|
map $http_upgrade $connection_upgrade {
|
|
default upgrade;
|
|
'' close;
|
|
}
|
|
}
|
|
"""
|
|
% CONF_DIR
|
|
)
|
|
|
|
with io.open(
|
|
os.path.join(
|
|
CONF_DIR,
|
|
'conf.d',
|
|
'default.conf',
|
|
),
|
|
'w',
|
|
) as f:
|
|
f.write(
|
|
r"""
|
|
server {
|
|
server_name %s;
|
|
listen %d;
|
|
charset UTF-8;
|
|
client_max_body_size 2K;
|
|
|
|
set $t1 $remote_addr;
|
|
if ($http_x_forwarded_for)
|
|
{
|
|
set $t1 $http_x_forwarded_for;
|
|
}
|
|
|
|
root %s;
|
|
%s
|
|
}
|
|
"""
|
|
% (
|
|
options.host,
|
|
options.port,
|
|
DATA_DIR,
|
|
location_section,
|
|
)
|
|
)
|
|
|
|
sys.stderr.flush()
|
|
sys.stdout.flush()
|
|
|
|
os.execv(
|
|
'/usr/sbin/nginx',
|
|
[
|
|
'/usr/sbin/nginx',
|
|
'-c',
|
|
os.path.join(CONF_DIR, 'nginx.conf'),
|
|
'-p',
|
|
DATA_DIR,
|
|
'-g',
|
|
'daemon off;',
|
|
],
|
|
)
|
|
|
|
|
|
class pass_ssh_osx_t:
|
|
class kwargs_t:
|
|
class Mode(enum.Enum):
|
|
clipboard = 'clipboard'
|
|
qrcode = 'qrcode'
|
|
|
|
|
|
def pass_ssh_osx(argv):
|
|
assert isinstance(argv, list) and all([isinstance(o, str) for o in argv])
|
|
parser = optparse.OptionParser()
|
|
parser.add_option(
|
|
'--list',
|
|
dest='list',
|
|
default=False,
|
|
action='store_true',
|
|
)
|
|
parser.add_option(
|
|
'--pass_option',
|
|
dest='pass_option',
|
|
action='append',
|
|
default=[],
|
|
type=str,
|
|
help='pass secret path, like --pass_option google.com/login/password/v1',
|
|
)
|
|
parser.add_option(
|
|
'--clipboard_copy',
|
|
dest='clipboard_copy',
|
|
default=None,
|
|
type=str,
|
|
)
|
|
|
|
parser.add_option(
|
|
'--mode',
|
|
dest='_mode',
|
|
choices=[o.value for o in pass_ssh_osx_t.kwargs_t.Mode],
|
|
default=None,
|
|
help='a mode to retrieve the password',
|
|
)
|
|
parser.add_option(
|
|
'--debug',
|
|
dest='debug',
|
|
action='store_true',
|
|
default=False,
|
|
)
|
|
assert sys.platform in ['darwin', 'linux']
|
|
options, args = parser.parse_args(argv)
|
|
|
|
if options._mode is None:
|
|
options._mode = pass_ssh_osx_t.kwargs_t.Mode.clipboard.value
|
|
|
|
options.mode = pass_ssh_osx_t.kwargs_t.Mode(options._mode)
|
|
|
|
if options.clipboard_copy is None:
|
|
if sys.platform == 'linux':
|
|
options.clipboard_copy = 'wl-copy'
|
|
elif sys.platform == 'darwin':
|
|
options.clipboard_copy = 'pbcopy'
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
if len(args) == 0:
|
|
raise RuntimeError('ssh_command is required')
|
|
|
|
if options.debug:
|
|
print(options.pass_option)
|
|
pprint.pprint(args)
|
|
|
|
reset_gpg_agent = r"""
|
|
gpgconf --kill gpg-agent && \
|
|
gpgconf --reload gpg-agent
|
|
"""
|
|
|
|
if not options.list:
|
|
t1 = options.pass_option
|
|
assert len(t1) > 0
|
|
|
|
print('select on of pass names\n%s' % '\n'.join(['%d: %s' % (k, v) for k, v in enumerate(t1)]))
|
|
|
|
while True:
|
|
try:
|
|
t2 = input()
|
|
t3 = int(t2)
|
|
assert t3 >= 0 and t3 < len(t1)
|
|
break
|
|
except AssertionError:
|
|
continue
|
|
|
|
command = r"""
|
|
%s
|
|
gpg \
|
|
--pinentry-mode=ask \
|
|
-q -u $(cat ~/.password-store/.gpg-id) \
|
|
--decrypt \
|
|
~/.password-store/%s.gpg && \
|
|
echo -n '['$?']' && \
|
|
%s
|
|
""" % (
|
|
reset_gpg_agent,
|
|
t1[t3],
|
|
reset_gpg_agent,
|
|
)
|
|
else:
|
|
command = 'pass list | less -R'
|
|
|
|
ssh_command = [
|
|
'ssh',
|
|
'-C',
|
|
'-o',
|
|
'ConnectTimeout 10',
|
|
'-o',
|
|
'ServerAliveInterval 1',
|
|
*args,
|
|
'-t',
|
|
command,
|
|
]
|
|
|
|
if options.debug:
|
|
pprint.pprint(
|
|
dict(
|
|
ssh_command=ssh_command,
|
|
)
|
|
)
|
|
|
|
if options.list:
|
|
subprocess.check_call(ssh_command)
|
|
else:
|
|
|
|
def clipboard_set(text):
|
|
with subprocess.Popen(
|
|
[
|
|
options.clipboard_copy,
|
|
],
|
|
stdin=subprocess.PIPE,
|
|
) as p:
|
|
p.stdin.write(text.encode('utf-8'))
|
|
p.stdin.flush()
|
|
p.stdin.close()
|
|
p.wait(1)
|
|
assert p.poll() == 0
|
|
|
|
with subprocess.Popen(ssh_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p:
|
|
password = None
|
|
last_chunk = None
|
|
|
|
hide_password = False
|
|
pinentry_delimeter = b'\x1b>'
|
|
|
|
def transform_callback(data):
|
|
nonlocal hide_password
|
|
nonlocal pinentry_delimeter
|
|
|
|
data2 = None
|
|
|
|
if not last_chunk is None:
|
|
data = last_chunk['data'] + data
|
|
|
|
if hide_password:
|
|
data2 = b''
|
|
elif pinentry_delimeter in data:
|
|
hide_password = True
|
|
pos = data.rfind(pinentry_delimeter)
|
|
if pos == -1:
|
|
data2 = data
|
|
else:
|
|
data2 = data[: pos + len(pinentry_delimeter)]
|
|
elif data == b'':
|
|
# return b'\r\n'
|
|
return b''
|
|
else:
|
|
data2 = None
|
|
|
|
return data2
|
|
|
|
for chunk in intercept_output(
|
|
current_subprocess=p,
|
|
return_aggregated=True,
|
|
transform_callback=transform_callback,
|
|
real_time=True,
|
|
# timeout=10,
|
|
):
|
|
if chunk['aggregated']:
|
|
last_chunk = chunk
|
|
break
|
|
|
|
assert not p.poll() is None
|
|
|
|
if p.poll() != 0:
|
|
logger.error(p.stderr.read())
|
|
sys.exit(p.poll())
|
|
|
|
assert not last_chunk is None
|
|
assert last_chunk['returncode'] == 0
|
|
|
|
if options.debug:
|
|
pprint.pprint(last_chunk['data'])
|
|
|
|
if last_chunk['data'].endswith('\r\n[0]'.encode('utf-8')) and last_chunk['data'].rfind(pinentry_delimeter) != -1:
|
|
last_line = last_chunk['data'].splitlines()[-2]
|
|
else:
|
|
raise RuntimeError('gpg failure %s' % str(last_chunk['data'][max(last_chunk['data'].find(pinentry_delimeter), -128) :]))
|
|
|
|
pos2 = last_line.rfind(pinentry_delimeter)
|
|
if pos2 == -1:
|
|
last_line2 = last_line
|
|
else:
|
|
last_line2 = last_line[pos2 + len(pinentry_delimeter) :]
|
|
|
|
password = last_line2.decode('utf-8').rstrip('\r\n')
|
|
assert not password is None
|
|
|
|
if options.mode is pass_ssh_osx_t.kwargs_t.Mode.clipboard:
|
|
try:
|
|
clipboard_set(password)
|
|
get_time = lambda: datetime.datetime.now().timestamp()
|
|
start = get_time()
|
|
while True:
|
|
cur = get_time()
|
|
remains = 10 - (cur - start)
|
|
if remains <= 1e-8:
|
|
break
|
|
else:
|
|
print('\r%5.2fs remains' % remains, end='')
|
|
time.sleep(0.1)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
clipboard_set('')
|
|
print('\rcleared cliboard\n', end='')
|
|
elif options.mode is pass_ssh_osx_t.kwargs_t.Mode.qrcode:
|
|
assert (
|
|
subprocess.run(
|
|
r"""
|
|
qrencode -t PNG -o - | feh -
|
|
""",
|
|
input=password.encode('utf-8'),
|
|
shell=True,
|
|
).returncode
|
|
== 0
|
|
)
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
|
|
def vpn(argv: list[str]) -> None:
|
|
python_path: list[str]
|
|
|
|
if (pathlib.Path(__file__).parent / 'env3').exists():
|
|
python_path = [str(pathlib.Path(__file__).parent / 'env3' / 'bin' / 'python3')]
|
|
elif (pathlib.Path(__file__).parent.parent.parent.parent / 'm').exists():
|
|
python_path = [
|
|
str(pathlib.Path(__file__).parent.parent.parent.parent / 'm'),
|
|
'env',
|
|
'--',
|
|
]
|
|
else:
|
|
python_path = [sys.executable]
|
|
|
|
subprocess.check_call(
|
|
[
|
|
'sudo',
|
|
*python_path,
|
|
'-B',
|
|
'-Xfrozen_modules=off',
|
|
'-m',
|
|
'online_fxreader.vpn.vpn',
|
|
*argv,
|
|
]
|
|
)
|
|
|
|
|
|
def player_v1(folder_url, item_id):
|
|
# import sys
|
|
import urllib.parse
|
|
import re
|
|
import subprocess
|
|
import os
|
|
import tqdm
|
|
|
|
t4 = folder_url
|
|
t1 = subprocess.check_output(['curl', '-s', t4]).decode('utf-8')
|
|
t2 = re.compile(r'href=\"(.*\.mp3)\"')
|
|
t3 = [o.group(1) for o in t2.finditer(t1)]
|
|
t5 = ['%s/%s' % (t4, o) for o in t3]
|
|
t6 = item_id
|
|
t9 = range(t6, len(t5))
|
|
with tqdm.tqdm(
|
|
total=len(t5),
|
|
) as progress_bar:
|
|
progress_bar.update(t6)
|
|
for k in t9:
|
|
t7 = t5[k]
|
|
t9 = urllib.parse.unquote(os.path.split(t7)[1])
|
|
progress_bar.set_description('%03d %s' % (k, t9))
|
|
with subprocess.Popen(['ffprobe', '-hide_banner', '-i', t7], stderr=subprocess.PIPE, stdout=subprocess.PIPE) as p:
|
|
p.wait()
|
|
assert p.returncode == 0
|
|
t8 = p.stderr.read().decode('utf-8')
|
|
assert isinstance(t8, str)
|
|
# print(t8)
|
|
with subprocess.Popen(
|
|
['ffplay', '-hide_banner', '-nodisp', '-autoexit', '-loop', '1', t7], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
) as p:
|
|
p.wait()
|
|
assert p.returncode == 0
|
|
progress_bar.update(1)
|
|
|
|
|
|
def numpy_linspace(a, b, count):
|
|
pos = a
|
|
step = (b - a) / count
|
|
steps = []
|
|
|
|
for i in range(count):
|
|
if i == 0:
|
|
pos = a
|
|
elif i == count - 1:
|
|
pos = b
|
|
else:
|
|
pos = a + i * step
|
|
steps.append(pos)
|
|
|
|
return steps
|
|
|
|
|
|
def pm_service(argv):
|
|
parser = optparse.OptionParser()
|
|
parser.add_option(
|
|
'--events',
|
|
dest='events',
|
|
default=[],
|
|
action='append',
|
|
help='pb,tp,kb',
|
|
)
|
|
parser.add_option(
|
|
'--verbose',
|
|
dest='verbose',
|
|
type=str,
|
|
default=None,
|
|
help='true,false',
|
|
)
|
|
options, args = parser.parse_args(argv)
|
|
|
|
if options.verbose is None:
|
|
options.verbose = False
|
|
else:
|
|
val = json.loads(options.verbose)
|
|
assert isinstance(val, bool)
|
|
options.verbose = val
|
|
|
|
if len(options.events) == 0:
|
|
options.events.extend(
|
|
[
|
|
'pb',
|
|
#'tp', 'kb'
|
|
]
|
|
)
|
|
|
|
assert all([o in ['pb', 'tp', 'kb'] for o in options.events])
|
|
|
|
assert sys.platform == 'darwin'
|
|
|
|
wu = 0
|
|
|
|
while True:
|
|
subprocess.check_call(['osascript', '-e', 'tell application "Finder" to sleep'])
|
|
subprocess.check_call(
|
|
['pmset', 'sleepnow'],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
|
|
wu += 1
|
|
|
|
sample = (
|
|
r'2024-03-19 22:00:36.808589+0300 0x4caa7 Default 0x0 102'
|
|
r'0 powerd: [com.apple.powerd:assertions] Process WindowServer.156 TurnedOn '
|
|
r'UserIsActive "com.apple.iohideventsystem.queue.tickle serviceID:10000267f '
|
|
r'service:AppleMultitouchDevice product:Apple Internal Keyboard / Trackpad '
|
|
r'eventType:11" age:00:00:00 id:38654742889 [System: PrevIdle DeclUser kDisp]'
|
|
)
|
|
assert isinstance(sample, str)
|
|
|
|
action = None
|
|
with subprocess.Popen(['log', 'stream'], stdout=subprocess.PIPE) as p:
|
|
for chunk in intercept_output(
|
|
p,
|
|
return_aggregated=False,
|
|
need_lines=True,
|
|
transform_callback=lambda x: b'',
|
|
):
|
|
line = chunk['line'].decode('utf-8')
|
|
# p.stdout.readline().decode('utf-8')
|
|
cmd = None
|
|
if 'powerd' in line:
|
|
cmd = line
|
|
if options.verbose:
|
|
logging.error(json.dumps(dict(line=cmd)))
|
|
|
|
# cmd = subprocess.check_output(r'''
|
|
# log stream | grep --line-buffered -i \
|
|
# -E 'powerd.*TurnedOn.*UserIsActive' | head -n 1
|
|
#''', shell=True).decode('utf-8')
|
|
|
|
if not cmd is None and ('TurnedOn' in cmd or 'PrevIdle' in cmd or 'PMRD: kIOMessageSystemWillPowerOn' in cmd):
|
|
if (
|
|
('AppleMultitouchDevice' in cmd and 'tp' in options.events)
|
|
or ('AppleACPIButton' in cmd and 'pb' in options.events)
|
|
or ('eventType:29' in cmd and 'kb' in options.events)
|
|
):
|
|
action = 'wake-up'
|
|
break
|
|
else:
|
|
action = 'sleep'
|
|
break
|
|
|
|
if options.verbose:
|
|
logging.error(
|
|
json.dumps(
|
|
dict(
|
|
cmd=cmd,
|
|
action=action,
|
|
)
|
|
)
|
|
)
|
|
else:
|
|
print('\r%s wu : %d, la : %s' % (datetime.datetime.now().isoformat(), wu, action), end='')
|
|
|
|
if action == 'wake-up':
|
|
break
|
|
elif action == 'sleep':
|
|
continue
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
print('')
|
|
|
|
|
|
def scrap_yt_music(argv: list[str]) -> None:
|
|
parser = optparse.OptionParser()
|
|
parser.add_option(
|
|
'--verbose',
|
|
dest='verbose',
|
|
type=str,
|
|
default=None,
|
|
help='true,false',
|
|
)
|
|
parser.add_option(
|
|
'-l',
|
|
'--library_path',
|
|
dest='library_path',
|
|
type=str,
|
|
default=None,
|
|
)
|
|
options, args = parser.parse_args(argv)
|
|
|
|
if options.library_path is None:
|
|
options.library_path = os.path.abspath(os.path.curdir)
|
|
|
|
if options.verbose is None:
|
|
options.verbose = False
|
|
else:
|
|
val = json.loads(options.verbose)
|
|
assert isinstance(val, bool)
|
|
options.verbose = val
|
|
|
|
import aiohttp.web
|
|
|
|
def http_events(context, res_cb):
|
|
data = []
|
|
|
|
async def handle(request):
|
|
data.append(request.rel_url.query.copy())
|
|
|
|
res_cb(event=data[-1], events=data)
|
|
if len(data) > 128:
|
|
del data[:128]
|
|
|
|
return aiohttp.web.Response(text='ok')
|
|
|
|
async def serve():
|
|
logging.info('http_events starting')
|
|
app = aiohttp.web.Application()
|
|
app.add_routes([aiohttp.web.get('/status', handle)])
|
|
runner = aiohttp.web.AppRunner(
|
|
app,
|
|
handle_signals=False,
|
|
)
|
|
await runner.setup()
|
|
site = aiohttp.web.TCPSite(runner, host='127.0.0.1', port=8877)
|
|
await site.start()
|
|
|
|
logging.info('http_events started')
|
|
|
|
while True:
|
|
await asyncio.sleep(1)
|
|
if context['shutdown']:
|
|
break
|
|
|
|
await runner.cleanup()
|
|
|
|
logging.info('http_events done')
|
|
|
|
asyncio.run(serve())
|
|
|
|
# aiohttp.web.run_app(
|
|
# app,
|
|
# host='127.0.0.1',
|
|
# port=8877,
|
|
# handle_signals=False,
|
|
# )
|
|
|
|
# while True:
|
|
# data.append(
|
|
# subprocess.check_output(r'''
|
|
# nc -w 1 -l 127.0.0.1 8877 | head -n 1
|
|
# ''', shell=True,)
|
|
# )
|
|
|
|
def audio_recorder(context):
|
|
current_name = None
|
|
|
|
p = None
|
|
|
|
try:
|
|
while True:
|
|
with context['track_cv']:
|
|
context['track_cv'].wait(1)
|
|
|
|
if context['track_name'] != current_name:
|
|
logging.info('audio_record, track changed, started')
|
|
if not p is None:
|
|
logging.info('audio_record, track changed, terminating')
|
|
p.terminate()
|
|
p.wait()
|
|
p = None
|
|
logging.info('audio_record, track changed, terminated')
|
|
current_name = context['track_name']
|
|
|
|
if context['shutdown']:
|
|
if not p is None:
|
|
p.terminate()
|
|
break
|
|
|
|
if p is None and not current_name is None:
|
|
output_name = os.path.join(options.library_path, '%s.mp3' % current_name)
|
|
logging.info('audio_record, new recording')
|
|
p = subprocess.Popen(
|
|
['sox', '-d', output_name],
|
|
stdout=subprocess.DEVNULL,
|
|
stdin=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
logging.info(json.dumps(dict(output_name=output_name)))
|
|
except Exception:
|
|
logging.error(traceback.format_exc())
|
|
finally:
|
|
if not p is None:
|
|
p.terminate()
|
|
|
|
class Context(TypedDict):
|
|
http_on_event: Callable[..., None]
|
|
shutdown: bool
|
|
workers: list[threading.Thread]
|
|
track_cv: threading.Condition
|
|
main_cv: threading.Condition
|
|
track_name: Optional[str]
|
|
|
|
context: Context = dict(
|
|
http_on_event=lambda *args, **kwargs: None,
|
|
shutdown=False,
|
|
workers=[],
|
|
track_cv=threading.Condition(),
|
|
main_cv=threading.Condition(),
|
|
track_name=None,
|
|
)
|
|
|
|
context['workers'].extend(
|
|
[
|
|
threading.Thread(
|
|
target=functools.partial(
|
|
http_events,
|
|
context=context,
|
|
res_cb=lambda *args, **kwargs: context['http_on_event'](*args, **kwargs),
|
|
)
|
|
),
|
|
threading.Thread(
|
|
target=functools.partial(
|
|
audio_recorder,
|
|
context=context,
|
|
)
|
|
),
|
|
]
|
|
)
|
|
|
|
def http_on_event(event, events):
|
|
with context['track_cv']:
|
|
if 'title' in event and event['title'].strip() != '':
|
|
context['track_name'] = str(event['title'])[:128].replace('\n', '')
|
|
else:
|
|
context['track_name'] = None
|
|
|
|
logging.info(event)
|
|
|
|
context['http_on_event'] = http_on_event
|
|
|
|
print(r"""
|
|
https://github.com/ExistentialAudio/BlackHole/wiki/Multi-Output-Device#5-set-audio-output-to-multi-output-device
|
|
|
|
Open Youtube Music,
|
|
and launch the following JS script:
|
|
```js
|
|
|
|
|
|
(function(){
|
|
let timer = null;
|
|
let is_first = true;
|
|
let last_play = null;
|
|
function last_play_update() {last_play = (new Date());}
|
|
let should_stop = () => (((new Date()).valueOf() - last_play.valueOf()) > 5 * 1000);
|
|
timer = setInterval(() => {
|
|
let is_playing = () => $$('#play-pause-button')[0].title == 'Pause';
|
|
let title = () => encodeURIComponent(
|
|
$$('.ytmusic-player-bar.middle-controls')[0].innerText
|
|
);
|
|
let update_status = (query) => fetch('http://127.0.0.1:8877/status?' + query);
|
|
|
|
if (is_playing())
|
|
{
|
|
last_play_update();
|
|
is_first = false;
|
|
update_status('title=' + title());
|
|
}
|
|
else if (!is_first)
|
|
{
|
|
update_status('')
|
|
if (should_stop())
|
|
{
|
|
console.log('should stop');
|
|
clearInterval(timer);
|
|
}
|
|
}
|
|
}, 1000);
|
|
})();
|
|
|
|
```
|
|
""")
|
|
|
|
for w in context['workers']:
|
|
w.start()
|
|
|
|
# context['main_cv'] = threading.Condition()
|
|
|
|
def on_interrupt(*args, **kwargs):
|
|
logging.info('on_interrupt')
|
|
with context['main_cv']:
|
|
context['main_cv'].notify()
|
|
|
|
signal.signal(
|
|
signal.SIGINT,
|
|
on_interrupt,
|
|
)
|
|
signal.signal(
|
|
signal.SIGTERM,
|
|
on_interrupt,
|
|
)
|
|
|
|
with context['main_cv']:
|
|
context['main_cv'].wait()
|
|
|
|
with context['main_cv']:
|
|
context['shutdown'] = True
|
|
context['main_cv'].notify()
|
|
with context['track_cv']:
|
|
context['track_cv'].notify()
|
|
|
|
for o in context['workers']:
|
|
o.join()
|
|
|
|
|
|
def loginctl(argv: list[str]) -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
'--action',
|
|
choices=[
|
|
'lock-session',
|
|
],
|
|
)
|
|
|
|
options = parser.parse_args(argv)
|
|
|
|
if options.action == 'lock-session':
|
|
subprocess.check_call(
|
|
r"""
|
|
loginctl list-sessions -j | jq -r ".[] | select(.uid==$UID) | \
|
|
.session" | loginctl lock-session
|
|
""",
|
|
shell=True,
|
|
timeout=1,
|
|
)
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
|
|
def desktop_services(argv):
|
|
parser = optparse.OptionParser()
|
|
parser.add_option(
|
|
'--background_image',
|
|
dest='background_image',
|
|
default=None,
|
|
type=str,
|
|
)
|
|
parser.add_option(
|
|
'--cpufreq',
|
|
dest='cpufreq',
|
|
default=None,
|
|
type=int,
|
|
help='0 - mac book air (no turbo boost, max pct 30, every 4 seconds',
|
|
)
|
|
parser.add_option(
|
|
'--cpufreq-action',
|
|
dest='cpufreq_action',
|
|
default=None,
|
|
choices=[
|
|
'performance',
|
|
'powersave',
|
|
],
|
|
# type=str,
|
|
)
|
|
parser.add_option(
|
|
'--battery',
|
|
dest='battery',
|
|
default=None,
|
|
type=int,
|
|
help='0 - battery check with sleep <10%, every 10 seconds',
|
|
)
|
|
parser.add_option(
|
|
'--backlight-increase',
|
|
dest='backlight_increase',
|
|
default=False,
|
|
action='store_true',
|
|
help='increase keyboard backlight',
|
|
)
|
|
parser.add_option(
|
|
'--backlight-type',
|
|
dest='backlight_type',
|
|
default=[],
|
|
action='append',
|
|
help='backlight type, like keyboard, output',
|
|
)
|
|
parser.add_option(
|
|
'--backlight-decrease',
|
|
dest='backlight_decrease',
|
|
default=False,
|
|
action='store_true',
|
|
help='decrease keyboard backlight',
|
|
)
|
|
parser.add_option(
|
|
'--backlight_service',
|
|
dest='backlight_service',
|
|
action='store_true',
|
|
default=False,
|
|
help='enable backlight_service',
|
|
)
|
|
parser.add_option(
|
|
'--polkit_service',
|
|
dest='polkit_service',
|
|
action='store_true',
|
|
default=False,
|
|
help='enable polkit_service',
|
|
)
|
|
|
|
options, args = parser.parse_args(argv)
|
|
|
|
class VLC:
|
|
@classmethod
|
|
def vlc_is_playing_fullscreen(cls):
|
|
import subprocess
|
|
import json
|
|
# import sys
|
|
# import pprint
|
|
|
|
t2 = []
|
|
try:
|
|
t1 = subprocess.check_output(['swaymsg', '-t', 'get_tree']).decode('utf-8')
|
|
t2 = json.loads(t1)
|
|
except Exception:
|
|
logging.error(traceback.format_exc())
|
|
|
|
def walk(o, cb):
|
|
if isinstance(o, dict):
|
|
cb(o)
|
|
for k, v in o.items():
|
|
walk(
|
|
v,
|
|
cb,
|
|
)
|
|
elif isinstance(o, list):
|
|
cb(o)
|
|
for o2 in o:
|
|
walk(
|
|
o2,
|
|
cb,
|
|
)
|
|
else:
|
|
cb(o)
|
|
|
|
t3 = []
|
|
|
|
walk(
|
|
t2,
|
|
lambda o: [
|
|
t3.append(o)
|
|
if isinstance(o, dict)
|
|
and 'fullscreen_mode' in o
|
|
and o['fullscreen_mode'] == 1
|
|
and 'window_properties' in o
|
|
and 'class' in o['window_properties']
|
|
and o['window_properties']['class'] == 'vlc'
|
|
else None
|
|
],
|
|
)
|
|
|
|
t4 = False
|
|
|
|
try:
|
|
t4 = (
|
|
subprocess.check_output(
|
|
['playerctl', '-p', 'vlc', 'status'],
|
|
timeout=1,
|
|
)
|
|
.decode('utf-8')
|
|
.strip()
|
|
== 'Playing'
|
|
)
|
|
except Exception:
|
|
logging.error(traceback.format_exc())
|
|
|
|
# pprint.pprint(t3)
|
|
|
|
return len(t3) > 0 and t4
|
|
|
|
class Battery:
|
|
def __init__(
|
|
self,
|
|
should_start=None,
|
|
):
|
|
if should_start is None:
|
|
should_start = False
|
|
|
|
assert isinstance(should_start, bool)
|
|
|
|
self.last_check = None
|
|
self.period = 10
|
|
self.is_running = should_start
|
|
|
|
def check_is_needed(self):
|
|
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
|
|
is_needed = None
|
|
|
|
if self.last_check is None:
|
|
is_needed = True
|
|
else:
|
|
if (now - self.last_check).total_seconds() >= self.period:
|
|
is_needed = True
|
|
else:
|
|
is_needed = False
|
|
|
|
if is_needed:
|
|
self.last_check = now
|
|
|
|
return is_needed
|
|
|
|
def run(self):
|
|
while True:
|
|
self.check()
|
|
|
|
time.sleep(self.period)
|
|
|
|
def terminate(self):
|
|
self.is_running = False
|
|
|
|
def wait(self, *args, **kwargs):
|
|
if self.is_running:
|
|
raise NotImplementedError
|
|
|
|
def poll(self):
|
|
if self.is_running:
|
|
return None
|
|
else:
|
|
return 0
|
|
|
|
@property
|
|
def percentage_low(self) -> int:
|
|
try:
|
|
return int(
|
|
subprocess.check_output(
|
|
r"""
|
|
cat /etc/UPower/UPower.conf | grep -Po '^PercentageLow=\d+'
|
|
""",
|
|
shell=True,
|
|
)
|
|
.decode('utf-8')
|
|
.strip()
|
|
.split('=')[1]
|
|
)
|
|
except:
|
|
logger.exception('')
|
|
return 15
|
|
|
|
@property
|
|
def percentage_critical(self) -> int:
|
|
try:
|
|
return int(
|
|
subprocess.check_output(
|
|
r"""
|
|
cat /etc/UPower/UPower.conf | grep -Po '^PercentageCritical=\d+'
|
|
""",
|
|
shell=True,
|
|
)
|
|
.decode('utf-8')
|
|
.strip()
|
|
.split('=')[1]
|
|
)
|
|
except:
|
|
logger.exception('')
|
|
return 10
|
|
|
|
def check(self):
|
|
try:
|
|
if not self.check_is_needed():
|
|
return
|
|
|
|
t1 = subprocess.check_output(
|
|
['upower', '-d'],
|
|
timeout=1,
|
|
).decode('utf-8')
|
|
t2 = [o for o in t1.splitlines() if 'percentage' in o.lower()]
|
|
t4 = [o for o in t1.splitlines() if 'state' in o.lower()]
|
|
t3 = float(t2[0].split(':')[1].strip()[:-1])
|
|
t5 = any(['discharging' in o.lower() for o in t4])
|
|
if t3 < self.percentage_critical and t5:
|
|
logging.error(json.dumps(dict(msg='too low', t3=t3, t5=t5)))
|
|
subprocess.check_call(['systemctl', 'suspend'])
|
|
elif t3 < self.percentage_low and t5:
|
|
msg = 'battery near low'
|
|
logging.error(json.dumps(dict(msg=msg, t3=t3, t5=t5)))
|
|
subprocess.check_call(
|
|
[
|
|
'notify-send',
|
|
'-t',
|
|
'%d' % (5 * 1000),
|
|
msg,
|
|
'% 5.2f' % t3,
|
|
]
|
|
)
|
|
else:
|
|
pass
|
|
print('\r%s % 5.2f%% %s' % (datetime.datetime.now().isoformat(), t3, str(t5)), end='')
|
|
except Exception:
|
|
logging.error(traceback.format_exc())
|
|
|
|
class Backlight:
|
|
class Direction(enum.Enum):
|
|
increase = 'increase'
|
|
decrease = 'decrease'
|
|
absolute = 'absolute'
|
|
get_state = 'get_state'
|
|
|
|
class Mode(enum.Enum):
|
|
light = 'light'
|
|
|
|
def __init__(self):
|
|
self.state = []
|
|
self.dpms = Backlight.dpms_get()
|
|
|
|
@classmethod
|
|
def dpms_get(cls):
|
|
try:
|
|
t1 = subprocess.check_output(['swaymsg', '-r', '-t', 'get_outputs'], timeout=1)
|
|
t2 = t1.decode('utf-8')
|
|
t3 = json.loads(t2)
|
|
t4 = [
|
|
dict(
|
|
id=o['id'],
|
|
name=o['name'],
|
|
dpms=o['dpms'],
|
|
)
|
|
for o in t3
|
|
]
|
|
|
|
return any([o['dpms'] for o in t4])
|
|
except Exception:
|
|
return True
|
|
|
|
def check(self):
|
|
try:
|
|
new_dpms = Backlight.dpms_get()
|
|
if new_dpms != self.dpms:
|
|
logging.info(
|
|
json.dumps(
|
|
dict(
|
|
module='backlight',
|
|
action='new_dpms',
|
|
dpms=self.dpms,
|
|
new_dpms=new_dpms,
|
|
)
|
|
)
|
|
)
|
|
if new_dpms:
|
|
Backlight.enable(self.state)
|
|
else:
|
|
self.state = Backlight.change(
|
|
Backlight.Direction.get_state,
|
|
)
|
|
logging.info(
|
|
json.dumps(
|
|
dict(
|
|
state=pprint.pformat(
|
|
self.state,
|
|
width=1e8,
|
|
compact=True,
|
|
),
|
|
action='disable',
|
|
)
|
|
)
|
|
)
|
|
Backlight.disable()
|
|
self.dpms = new_dpms
|
|
except Exception:
|
|
logging.error(traceback.format_exc())
|
|
|
|
@classmethod
|
|
def get_state(cls):
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def set_state(cls):
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def disable(cls):
|
|
return cls.change(
|
|
cls.Direction.absolute,
|
|
0,
|
|
)
|
|
|
|
@classmethod
|
|
def enable(
|
|
cls,
|
|
state,
|
|
):
|
|
res = []
|
|
for device_state in state:
|
|
res.append(
|
|
cls.change(
|
|
direction=cls.Direction.absolute,
|
|
value=device_state['value'],
|
|
device_name=device_state['device_name'],
|
|
)
|
|
)
|
|
return res
|
|
|
|
@classmethod
|
|
def change(
|
|
cls,
|
|
direction,
|
|
value=None,
|
|
devices=None,
|
|
device_name=None,
|
|
types=None,
|
|
):
|
|
assert isinstance(direction, Backlight.Direction)
|
|
|
|
state = []
|
|
devices_all = dict(
|
|
smc_kbd=dict(
|
|
sysfs_path='sysfs/leds/smc::kbd_backlight',
|
|
),
|
|
intel_backlight=dict(
|
|
sysfs_path='sysfs/backlight/intel_backlight',
|
|
),
|
|
)
|
|
|
|
if devices is None:
|
|
devices = []
|
|
|
|
if not device_name is None:
|
|
devices.append(device_name)
|
|
|
|
if len(devices) == 0:
|
|
if types is None:
|
|
types = [
|
|
'keyboard',
|
|
'output',
|
|
]
|
|
|
|
for current_type in types:
|
|
if current_type == 'keyboard':
|
|
devices.extend(['smc_kbd'])
|
|
elif current_type == 'output':
|
|
devices.extend(
|
|
[
|
|
'intel_backlight',
|
|
]
|
|
)
|
|
else:
|
|
raise NotImplementedError
|
|
else:
|
|
assert types is None
|
|
|
|
devices2 = list(set(devices))
|
|
|
|
if sys.platform == 'linux':
|
|
assert all([o in devices_all for o in devices2])
|
|
|
|
leds = [
|
|
o.strip()
|
|
for o in subprocess.check_output(
|
|
['light', '-L'],
|
|
timeout=1,
|
|
)
|
|
.decode('utf-8')
|
|
.splitlines()[1:]
|
|
]
|
|
|
|
for current_device_name in devices2:
|
|
device = devices_all[current_device_name]
|
|
|
|
sysfs_path = device['sysfs_path']
|
|
|
|
if not sysfs_path in leds:
|
|
raise NotImplementedError
|
|
|
|
extra_args = []
|
|
if value is None:
|
|
value = 20.0
|
|
|
|
value2 = max(float(value), 0.0)
|
|
|
|
assert isinstance(value2, float) and value >= -1e-8
|
|
|
|
if direction == cls.Direction.increase:
|
|
extra_args.extend(['-A', '%f' % value2])
|
|
elif direction == cls.Direction.decrease:
|
|
extra_args.extend(['-U', '%f' % value2])
|
|
elif direction == cls.Direction.absolute:
|
|
extra_args.extend(['-S', '%f' % value2])
|
|
elif direction == cls.Direction.get_state:
|
|
pass
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
get_current = lambda: float(
|
|
subprocess.check_output(
|
|
[
|
|
'light',
|
|
'-G',
|
|
'-s',
|
|
sysfs_path,
|
|
],
|
|
timeout=1,
|
|
).decode('utf-8')
|
|
)
|
|
|
|
if not (direction == cls.Direction.get_state):
|
|
old_value = get_current()
|
|
|
|
value_steps = None
|
|
|
|
if direction == cls.Direction.decrease:
|
|
value_steps = numpy_linspace(
|
|
old_value,
|
|
max(old_value - value2, 0),
|
|
10,
|
|
)
|
|
elif direction == cls.Direction.increase:
|
|
value_steps = numpy_linspace(
|
|
old_value,
|
|
min(old_value + value2, 100),
|
|
10,
|
|
)
|
|
elif direction == cls.Direction.absolute:
|
|
value_steps = numpy_linspace(
|
|
old_value,
|
|
min(
|
|
max(
|
|
0,
|
|
value2,
|
|
),
|
|
100,
|
|
),
|
|
10,
|
|
)
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
for current_value in value_steps:
|
|
subprocess.check_call(
|
|
[
|
|
'light',
|
|
'-v',
|
|
'3',
|
|
'-s',
|
|
sysfs_path,
|
|
'-S',
|
|
'%f' % current_value,
|
|
],
|
|
stderr=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
time.sleep(0.05)
|
|
|
|
state.append(
|
|
dict(
|
|
mode=cls.Mode.light,
|
|
device_path=sysfs_path,
|
|
device_name=current_device_name,
|
|
value=get_current(),
|
|
)
|
|
)
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
return state
|
|
|
|
class Cpufreq:
|
|
@classmethod
|
|
def profile(
|
|
cls,
|
|
) -> Literal[
|
|
'applesmc.768',
|
|
'intel_pstate',
|
|
]:
|
|
if os.path.exists('/sys/bus/platform/devices/applesmc.768'):
|
|
return 'applesmc.768'
|
|
if os.path.exists('/sys/devices/system/cpu/intel_pstate/no_turbo'):
|
|
return 'intel_pstate'
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def powersave(cls):
|
|
if cls.profile() == 'applesmc.768':
|
|
subprocess.check_call(
|
|
r"""
|
|
#echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor;
|
|
#echo powersave | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor;
|
|
#echo 1 > /sys/bus/platform/devices/applesmc.768/fan1_manual;
|
|
#echo 2000 > /sys/bus/platform/devices/applesmc.768/fan1_output;
|
|
|
|
echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor;
|
|
echo 30 > /sys/devices/system/cpu/intel_pstate/max_perf_pct;
|
|
echo 900000 | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq;
|
|
echo schedutil | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor;
|
|
|
|
echo 1 > /sys/bus/platform/devices/applesmc.768/fan1_manual;
|
|
echo 2000 > /sys/bus/platform/devices/applesmc.768/fan1_output;
|
|
""",
|
|
shell=True,
|
|
)
|
|
elif cls.profile() == 'intel_pstate':
|
|
subprocess.check_call(
|
|
r"""
|
|
echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor;
|
|
echo 40 > /sys/devices/system/cpu/intel_pstate/max_perf_pct;
|
|
echo 900000 | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq;
|
|
echo schedutil | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor;
|
|
""",
|
|
shell=True,
|
|
)
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def performance(cls):
|
|
if cls.profile() == 'applesmc.768':
|
|
subprocess.check_call(
|
|
r"""
|
|
#echo powersave | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor;
|
|
#echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor;
|
|
|
|
echo powersave | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor;
|
|
echo 60 > /sys/devices/system/cpu/intel_pstate/max_perf_pct;
|
|
echo 1200000 | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq;
|
|
echo schedutil | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor;
|
|
|
|
echo 1 > /sys/bus/platform/devices/applesmc.768/fan1_manual;
|
|
echo 6500 > /sys/bus/platform/devices/applesmc.768/fan1_output;
|
|
""",
|
|
shell=True,
|
|
)
|
|
elif cls.profile() == 'intel_pstate':
|
|
subprocess.check_call(
|
|
r"""
|
|
echo powersave | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor;
|
|
echo 60 > /sys/devices/system/cpu/intel_pstate/max_perf_pct;
|
|
echo 1200000 | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq;
|
|
echo schedutil | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor;
|
|
""",
|
|
shell=True,
|
|
)
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
if options.backlight_increase or options.backlight_decrease:
|
|
if options.backlight_increase:
|
|
direction = Backlight.Direction.increase
|
|
elif options.backlight_decrease:
|
|
direction = Backlight.Direction.decrease
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
Backlight.change(
|
|
direction=direction,
|
|
types=options.backlight_type,
|
|
)
|
|
|
|
return
|
|
|
|
elif not options.cpufreq_action is None:
|
|
if options.cpufreq_action == 'performance':
|
|
Cpufreq.performance()
|
|
elif options.cpufreq_action == 'powersave':
|
|
Cpufreq.powersave()
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
return
|
|
else:
|
|
pass
|
|
|
|
# logger.info(dict(
|
|
# environ=os.environ,
|
|
# #session=subprocess.check_output(['loginctl', 'show-session'], timeout=1,),
|
|
# ))
|
|
|
|
sway_sock_res = sway_sock(True)
|
|
|
|
if sway_sock_res:
|
|
os.environ['SWAYSOCK'] = sway_sock_res
|
|
|
|
assert (
|
|
all(
|
|
[
|
|
env_name in os.environ
|
|
for env_name in [
|
|
'GTK_IM_MODULE',
|
|
'XMODIFIERS',
|
|
'QT_IM_MODULE',
|
|
#'I3SOCK',
|
|
#'XDG_SEAT',
|
|
'SWAYSOCK',
|
|
'WAYLAND_DISPLAY',
|
|
]
|
|
]
|
|
)
|
|
and os.environ['SWAYSOCK'] == sway_sock()
|
|
)
|
|
services = []
|
|
|
|
shutdown = False
|
|
|
|
def on_interrupt(*args, **kwargs):
|
|
logging.info('blah')
|
|
nonlocal shutdown
|
|
shutdown = True
|
|
|
|
signal.signal(
|
|
signal.SIGINT,
|
|
on_interrupt,
|
|
)
|
|
signal.signal(
|
|
signal.SIGTERM,
|
|
on_interrupt,
|
|
)
|
|
|
|
try:
|
|
if options.cpufreq == 0:
|
|
# logging.info('launching cpufreq, need sudo')
|
|
# subprocess.check_call(['sudo', 'whoami'])
|
|
|
|
services.append(
|
|
subprocess.Popen(
|
|
r"""
|
|
exec sh -c 'echo cpufreq, user; whoami;
|
|
while [[ -a /proc/{pid} ]]; do
|
|
#echo passive > /sys/devices/system/cpu/intel_pstate/status;
|
|
echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo;
|
|
#echo 40 > /sys/devices/system/cpu/intel_pstate/max_perf_pct;
|
|
for cpu_path in /sys/devices/system/cpu/cpu?; do
|
|
#online-fxreader-pr34-udev --device $cpu_path;
|
|
#echo 900000 > $cpu_path/cpufreq/scaling_max_freq;
|
|
#echo schedutil > $cpu_path/cpufreq/scaling_governor;
|
|
true;
|
|
done;
|
|
sleep 10;
|
|
done;'
|
|
""".format(pid=os.getpid()),
|
|
shell=True,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
stdin=subprocess.DEVNULL,
|
|
)
|
|
)
|
|
|
|
class start_swayidle:
|
|
def __init__(self):
|
|
swaylock_cmd = [
|
|
'swaylock',
|
|
'-f',
|
|
'-d',
|
|
]
|
|
if not options.background_image is None:
|
|
swaylock_cmd.extend(
|
|
[
|
|
'-i',
|
|
'"%s"' % options.background_image,
|
|
]
|
|
)
|
|
|
|
self.commands = dict(
|
|
swaylock_cmd2=' '.join(swaylock_cmd),
|
|
timeout1='echo timeout1; swaymsg "output * dpms off";',
|
|
lock='echo lock; pkill --signal SIGUSR1 swayidle;',
|
|
unlock='echo unlock; pkill --signal SIGINT swaylock; swaymsg "output * dpms on";',
|
|
unlock2='pkill --signal SIGINT swaylock;',
|
|
resume='echo resume; swaymsg "output * dpms on";',
|
|
before_sleep='echo before_sleep; commands loginctl lock-session;',
|
|
# before_sleep='echo blah;',
|
|
after_resume='echo after_resume; pkill --signal SIGUSR1 swayidle;',
|
|
)
|
|
self.last_force_idle = None
|
|
self.commands.update(timeout2='echo timeout2; {swaylock_cmd};'.format(swaylock_cmd=self.commands['swaylock_cmd2']))
|
|
self.swayidle = subprocess.Popen(
|
|
r"""
|
|
exec swayidle -d -w \
|
|
timeout 300 'echo t1; read;' \
|
|
resume 'echo t5; ' \
|
|
timeout 900 'echo t4; read;' \
|
|
resume 'echo t5; ' \
|
|
lock 'echo t2; read;' \
|
|
unlock 'echo t3;' \
|
|
before-sleep 'echo t6; read;' \
|
|
after-resume 'echo t7; read;' 2>&1
|
|
""",
|
|
shell=True,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL,
|
|
restore_signals=False,
|
|
preexec_fn=lambda: os.setpgrp(),
|
|
)
|
|
self.output = intercept_output(
|
|
self.swayidle,
|
|
real_time=True,
|
|
transform_callback=lambda x: [logging.info(x), b''][-1],
|
|
)
|
|
self.events = []
|
|
self.last_skip_loop = None
|
|
self.data = []
|
|
if options.backlight_service:
|
|
self.backlight = Backlight()
|
|
else:
|
|
self.backlight = None
|
|
self.bg = None
|
|
self.bg_terminate = False
|
|
|
|
def skip_loop_long_ago(self):
|
|
if self.last_skip_loop is None or (datetime.datetime.now() - self.last_skip_loop).total_seconds() >= 30:
|
|
self.last_skip_loop = datetime.datetime.now()
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def background_check(self):
|
|
if (self.bg is None or not self.bg.poll() is None) and not self.bg_terminate:
|
|
if not options.background_image is None:
|
|
self.bg = subprocess.Popen(
|
|
[
|
|
'swaybg',
|
|
'--output',
|
|
'*',
|
|
'--image',
|
|
options.background_image,
|
|
'--mode',
|
|
'fill',
|
|
]
|
|
)
|
|
|
|
def background_terminate(self, *args, **kwargs):
|
|
if not self.bg is None:
|
|
self.bg_terminate = True
|
|
self.bg.terminate(*args, **kwargs)
|
|
|
|
def poll(self):
|
|
return self.swayidle.poll()
|
|
|
|
def release_lock(self):
|
|
self.swayidle.stdin.write(b'\n')
|
|
self.swayidle.stdin.flush()
|
|
|
|
def force_idle(self):
|
|
if self.last_force_idle is None or (datetime.datetime.now() - self.last_force_idle).total_seconds() >= 10:
|
|
self.last_force_idle = datetime.datetime.now()
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def terminate(self, *args, **kwargs):
|
|
self.background_terminate()
|
|
|
|
return self.swayidle.terminate(*args, **kwargs)
|
|
|
|
def wait(self, *args, **kwargs):
|
|
return self.swayidle.wait(*args, **kwargs)
|
|
|
|
def kill(self):
|
|
return self.swayidle.kill()
|
|
|
|
def dpms(self, direction):
|
|
assert direction in ['on', 'off']
|
|
|
|
raise NotImplementedError
|
|
|
|
def check(self):
|
|
new_events = []
|
|
|
|
class event_t(enum.Enum):
|
|
idle_state = 'idle state'
|
|
active_state = 'active state'
|
|
|
|
while True:
|
|
if self.output is None:
|
|
break
|
|
|
|
chunk = next(self.output)
|
|
|
|
if chunk['aggregated']:
|
|
self.output = None
|
|
continue
|
|
|
|
if len(chunk['data']) == 0:
|
|
break
|
|
|
|
self.data.append(chunk)
|
|
|
|
if b'\n' in chunk['data']:
|
|
total = b''.join([o['data'] for o in self.data]).decode('utf-8')
|
|
sep_pos = total.rfind('\n')
|
|
lines = total[:sep_pos].splitlines()
|
|
self.data = [
|
|
dict(
|
|
data=total[sep_pos:].encode('utf-8'),
|
|
aggregated=False,
|
|
)
|
|
]
|
|
for line in lines:
|
|
if event_t.idle_state.value in line:
|
|
line = event_t.idle_state.value
|
|
elif event_t.active_state.value in line:
|
|
line = event_t.active_state.value
|
|
else:
|
|
pass
|
|
|
|
if line in [
|
|
't1',
|
|
't2',
|
|
't3',
|
|
't4',
|
|
't5',
|
|
't5',
|
|
't6',
|
|
't7',
|
|
event_t.idle_state.value,
|
|
event_t.active_state.value,
|
|
]:
|
|
new_events.append(line)
|
|
|
|
def retry(cb, cnt=None):
|
|
if cnt is None:
|
|
cnt = 10
|
|
|
|
i = 0
|
|
while True:
|
|
logging.info('retry i = %d, cnt = %d' % (i, cnt))
|
|
|
|
if not (subprocess.call(['swaymsg', '-t', 'get_version']) == 0):
|
|
continue
|
|
|
|
if cb() == 0:
|
|
break
|
|
|
|
time.sleep(0.5)
|
|
|
|
i += 1
|
|
|
|
if len(new_events) > 0 or len(self.events) > 0 and self.skip_loop_long_ago():
|
|
self.events.extend(new_events)
|
|
|
|
skip_loop = False
|
|
|
|
if all([o in ['t1', 't4'] for o in self.events]) and VLC.vlc_is_playing_fullscreen() and self.backlight.dpms:
|
|
skip_loop = True
|
|
logging.info(
|
|
'skip loop, %s'
|
|
% (
|
|
[
|
|
json.dumps(self.events),
|
|
self.backlight.dpms,
|
|
VLC.vlc_is_playing_fullscreen(),
|
|
self.events,
|
|
new_events,
|
|
],
|
|
)
|
|
)
|
|
elif len(new_events) == 0 and len(self.events) > 1 and all([o in ['t1', 't4'] for o in self.events]):
|
|
self.events = ['t4']
|
|
elif len(self.events) > 1 and (self.events == ['t1', 't4', 't5', 't5'] or self.events == ['t1', 't5', 't5'] or self.events == ['t1', 't5']):
|
|
for o in new_events:
|
|
self.release_lock()
|
|
|
|
self.events = []
|
|
|
|
for o in self.events:
|
|
if skip_loop:
|
|
self.release_lock()
|
|
continue
|
|
|
|
if o == 't1':
|
|
# if self.force_idle():
|
|
# subprocess.check_call(self.commands['lock'], shell=True)
|
|
logging.info('started t1')
|
|
if self.force_idle():
|
|
subprocess.check_call(self.commands['timeout1'], shell=True)
|
|
logging.info('done t1')
|
|
self.release_lock()
|
|
elif o == 't2':
|
|
logging.info('started lock')
|
|
if self.force_idle():
|
|
custom_notify(
|
|
title='swayidle',
|
|
msg='loginctl lock started',
|
|
)
|
|
while True:
|
|
if not subprocess.call(self.commands['lock'], shell=True) == 0:
|
|
continue
|
|
if not subprocess.call(self.commands['timeout2'], shell=True) == 0:
|
|
# continue
|
|
pass
|
|
if not subprocess.call(self.commands['timeout1'], shell=True) == 0:
|
|
continue
|
|
break
|
|
logging.info('done lock')
|
|
self.release_lock()
|
|
elif o == 't3':
|
|
pass
|
|
elif o == 't4':
|
|
logging.info('started t4')
|
|
if self.force_idle():
|
|
subprocess.check_call(self.commands['lock'], shell=True)
|
|
subprocess.call(self.commands['timeout2'], shell=True)
|
|
subprocess.check_call(self.commands['timeout1'], shell=True)
|
|
logging.info('done t4')
|
|
self.release_lock()
|
|
elif o == 't5':
|
|
logging.info('started timeout resume')
|
|
if self.force_idle():
|
|
subprocess.check_call(self.commands['lock'], shell=True)
|
|
retry(
|
|
lambda: subprocess.call(self.commands['resume'], shell=True),
|
|
)
|
|
logging.info('done timeout resume')
|
|
elif o == 't6':
|
|
logging.info('started before-sleep')
|
|
if self.force_idle():
|
|
(subprocess.call(self.commands['timeout2'], shell=True),)
|
|
(subprocess.check_call(self.commands['timeout1'], shell=True),)
|
|
logging.info('done before-sleep')
|
|
self.release_lock()
|
|
elif o == 't7':
|
|
logging.info('started after-resume')
|
|
# if self.force_idle():
|
|
# subprocess.check_call(self.commands['lock'], shell=True)
|
|
while True:
|
|
if subprocess.call(self.commands['resume'], shell=True) == 0:
|
|
break
|
|
else:
|
|
time.sleep(0.5)
|
|
logging.info('done after-resume')
|
|
self.release_lock()
|
|
elif o in [
|
|
event_t.idle_state.value,
|
|
event_t.active_state.value,
|
|
]:
|
|
logging.info(json.dumps(dict(o=o)))
|
|
else:
|
|
logging.error(json.dumps(dict(o=o)))
|
|
raise NotImplementedError
|
|
|
|
if not skip_loop:
|
|
pprint.pprint(self.events)
|
|
del self.events[:]
|
|
|
|
if not self.backlight is None:
|
|
self.backlight.check()
|
|
|
|
self.background_check()
|
|
|
|
if options.polkit_service:
|
|
services.extend([subprocess.Popen(['/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1'])])
|
|
|
|
services.extend(
|
|
[
|
|
subprocess.Popen(
|
|
['ibus-daemon'],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
stdin=subprocess.DEVNULL,
|
|
),
|
|
start_swayidle(),
|
|
]
|
|
)
|
|
|
|
if not options.battery is None:
|
|
assert options.battery in [0]
|
|
|
|
logging.info('launching battery')
|
|
services.append(
|
|
Battery(
|
|
should_start=True,
|
|
)
|
|
)
|
|
|
|
while True:
|
|
if shutdown:
|
|
logging.info('shutdown')
|
|
break
|
|
|
|
if all([not o.poll() is None for o in services]):
|
|
logging.info('done')
|
|
break
|
|
|
|
for o in services:
|
|
if hasattr(o, 'check'):
|
|
o.check()
|
|
|
|
time.sleep(0.1)
|
|
except Exception:
|
|
logging.error(traceback.format_exc())
|
|
finally:
|
|
for o in services:
|
|
try:
|
|
o.terminate()
|
|
o.wait(timeout=10)
|
|
except Exception:
|
|
logging.error(traceback.format_exc())
|
|
logging.error('killed %s' % str(o.__dict__))
|
|
o.kill()
|
|
|
|
|
|
def suspend_timer(argv):
|
|
import datetime
|
|
import subprocess
|
|
import time
|
|
|
|
# import sys;
|
|
if len(argv) == 0:
|
|
print('enter HH:MM')
|
|
t3 = input().strip()
|
|
else:
|
|
t3 = argv[0]
|
|
t2 = datetime.datetime.strptime(t3, '%H:%M').time()
|
|
while True:
|
|
t1 = datetime.datetime.now()
|
|
if (t1.hour, t1.minute) >= (t2.hour, t2.minute):
|
|
break
|
|
else:
|
|
t3 = [(t2.hour - t1.hour), t2.minute - t1.minute]
|
|
if t3[1] < 0:
|
|
t3[1] += 60
|
|
t3[0] -= 1
|
|
print(
|
|
'\r%s, %02d:%02d'
|
|
% (
|
|
t1,
|
|
*t3,
|
|
),
|
|
end='',
|
|
)
|
|
time.sleep(1)
|
|
print('suspend computer at %s' % t1.isoformat())
|
|
subprocess.check_call(['systemctl', 'suspend'])
|
|
|
|
|
|
def gnome_shortcuts(argv: list[str]) -> None:
|
|
parser = optparse.OptionParser()
|
|
parser.add_option(
|
|
'-a',
|
|
'--add',
|
|
action='store_true',
|
|
default=None,
|
|
)
|
|
parser.add_option(
|
|
'-l',
|
|
'--list',
|
|
action='store_true',
|
|
default=None,
|
|
)
|
|
|
|
options, args = parser.parse_args(argv)
|
|
|
|
def commands_ids() -> list[str]:
|
|
bindings = (
|
|
subprocess.check_output(
|
|
[
|
|
'gsettings',
|
|
'get',
|
|
'org.gnome.settings-daemon.plugins.media-keys',
|
|
'custom-keybindings',
|
|
]
|
|
)
|
|
.decode('utf-8')
|
|
.strip()
|
|
.replace(
|
|
"'",
|
|
'"',
|
|
)
|
|
)
|
|
if bindings == '@as []':
|
|
t1 = []
|
|
else:
|
|
t1 = json.loads(bindings)
|
|
|
|
return t1
|
|
|
|
def add_command(name, command, binding):
|
|
command_id = len(commands_ids())
|
|
|
|
for cmd in [
|
|
(
|
|
'gsettings',
|
|
'set',
|
|
'org.gnome.settings-daemon.plugins.media-keys',
|
|
'custom-keybindings',
|
|
'[%s]' % ','.join(["'%s'" % ('/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom%d/') % o for o in range(command_id + 1)]),
|
|
),
|
|
(
|
|
'gsettings',
|
|
'set',
|
|
('org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom%d/')
|
|
% command_id,
|
|
'name',
|
|
name,
|
|
),
|
|
(
|
|
'gsettings',
|
|
'set',
|
|
('org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom%d/')
|
|
% command_id,
|
|
'command',
|
|
command,
|
|
),
|
|
(
|
|
'gsettings',
|
|
'set',
|
|
('org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom%d/')
|
|
% command_id,
|
|
'binding',
|
|
binding,
|
|
),
|
|
]:
|
|
subprocess.check_call(cmd)
|
|
|
|
if options.list:
|
|
t1 = commands_ids()
|
|
|
|
t2 = [
|
|
{
|
|
k: json.loads(
|
|
subprocess.check_output(
|
|
[
|
|
'gsettings',
|
|
'get',
|
|
('org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:%s') % o,
|
|
k,
|
|
]
|
|
)
|
|
.decode('utf-8')
|
|
.replace(
|
|
"'",
|
|
'"',
|
|
)
|
|
)
|
|
for k in ['name', 'binding', 'command']
|
|
}
|
|
for o in t1
|
|
]
|
|
|
|
pprint.pprint(t2)
|
|
elif options.add:
|
|
add_command(*args)
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
|
|
def socat_ssh(argv):
|
|
parser = optparse.OptionParser()
|
|
parser.add_option(
|
|
'--local_port',
|
|
dest='local_port',
|
|
default=None,
|
|
type=int,
|
|
)
|
|
parser.add_option(
|
|
'--ssh_key',
|
|
dest='ssh_key',
|
|
default=None,
|
|
type=str,
|
|
)
|
|
parser.add_option(
|
|
'--socat_verbose',
|
|
dest='socat_verbose',
|
|
action='store_true',
|
|
default=False,
|
|
)
|
|
parser.add_option(
|
|
'--ssh_host',
|
|
dest='ssh_host',
|
|
default=None,
|
|
type=str,
|
|
)
|
|
parser.add_option(
|
|
'--target_port',
|
|
dest='target_port',
|
|
default=None,
|
|
type=int,
|
|
)
|
|
parser.add_option(
|
|
'--ssh_command',
|
|
dest='ssh_command',
|
|
default=None,
|
|
type=str,
|
|
)
|
|
parser.add_option(
|
|
'--gateway_command',
|
|
dest='gateway_command',
|
|
default=None,
|
|
type=str,
|
|
help=('a shell command that forwards ssh socket data somewhere else, like busybox nc 127.0.0.1 $(cat remote-ssh.port)'),
|
|
)
|
|
options, args = parser.parse_args(argv)
|
|
|
|
if options.ssh_command is None:
|
|
ssh_command = ['ssh', '-T', '-C']
|
|
else:
|
|
ssh_command = options.ssh_command.split()
|
|
|
|
if not options.ssh_key is None:
|
|
subprocess.check_call(['ssh-add', options.ssh_key])
|
|
ssh_command.extend(
|
|
[
|
|
'-i',
|
|
options.ssh_key,
|
|
]
|
|
)
|
|
|
|
if not options.ssh_host is None:
|
|
ssh_command.extend([options.ssh_host])
|
|
|
|
restart = False
|
|
|
|
def on_interrupt(*args, **kwargs):
|
|
nonlocal restart
|
|
restart = True
|
|
|
|
socat_command = ['socat']
|
|
|
|
if options.socat_verbose:
|
|
socat_command.extend(['-v'])
|
|
|
|
socat_command.extend(
|
|
[
|
|
'tcp-listen:%d,fork,bind=127.0.0.1' % (options.local_port,),
|
|
]
|
|
)
|
|
|
|
signal.signal(
|
|
signal.SIGINT,
|
|
on_interrupt,
|
|
)
|
|
signal.signal(
|
|
signal.SIGTERM,
|
|
on_interrupt,
|
|
)
|
|
|
|
gateway = None
|
|
p = None
|
|
|
|
while True:
|
|
if gateway is None:
|
|
gateway = tempfile.NamedTemporaryFile(suffix='.sh', mode='w')
|
|
gateway.write(
|
|
r"""
|
|
exec %s
|
|
"""
|
|
% ' '.join(ssh_command + [options.gateway_command])
|
|
)
|
|
gateway.flush()
|
|
|
|
if p is None:
|
|
p = subprocess.Popen(
|
|
socat_command
|
|
+ [
|
|
'EXEC:sh %s' % gateway.name,
|
|
]
|
|
)
|
|
|
|
time.sleep(1)
|
|
|
|
if restart:
|
|
try:
|
|
p.terminate()
|
|
p.wait(10)
|
|
except Exception:
|
|
p.kill()
|
|
|
|
restart = False
|
|
|
|
if not p.poll() is None:
|
|
p = None
|
|
|
|
if not gateway is None:
|
|
os.path.unlink(gateway.name)
|
|
if not p is None:
|
|
p.terminate()
|
|
|
|
|
|
def share_wifi(argv):
|
|
parser = optparse.OptionParser()
|
|
parser.add_option(
|
|
'--to-wifi',
|
|
dest='to_wifi',
|
|
default=None,
|
|
type=str,
|
|
)
|
|
parser.add_option(
|
|
'--from-eth',
|
|
dest='from_eth',
|
|
default=None,
|
|
type=str,
|
|
)
|
|
parser.add_option(
|
|
'--channel',
|
|
dest='channel',
|
|
default=None,
|
|
type=int,
|
|
)
|
|
parser.add_option(
|
|
'--ap-name',
|
|
dest='ap_name',
|
|
default=None,
|
|
type=str,
|
|
)
|
|
parser.add_option(
|
|
'--restart-delay',
|
|
dest='restart_delay',
|
|
default=None,
|
|
type=int,
|
|
)
|
|
options, args = parser.parse_args(argv)
|
|
|
|
if options.restart_delay is None:
|
|
options.restart_delay = 2
|
|
|
|
assert not options.to_wifi is None
|
|
assert not options.from_eth is None
|
|
assert not options.ap_name is None
|
|
assert options.restart_delay >= 1
|
|
|
|
print('enter password:')
|
|
|
|
pw = subprocess.check_output('read -s PW; echo -n $PW', shell=True).decode('utf-8')
|
|
if len(pw) == 0:
|
|
pw = subprocess.check_output('pwgen -syn 20 1', shell=True).decode('utf-8').strip()
|
|
with subprocess.Popen(['qrencode', '-t', 'UTF8'], stdin=subprocess.PIPE) as p:
|
|
p.stdin.write(pw.encode('utf-8'))
|
|
p.stdin.flush()
|
|
p.stdin.close()
|
|
try:
|
|
p.wait(5)
|
|
except Exception as exception:
|
|
p.kill()
|
|
raise exception
|
|
|
|
last_timestamp = datetime.datetime.now()
|
|
hostapd = None
|
|
restart = False
|
|
shutdown = False
|
|
|
|
def on_interrupt(sig, *args, **kwargs):
|
|
nonlocal restart
|
|
nonlocal shutdown
|
|
|
|
if sig == signal.SIGINT:
|
|
restart = True
|
|
elif sig == signal.SIGTERM:
|
|
shutdown = True
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
signal.signal(
|
|
signal.SIGINT,
|
|
on_interrupt,
|
|
)
|
|
signal.signal(
|
|
signal.SIGTERM,
|
|
on_interrupt,
|
|
)
|
|
|
|
hostapd_args = [
|
|
'create_ap',
|
|
'--hostapd-timestamps',
|
|
options.to_wifi,
|
|
options.from_eth,
|
|
options.ap_name,
|
|
pw,
|
|
]
|
|
if not options.channel is None:
|
|
hostapd_args.extend(['-c', '%d' % options.channel])
|
|
|
|
while True:
|
|
try:
|
|
if hostapd is None:
|
|
print('\n%s, start new' % last_timestamp)
|
|
hostapd = subprocess.Popen(hostapd_args)
|
|
else:
|
|
if restart or shutdown:
|
|
print('\n%s, shutdown current' % last_timestamp)
|
|
os.kill(hostapd.pid, signal.SIGINT)
|
|
try:
|
|
hostapd.wait(20)
|
|
except Exception:
|
|
hostapd.terminate()
|
|
restart = False
|
|
|
|
if not hostapd.poll() is None:
|
|
hostapd = None
|
|
|
|
if shutdown:
|
|
break
|
|
|
|
if (datetime.datetime.now() - last_timestamp).total_seconds() > options.restart_delay:
|
|
restart = True
|
|
|
|
last_timestamp = datetime.datetime.now()
|
|
except Exception:
|
|
print(traceback.format_exc())
|
|
restart = True
|
|
finally:
|
|
time.sleep(1)
|
|
|
|
|
|
def status(argv):
|
|
import inspect
|
|
import textwrap
|
|
|
|
assert isinstance(argv, list) and all([isinstance(o, str) for o in argv])
|
|
|
|
class c1(optparse.IndentedHelpFormatter):
|
|
def format_option(self, *args, **kwargs):
|
|
f1 = lambda text, width: '\n'.join([textwrap.fill('\t' + o, width, replace_whitespace=False) for o in text.splitlines()]).splitlines()
|
|
t1 = inspect.getsource(optparse.IndentedHelpFormatter.format_option)
|
|
t2 = (
|
|
'\n'.join([o[4:] for o in t1.splitlines()[:]])
|
|
.replace(
|
|
'textwrap.wrap',
|
|
'f1',
|
|
)
|
|
.replace('format_option', 'f2')
|
|
)
|
|
exec(t2, dict(f1=f1), locals())
|
|
return locals()['f2'](self, *args, **kwargs)
|
|
|
|
parser = optparse.OptionParser(
|
|
formatter=c1(
|
|
width=None,
|
|
),
|
|
)
|
|
parser.add_option(
|
|
'--sh',
|
|
dest='sh',
|
|
default=[],
|
|
action='append',
|
|
type=str,
|
|
)
|
|
parser.add_option(
|
|
'--timeout',
|
|
dest='timeout',
|
|
default=None,
|
|
type=float,
|
|
)
|
|
parser.add_option(
|
|
'--config',
|
|
dest='config',
|
|
default=None,
|
|
type=str,
|
|
help=''.join(
|
|
[
|
|
'.json file with array of strings, each is a shell command ',
|
|
'that outputs a separate status text value, ',
|
|
'like\n',
|
|
r"""
|
|
ping -w 1 -i 0.02 <hostname> -c 3 | tail -n 2| head -n 1 | grep -Po $'time\\s+.*$'
|
|
sensors -j | jq -r '.\"coretemp-isa-0000\".\"Package id 0\".temp1_input|tostring + \" C\"'
|
|
printf '%d RPM' $(cat /sys/devices/platform/applesmc.768/fan1_input)
|
|
printf '% 3.0f%%' $(upower -d | grep -Po 'percentage:\\s+\\d+(\\.\\d+)?%' | grep -Po '\\d+(\\.\\d+)?' | head -n 1)
|
|
""".strip(),
|
|
]
|
|
),
|
|
)
|
|
options, args = parser.parse_args(argv)
|
|
|
|
if options.timeout is None:
|
|
options.timeout = 0.5
|
|
|
|
timeout2 = max(options.timeout, 0.0)
|
|
|
|
assert timeout2 >= 0.0 and timeout2 <= 4
|
|
|
|
config = dict()
|
|
try:
|
|
if not options.config is None:
|
|
with io.open(options.config, 'r') as f:
|
|
config.update(json.load(f))
|
|
except Exception:
|
|
logging.error(traceback.format_exc())
|
|
pass
|
|
|
|
options.sh.extend(config.get('sh', []))
|
|
|
|
t1 = []
|
|
|
|
for sh_index, o in enumerate(
|
|
[
|
|
*options.sh,
|
|
*[
|
|
r"""
|
|
A=$(free -h | grep -P Mem: | grep -Po '[\w\.\d]+');
|
|
echo -n $A | awk '{print $2, $7}';
|
|
""",
|
|
r"""
|
|
date +'%Y-%m-%d %l:%M:%S %p';
|
|
""",
|
|
],
|
|
]
|
|
):
|
|
try:
|
|
t1.append(
|
|
subprocess.check_output(
|
|
o,
|
|
shell=True,
|
|
timeout=timeout2,
|
|
)
|
|
.decode('utf-8')
|
|
.strip()
|
|
)
|
|
except Exception:
|
|
t1.append('fail %d' % sh_index)
|
|
|
|
t3 = ' | '.join(t1).replace('\n\r', '')
|
|
|
|
sys.stdout.write(t3)
|
|
sys.stdout.flush()
|
|
|
|
|
|
def custom_translate(
|
|
current_string,
|
|
check,
|
|
none_char=None,
|
|
):
|
|
if none_char is None:
|
|
none_char = '.'
|
|
|
|
class A:
|
|
def __getitem__(self, k):
|
|
t1 = chr(k)
|
|
|
|
t2 = check(k, t1)
|
|
if isinstance(t2, bool):
|
|
if t2:
|
|
return t1
|
|
else:
|
|
return none_char
|
|
elif isinstance(t2, str):
|
|
return t2
|
|
|
|
return current_string.translate(A())
|
|
|
|
|
|
def media_keys(argv):
|
|
assert isinstance(argv, list) and all([isinstance(o, str) for o in argv])
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
#'-c', '--command',
|
|
'command',
|
|
# dest='command',
|
|
type=str,
|
|
default=None,
|
|
)
|
|
|
|
options, args = parser.parse_known_args(argv)
|
|
|
|
if options.command is None and len(args) > 0:
|
|
assert len(args) == 1
|
|
options.command = args[0]
|
|
|
|
assert options.command in [
|
|
'media-play-pause',
|
|
'media-next',
|
|
'media-forward-seconds',
|
|
'media-backward-seconds',
|
|
'media-prev',
|
|
'media-lower-volume',
|
|
'media-raise-volume',
|
|
'media-toggle-volume',
|
|
]
|
|
|
|
msg = None
|
|
|
|
mode = None
|
|
is_mocp = (
|
|
lambda: subprocess.call(
|
|
[
|
|
'pgrep',
|
|
'-u',
|
|
os.environ['USER'],
|
|
'mocp',
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
== 0
|
|
)
|
|
|
|
def mocp_info() -> str:
|
|
t1 = subprocess.check_output(['mocp', '-i'])
|
|
t3 = t1.decode('utf-8')
|
|
t2: dict[str, str] = dict(
|
|
[
|
|
(lambda o2: (o2[0], o2[1]))(o.split(':'))
|
|
# tuple(o.split(':')[:2])
|
|
for o in t3.splitlines()
|
|
]
|
|
)
|
|
|
|
return t2['Title'].strip()[:128]
|
|
|
|
if is_mocp():
|
|
mode = 'mocp'
|
|
else:
|
|
mode = 'playerctl'
|
|
|
|
if options.command == 'media-play-pause':
|
|
if mode == 'mocp':
|
|
subprocess.check_call(['mocp', '--toggle-pause'])
|
|
msg = mocp_info()
|
|
elif mode == 'playerctl':
|
|
subprocess.check_call(['playerctl', 'play-pause'])
|
|
msg = player_metadata()
|
|
else:
|
|
raise NotImplementedError
|
|
elif options.command == 'media-next':
|
|
if mode == 'mocp':
|
|
subprocess.check_call(['mocp', '--next'])
|
|
msg = mocp_info()
|
|
elif mode == 'playerctl':
|
|
subprocess.check_call(['playerctl', 'next'])
|
|
msg = player_metadata()
|
|
else:
|
|
raise NotImplementedError
|
|
elif options.command == 'media-backward-seconds':
|
|
if mode == 'mocp':
|
|
raise NotImplementedError
|
|
elif mode == 'playerctl':
|
|
pos = float(subprocess.check_output(['playerctl', 'position']).decode('utf-8'))
|
|
subprocess.check_call(['playerctl', 'position', '%f' % (pos - float(args[0]))])
|
|
# msg = player_metadata()
|
|
else:
|
|
raise NotImplementedError
|
|
elif options.command == 'media-forward-seconds':
|
|
if mode == 'mocp':
|
|
raise NotImplementedError
|
|
elif mode == 'playerctl':
|
|
pos = float(subprocess.check_output(['playerctl', 'position']).decode('utf-8'))
|
|
subprocess.check_call(['playerctl', 'position', '%f' % (pos + float(args[0]))])
|
|
# msg = player_metadata()
|
|
else:
|
|
raise NotImplementedError
|
|
elif options.command == 'media-prev':
|
|
if mode == 'mocp':
|
|
subprocess.check_call(['mocp', '--previous'])
|
|
msg = mocp_info()
|
|
elif mode == 'playerctl':
|
|
subprocess.check_call(['playerctl', 'previous'])
|
|
msg = player_metadata()
|
|
else:
|
|
raise NotImplementedError
|
|
elif options.command == 'media-lower-volume':
|
|
subprocess.check_call(['pactl', 'set-sink-volume', '@DEFAULT_SINK@', '-5%'])
|
|
msg = subprocess.check_output(['pactl', 'get-sink-volume', '@DEFAULT_SINK@']).decode('utf-8').strip()
|
|
elif options.command == 'media-toggle-volume':
|
|
subprocess.check_call(
|
|
[
|
|
'pactl',
|
|
'set-sink-mute',
|
|
'@DEFAULT_SINK@',
|
|
'toggle',
|
|
]
|
|
)
|
|
msg = subprocess.check_output(['pactl', 'get-sink-volume', '@DEFAULT_SINK@']).decode('utf-8').strip()
|
|
elif options.command == 'media-raise-volume':
|
|
subprocess.check_call(['pactl', 'set-sink-volume', '@DEFAULT_SINK@', '+5%'])
|
|
msg = subprocess.check_output(['pactl', 'get-sink-volume', '@DEFAULT_SINK@']).decode('utf-8').strip()
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
logging.info(
|
|
json.dumps(
|
|
dict(
|
|
command=options.command,
|
|
msg=msg,
|
|
mode=mode,
|
|
),
|
|
ensure_ascii=False,
|
|
)
|
|
)
|
|
|
|
return dict(
|
|
msg=msg,
|
|
)
|
|
|
|
|
|
def install(argv: list[str]) -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
'-r',
|
|
dest='recursive',
|
|
)
|
|
parser.add_argument(
|
|
'-s',
|
|
'--source',
|
|
help='source file/dir to install (copy with permissions preserving)',
|
|
dest='source',
|
|
type=pathlib.Path,
|
|
required=True,
|
|
)
|
|
parser.add_argument(
|
|
'-p',
|
|
'--relative',
|
|
type=pathlib.Path,
|
|
help='copy source relative to path, allows to strip extra components',
|
|
dest='relative',
|
|
default=None,
|
|
)
|
|
parser.add_argument(
|
|
'-t',
|
|
'--target',
|
|
type=pathlib.Path,
|
|
help='target file/dir to install (copy with permissions preserving)',
|
|
dest='target',
|
|
required=True,
|
|
)
|
|
parser.add_argument(
|
|
'-f',
|
|
'--overwrite',
|
|
help='overwrite if target is present',
|
|
dest='overwrite',
|
|
action='store_true',
|
|
default=False,
|
|
)
|
|
|
|
options, args = parser.parse_known_args(argv)
|
|
|
|
if options.relative:
|
|
relative_source = options.source.relative_to(options.relative)
|
|
else:
|
|
if options.source.is_absolute():
|
|
relative_source = options.source.relative_to('/')
|
|
else:
|
|
relative_source = options.source
|
|
|
|
logger.info(dict(source=options.source, target=options.source))
|
|
|
|
final_target = options.target / relative_source
|
|
|
|
logger.info(dict(final_target=final_target, relative_source=relative_source))
|
|
|
|
if final_target.exists():
|
|
if not options.overwrite:
|
|
raise NotImplementedError
|
|
|
|
if final_target.is_dir():
|
|
shutil.rmtree(final_target)
|
|
else:
|
|
os.unlink(final_target)
|
|
|
|
if options.source.is_dir() and not options.recursive:
|
|
raise NotImplementedError
|
|
|
|
if options.source.is_dir():
|
|
os.makedirs(
|
|
final_target,
|
|
exist_ok=True,
|
|
)
|
|
else:
|
|
os.makedirs(
|
|
final_target.parent,
|
|
exist_ok=True,
|
|
)
|
|
|
|
subprocess.check_call(
|
|
[
|
|
'cp',
|
|
'-rp',
|
|
str(options.source),
|
|
'-T',
|
|
str(final_target),
|
|
]
|
|
)
|
|
|
|
# shutil.copy(
|
|
# options.source,
|
|
# final_target.parent,
|
|
# )
|
|
|
|
logger.info(dict(msg='done'))
|
|
|
|
|
|
def backup(argv: list[str]) -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
'-r',
|
|
'--recipients',
|
|
# type=list[str],
|
|
action='append',
|
|
default=[],
|
|
)
|
|
parser.add_argument(
|
|
'-o',
|
|
'--output_dir',
|
|
# type=list[str],
|
|
required=True,
|
|
)
|
|
|
|
options, args = parser.parse_known_args(argv)
|
|
|
|
assert len(options.recipients) > 0
|
|
|
|
subprocess.check_call(
|
|
r"""
|
|
(\
|
|
mkdir -p ~/.cache/; \
|
|
P=~/.cache/"secrets-$(date -Iseconds).tar.xz.gpg"; \
|
|
tar -J -cvf - ~/.gnupg ~/.ssh ~/.histfile | \
|
|
gpg -e {GPG_ARGS} > $P; \
|
|
echo $P; \
|
|
sudo -E P=$P sh -c \
|
|
'\
|
|
mkdir -p {OUTPUT_DIR}; \
|
|
cp $P {OUTPUT_DIR};
|
|
';\
|
|
sync; \
|
|
)
|
|
""".replace(
|
|
'{GPG_ARGS}',
|
|
' '.join(['-r %s' % o for o in options.recipients]),
|
|
).replace(
|
|
'{OUTPUT_DIR}',
|
|
options.output_dir,
|
|
),
|
|
shell=True,
|
|
)
|
|
|
|
|
|
class Command(enum.Enum):
|
|
media = 'media'
|
|
status = 'status'
|
|
http_server = 'http-server'
|
|
pass_ssh_osx = 'pass-ssh-osx'
|
|
wl_screenshot = 'wl-screenshot'
|
|
chrome = 'chrome'
|
|
eternal_oom = 'eternal-oom'
|
|
resilient_vlc = 'resilient-vlc'
|
|
eternal_firefox = 'eternal-firefox'
|
|
install = 'install'
|
|
resilient_ethernet = 'resilient-ethernet'
|
|
player = 'player'
|
|
share_wifi = 'share-wifi'
|
|
socat_ssh = 'socat-ssh'
|
|
gnome_shortcuts = 'gnome-shortcuts'
|
|
sway_sock = 'sway_sock'
|
|
loginctl = 'loginctl'
|
|
suspend_timer = 'suspend-timer'
|
|
desktop_services = 'desktop-services'
|
|
pm_service = 'pm-service'
|
|
scrap_yt_music = 'scrap-yt-music'
|
|
vpn = 'vpn'
|
|
backup = 'backup'
|
|
pip_resolve = 'pip_resolve'
|
|
pip_check_conflicts = 'pip_check_conflicts'
|
|
|
|
|
|
def pip_check_conflicts(
|
|
args: list[str],
|
|
) -> None:
|
|
from .commands_typed.pip import pip_check_conflicts
|
|
from .commands_typed.argparse import parse_args as pr34_parse_args
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
'-p',
|
|
dest='venv_path',
|
|
type=pathlib.Path,
|
|
help='venv path',
|
|
default=None,
|
|
)
|
|
|
|
options, argv = pr34_parse_args(parser, args)
|
|
|
|
res = pip_check_conflicts(options.venv_path)
|
|
logger.info(dict(res=res))
|
|
|
|
assert res.status == 'ok'
|
|
|
|
|
|
def pip_resolve(
|
|
args: list[str],
|
|
) -> None:
|
|
from online.fxreader.pr34.commands_typed.pip import pip_resolve, pip_resolve_t
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
'-m',
|
|
'--mode',
|
|
choices=[o.value for o in pip_resolve_t.kwargs_t.mode_t],
|
|
required=True,
|
|
)
|
|
parser.add_argument(
|
|
'-r',
|
|
'--requirement',
|
|
default=[],
|
|
dest='requirements',
|
|
type=str,
|
|
action='append',
|
|
help=r"""
|
|
requirement,
|
|
can be multiple in a single parameter,
|
|
all of them are to be split by whitespace
|
|
and printed into a temp file,
|
|
that is fed into uv pip compile
|
|
""",
|
|
)
|
|
|
|
options, argv = parser.parse_known_args(args)
|
|
|
|
requirements: Optional[list[str]] = []
|
|
|
|
for o in options.requirements:
|
|
requirements.extend(o.split())
|
|
|
|
if len(requirements) == 0:
|
|
requirements = None
|
|
|
|
options.mode = pip_resolve_t.kwargs_t.mode_t(options.mode)
|
|
|
|
resolve_res = pip_resolve(
|
|
argv,
|
|
mode=options.mode,
|
|
requirements=requirements,
|
|
)
|
|
|
|
assert not resolve_res.txt is None
|
|
|
|
sys.stdout.write(resolve_res.txt)
|
|
sys.stdout.flush()
|
|
|
|
|
|
def commands_cli(argv: Optional[list[str]] = None) -> int:
|
|
if argv is None:
|
|
argv = sys.argv[1:]
|
|
|
|
from online.fxreader.pr34.commands_typed.logging import setup as logging_setup
|
|
|
|
logging_setup()
|
|
# logging.getLogger().setLevel(logging.INFO)
|
|
# logger.setLevel(logging.INFO)
|
|
# handler = logging.StreamHandler(sys.stderr)
|
|
# logging.getLogger().addHandler(handler)
|
|
|
|
msg: Optional[str] = None
|
|
|
|
try:
|
|
if len(argv) > 0 and argv[0].startswith('media'):
|
|
msg = media_keys(argv).get('msg')
|
|
else:
|
|
parser = argparse.ArgumentParser(
|
|
#'online_fxreader.commands'
|
|
)
|
|
parser.add_argument(
|
|
'_command',
|
|
choices=[o.value for o in Command],
|
|
)
|
|
|
|
options, args = parser.parse_known_args()
|
|
options.command = Command(options._command)
|
|
|
|
if options.command is Command.status:
|
|
status(args)
|
|
elif options.command is Command.http_server:
|
|
http_server(args)
|
|
elif options.command is Command.pass_ssh_osx:
|
|
pass_ssh_osx(args)
|
|
elif options.command is Command.wl_screenshot:
|
|
subprocess.check_call(
|
|
r"""
|
|
grim -g "$(slurp)" - | wl-copy
|
|
""",
|
|
shell=True,
|
|
)
|
|
elif options.command is Command.chrome:
|
|
chrome(args)
|
|
elif options.command is Command.eternal_oom:
|
|
eternal_oom(args)
|
|
elif options.command is Command.resilient_vlc:
|
|
resilient_vlc(args)
|
|
elif options.command is Command.eternal_firefox:
|
|
eternal_firefox(
|
|
profile=sys.argv[2],
|
|
group_name=sys.argv[3],
|
|
window_position=json.loads(sys.argv[4]),
|
|
debug=json.loads(sys.argv[5]),
|
|
tabs=sys.argv[6:],
|
|
)
|
|
elif options.command is Command.install:
|
|
install(args)
|
|
elif options.command is Command.resilient_ethernet:
|
|
resilient_ethernet(
|
|
ip_addr=sys.argv[2],
|
|
ethernet_device=sys.argv[3],
|
|
)
|
|
elif options.command is Command.player:
|
|
player_v1(
|
|
folder_url=sys.argv[2],
|
|
item_id=int(sys.argv[3]),
|
|
)
|
|
elif options.command is Command.share_wifi:
|
|
share_wifi(args)
|
|
elif options.command is Command.socat_ssh:
|
|
socat_ssh(args)
|
|
elif options.command is Command.gnome_shortcuts:
|
|
gnome_shortcuts(args)
|
|
elif options.command is Command.sway_sock:
|
|
print(sway_sock())
|
|
elif options.command is Command.loginctl:
|
|
loginctl(args)
|
|
elif options.command is Command.suspend_timer:
|
|
suspend_timer(args)
|
|
elif options.command is Command.desktop_services:
|
|
desktop_services(args)
|
|
elif options.command is Command.pip_resolve:
|
|
pip_resolve(args)
|
|
elif options.command is Command.pip_check_conflicts:
|
|
pip_check_conflicts(args)
|
|
elif options.command is Command.pm_service:
|
|
pm_service(args)
|
|
elif options.command is Command.backup:
|
|
backup(args)
|
|
elif options.command is Command.scrap_yt_music:
|
|
scrap_yt_music(args)
|
|
elif options.command is Command.vpn:
|
|
vpn(args)
|
|
else:
|
|
raise NotImplementedError
|
|
except SystemExit:
|
|
raise
|
|
except Exception:
|
|
msg = 'not implemented\n%s' % traceback.format_exc()
|
|
logging.error(msg)
|
|
finally:
|
|
if not msg is None:
|
|
custom_notify(msg=msg)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(commands_cli())
|