Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
8932242a86
73 changed files with 8882 additions and 1433 deletions
48
.gitignore
vendored
48
.gitignore
vendored
|
|
@ -1,41 +1,7 @@
|
||||||
**/__pycache__/*
|
**/__pycache__
|
||||||
*.bak
|
**/*.7z
|
||||||
*.exe
|
**/*.bak
|
||||||
*.swp
|
**/*.exe
|
||||||
.bin/7-Zip/
|
**/*.swp
|
||||||
.bin/AIDA64/
|
setup/BUILD*
|
||||||
.bin/BleachBit/
|
setup/OUT*
|
||||||
.bin/ClassicStartSkin/
|
|
||||||
.bin/ConEmu/
|
|
||||||
.bin/Erunt/
|
|
||||||
.bin/Everything/
|
|
||||||
.bin/FastCopy/
|
|
||||||
.bin/HWiNFO/HWiNFO*.ini
|
|
||||||
.bin/NotepadPlusPlus/
|
|
||||||
.bin/ProcessKiller/
|
|
||||||
.bin/ProduKey/
|
|
||||||
.bin/Python/
|
|
||||||
.bin/Tmp/
|
|
||||||
.bin/XMPlay/
|
|
||||||
.bin/_Drivers/SDIO/
|
|
||||||
.cbin/*.7z
|
|
||||||
.cbin/AIDA64/
|
|
||||||
.cbin/Autoruns/
|
|
||||||
.cbin/BleachBit-Portable/
|
|
||||||
.cbin/BlueScreenView/
|
|
||||||
.cbin/Caffeine/
|
|
||||||
.cbin/Du/
|
|
||||||
.cbin/Everything/
|
|
||||||
.cbin/FirefoxExtensions/
|
|
||||||
.cbin/IObitUninstallerPortable/
|
|
||||||
.cbin/ProduKey/
|
|
||||||
.cbin/TestDisk/
|
|
||||||
.cbin/TreeSizeFree-Portable/
|
|
||||||
.cbin/XMPlay/
|
|
||||||
.cbin/XYplorerFree/
|
|
||||||
.cbin/_Drivers/
|
|
||||||
.cbin/_Office/
|
|
||||||
.cbin/_vcredists/
|
|
||||||
.cbin/wimlib/
|
|
||||||
BUILD*/
|
|
||||||
OUT*/
|
|
||||||
|
|
|
||||||
6
scripts/README.md
Normal file
6
scripts/README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
## pylint ##
|
||||||
|
|
||||||
|
These scripts use two spaces per indent instead of the default four. As such you will need to update your pylintrc file or run like this:
|
||||||
|
|
||||||
|
`pylint --indent-after-paren=2 --indent-string=' ' wk`
|
||||||
|
|
||||||
31
scripts/activate.py
Normal file
31
scripts/activate.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
"""Wizard Kit: Activate Windows using a BIOS key"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import wk
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Attempt to activate Windows and show result."""
|
||||||
|
title = f'{wk.cfg.main.KIT_NAME_FULL}: Activation Tool'
|
||||||
|
try_print = wk.std.TryAndPrint()
|
||||||
|
wk.std.clear_screen()
|
||||||
|
wk.std.set_title(title)
|
||||||
|
wk.std.print_info(title)
|
||||||
|
print('')
|
||||||
|
|
||||||
|
# Attempt activation
|
||||||
|
try_print.run('Attempting activation...', wk.os.win.activate_with_bios)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
print('')
|
||||||
|
print('Done.')
|
||||||
|
wk.std.pause('Press Enter to exit...')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except: #pylint: disable=bare-except
|
||||||
|
wk.std.major_exception()
|
||||||
14
scripts/build-ufd
Executable file
14
scripts/build-ufd
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Wizard Kit: Build UFD Tool"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import wk
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
wk.kit.ufd.build_ufd()
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except: #pylint: disable=bare-except
|
||||||
|
wk.std.major_exception()
|
||||||
49
scripts/check_disk.py
Normal file
49
scripts/check_disk.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
"""Wizard Kit: Check or repair the %SYSTEMDRIVE% filesystem via CHKDSK"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import os
|
||||||
|
import wk
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run or schedule CHKDSK and show result."""
|
||||||
|
title = f'{wk.cfg.main.KIT_NAME_FULL}: Check Disk Tool'
|
||||||
|
menu = wk.std.Menu(title=title)
|
||||||
|
try_print = wk.std.TryAndPrint()
|
||||||
|
wk.std.clear_screen()
|
||||||
|
wk.std.set_title(title)
|
||||||
|
print('')
|
||||||
|
|
||||||
|
# Add menu entries
|
||||||
|
menu.add_option('Offline scan')
|
||||||
|
menu.add_option('Online scan')
|
||||||
|
|
||||||
|
# Show menu and make selection
|
||||||
|
selection = menu.simple_select()
|
||||||
|
|
||||||
|
# Run or schedule scan
|
||||||
|
if 'Offline' in selection[0]:
|
||||||
|
function = wk.os.win.run_chkdsk_offline
|
||||||
|
msg_good = 'Scheduled'
|
||||||
|
else:
|
||||||
|
function = wk.os.win.run_chkdsk_online
|
||||||
|
msg_good = 'No issues detected'
|
||||||
|
try_print.run(
|
||||||
|
message=f'CHKDSK ({os.environ.get("SYSTEMDRIVE")})...',
|
||||||
|
function=function,
|
||||||
|
msg_good=msg_good,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
print('')
|
||||||
|
print('Done.')
|
||||||
|
wk.std.pause('Press Enter to exit...')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except: #pylint: disable=bare-except
|
||||||
|
wk.std.major_exception()
|
||||||
21
scripts/ddrescue-tui
Executable file
21
scripts/ddrescue-tui
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
## Wizard Kit: ddrescue TUI Launcher
|
||||||
|
|
||||||
|
# Check if running under Linux
|
||||||
|
os_name="$(uname -s)"
|
||||||
|
if [[ "$os_name" == "Darwin" ]]; then
|
||||||
|
os_name="macOS"
|
||||||
|
fi
|
||||||
|
if [[ "$os_name" != "Linux" ]]; then
|
||||||
|
echo "This script is not supported under $os_name." 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
source ./launch-in-tmux
|
||||||
|
|
||||||
|
SESSION_NAME="ddrescue-tui"
|
||||||
|
WINDOW_NAME="ddrescue TUI"
|
||||||
|
TMUX_CMD="./ddrescue-tui.py"
|
||||||
|
|
||||||
|
launch_in_tmux "$@"
|
||||||
14
scripts/ddrescue-tui.py
Executable file
14
scripts/ddrescue-tui.py
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Wizard Kit: ddrescue TUI"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import wk
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
wk.hw.ddrescue.main()
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except: #pylint: disable=bare-except
|
||||||
|
wk.std.major_exception()
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
#
|
#
|
||||||
## Wizard Kit: HW Diagnostics Launcher
|
## Wizard Kit: HW Diagnostics Launcher
|
||||||
|
|
||||||
source launch-in-tmux
|
source ./launch-in-tmux
|
||||||
|
|
||||||
SESSION_NAME="hw-diags"
|
SESSION_NAME="hw-diags"
|
||||||
WINDOW_NAME="Hardware Diagnostics"
|
WINDOW_NAME="Hardware Diagnostics"
|
||||||
TMUX_CMD="hw-diags-menu"
|
TMUX_CMD="./hw-diags.py"
|
||||||
|
|
||||||
launch_in_tmux "$@"
|
launch_in_tmux "$@"
|
||||||
14
scripts/hw-diags.py
Executable file
14
scripts/hw-diags.py
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Wizard Kit: Hardware Diagnostics"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import wk
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
wk.hw.diags.main()
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except: #pylint: disable=bare-except
|
||||||
|
wk.std.major_exception()
|
||||||
49
scripts/hw-drive-info
Executable file
49
scripts/hw-drive-info
Executable file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
|
||||||
|
BLUE='\033[34m'
|
||||||
|
CLEAR='\033[0m'
|
||||||
|
IFS=$'\n'
|
||||||
|
|
||||||
|
# Check if running under Linux
|
||||||
|
os_name="$(uname -s)"
|
||||||
|
if [[ "$os_name" == "Darwin" ]]; then
|
||||||
|
os_name="macOS"
|
||||||
|
fi
|
||||||
|
if [[ "$os_name" != "Linux" ]]; then
|
||||||
|
echo "This script is not supported under $os_name." 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List devices
|
||||||
|
for line in $(lsblk -do NAME,TRAN,SIZE,VENDOR,MODEL,SERIAL); do
|
||||||
|
if [[ "${line:0:4}" == "NAME" ]]; then
|
||||||
|
echo -e "${BLUE}${line}${CLEAR}"
|
||||||
|
else
|
||||||
|
echo "${line}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# List loopback devices
|
||||||
|
if [[ "$(losetup -l | wc -l)" > 0 ]]; then
|
||||||
|
for line in $(losetup -lO NAME,PARTSCAN,RO,BACK-FILE); do
|
||||||
|
if [[ "${line:0:4}" == "NAME" ]]; then
|
||||||
|
echo -e "${BLUE}${line}${CLEAR}"
|
||||||
|
else
|
||||||
|
echo "${line}" | sed -r 's#/dev/(loop[0-9]+)#\1 #'
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List partitions
|
||||||
|
for line in $(lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT); do
|
||||||
|
if [[ "${line:0:4}" == "NAME" ]]; then
|
||||||
|
echo -e "${BLUE}${line}${CLEAR}"
|
||||||
|
else
|
||||||
|
echo "${line}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
|
@ -25,6 +25,16 @@ function print_dmi_value() {
|
||||||
print_in_columns "$name: $value"
|
print_in_columns "$name: $value"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check if running under Linux
|
||||||
|
os_name="$(uname -s)"
|
||||||
|
if [[ "$os_name" == "Darwin" ]]; then
|
||||||
|
os_name="macOS"
|
||||||
|
fi
|
||||||
|
if [[ "$os_name" != "Linux" ]]; then
|
||||||
|
echo "This script is not supported under $os_name." 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# System
|
# System
|
||||||
echo -e "${BLUE}System Information${CLEAR}"
|
echo -e "${BLUE}System Information${CLEAR}"
|
||||||
print_dmi_value "Vendor" "sys_vendor"
|
print_dmi_value "Vendor" "sys_vendor"
|
||||||
|
|
@ -86,7 +96,7 @@ echo ""
|
||||||
# Audio
|
# Audio
|
||||||
echo -e "${BLUE}Audio${CLEAR}"
|
echo -e "${BLUE}Audio${CLEAR}"
|
||||||
while read -r line; do
|
while read -r line; do
|
||||||
if [[ "$line" =~ .*no.soundcards.found.* ]]; then
|
if [[ "$line" = .*no.soundcards.found.* ]]; then
|
||||||
echo " No soundcards found"
|
echo " No soundcards found"
|
||||||
else
|
else
|
||||||
print_in_columns "$line"
|
print_in_columns "$line"
|
||||||
46
scripts/hw-sensors
Executable file
46
scripts/hw-sensors
Executable file
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Wizard Kit: Hardware Sensors"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import platform
|
||||||
|
|
||||||
|
import wk
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Show sensor data on screen."""
|
||||||
|
sensors = wk.hw.sensors.Sensors()
|
||||||
|
if platform.system() == 'Darwin':
|
||||||
|
wk.std.clear_screen()
|
||||||
|
while True:
|
||||||
|
print('\033[100A', end='')
|
||||||
|
sensors.update_sensor_data()
|
||||||
|
wk.std.print_report(sensors.generate_report('Current', 'Max'))
|
||||||
|
wk.std.sleep(1)
|
||||||
|
elif platform.system() == 'Linux':
|
||||||
|
proc = wk.exe.run_program(cmd=['mktemp'])
|
||||||
|
sensors.start_background_monitor(
|
||||||
|
out_path=proc.stdout.strip(),
|
||||||
|
exit_on_thermal_limit=False,
|
||||||
|
temp_labels=('Current', 'Max'),
|
||||||
|
)
|
||||||
|
watch_cmd = [
|
||||||
|
'watch',
|
||||||
|
'--color',
|
||||||
|
'--exec',
|
||||||
|
'--no-title',
|
||||||
|
'--interval', '1',
|
||||||
|
'cat',
|
||||||
|
proc.stdout.strip(),
|
||||||
|
]
|
||||||
|
wk.exe.run_program(watch_cmd, check=False, pipe=False)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except: #pylint: disable=bare-except
|
||||||
|
wk.std.major_exception()
|
||||||
|
|
@ -13,16 +13,16 @@ function ask() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
die () {
|
function err () {
|
||||||
echo "$0:" "$@" >&2
|
echo "$0:" "$@" >&2
|
||||||
exit 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function launch_in_tmux() {
|
function launch_in_tmux() {
|
||||||
# Check for required vars
|
# Check for required vars
|
||||||
[[ -n "${SESSION_NAME:-}" ]] || die "Required variable missing (SESSION_NAME)"
|
[[ -n "${SESSION_NAME:-}" ]] || return $(err "Required variable missing (SESSION_NAME)")
|
||||||
[[ -n "${WINDOW_NAME:-}" ]] || die "Required variable missing (WINDOW_NAME)"
|
[[ -n "${WINDOW_NAME:-}" ]] || return $(err "Required variable missing (WINDOW_NAME)")
|
||||||
[[ -n "${TMUX_CMD:-}" ]] || die "Required variable missing (TMUX_CMD)"
|
[[ -n "${TMUX_CMD:-}" ]] || return $(err "Required variable missing (TMUX_CMD)")
|
||||||
|
|
||||||
# Check for running session
|
# Check for running session
|
||||||
if tmux list-session | grep -q "$SESSION_NAME"; then
|
if tmux list-session | grep -q "$SESSION_NAME"; then
|
||||||
|
|
@ -33,33 +33,34 @@ function launch_in_tmux() {
|
||||||
# Running inside TMUX, switch to session
|
# Running inside TMUX, switch to session
|
||||||
tmux switch-client -t "$SESSION_NAME"
|
tmux switch-client -t "$SESSION_NAME"
|
||||||
if ! jobs %% >/dev/null 2>&1; then
|
if ! jobs %% >/dev/null 2>&1; then
|
||||||
|
# No running jobs, try exiting abandoned tmux session
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Running outside TMUX, attach to session
|
# Running outside TMUX, attach to session
|
||||||
tmux attach-session -t "$SESSION_NAME"
|
tmux attach-session -t "$SESSION_NAME"
|
||||||
fi
|
fi
|
||||||
exit 0
|
return 0
|
||||||
elif ask "Kill current session and start new session?"; then
|
elif ask "Kill current session and start new session?"; then
|
||||||
tmux kill-session -t "$SESSION_NAME" || \
|
tmux kill-session -t "$SESSION_NAME" || \
|
||||||
die "Failed to kill session: $SESSION_NAME"
|
die "Failed to kill session: $SESSION_NAME"
|
||||||
else
|
else
|
||||||
echo "Aborted."
|
echo "Aborted."
|
||||||
echo ""
|
return 1
|
||||||
echo -n "Press Enter to exit... "
|
|
||||||
read -r
|
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Start/Rename session
|
# Start session
|
||||||
if [[ -n "${TMUX:-}" ]]; then
|
if [[ -n "${TMUX:-}" ]]; then
|
||||||
# Running inside TMUX, rename session/window and open the menu
|
# Running inside TMUX, save current session/window names
|
||||||
|
ORIGINAL_SESSION_NAME="$(tmux display-message -p '#S')"
|
||||||
|
ORIGINAL_WINDOW_NAME="$(tmux display-message -p '#W')"
|
||||||
tmux rename-session "$SESSION_NAME"
|
tmux rename-session "$SESSION_NAME"
|
||||||
tmux rename-window "$WINDOW_NAME"
|
tmux rename-window "$WINDOW_NAME"
|
||||||
"$TMUX_CMD" "$@"
|
"$TMUX_CMD" "$@"
|
||||||
tmux rename-session "${SESSION_NAME}_DONE"
|
# Restore previous session/window names
|
||||||
tmux rename-window "${WINDOW_NAME}_DONE"
|
tmux rename-session "${ORIGINAL_SESSION_NAME}"
|
||||||
|
tmux rename-window "${ORIGINAL_WINDOW_NAME}"
|
||||||
else
|
else
|
||||||
# Running outside TMUX, start/attach to session
|
# Running outside TMUX, start/attach to session
|
||||||
tmux new-session -s "$SESSION_NAME" -n "$WINDOW_NAME" "$TMUX_CMD" "$@"
|
tmux new-session -s "$SESSION_NAME" -n "$WINDOW_NAME" "$TMUX_CMD" "$@"
|
||||||
|
|
|
||||||
33
scripts/mount-all-volumes
Executable file
33
scripts/mount-all-volumes
Executable file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Wizard Kit: Mount all volumes"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import wk
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def main():
|
||||||
|
"""Mount all volumes and show results."""
|
||||||
|
wk.std.print_standard(f'{wk.cfg.main.KIT_NAME_FULL}: Volume mount tool')
|
||||||
|
wk.std.print_standard(' ')
|
||||||
|
|
||||||
|
# Mount volumes and get report
|
||||||
|
wk.std.print_standard('Mounting volumes...')
|
||||||
|
report = wk.os.linux.mount_volumes()
|
||||||
|
|
||||||
|
# Show results
|
||||||
|
wk.std.print_info('Results')
|
||||||
|
wk.std.print_report(report, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if wk.std.PLATFORM != 'Linux':
|
||||||
|
os_name = wk.std.PLATFORM.replace('Darwin', 'macOS')
|
||||||
|
wk.std.print_error(f'This script is not supported under {os_name}.')
|
||||||
|
wk.std.abort()
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except: #pylint: disable=bare-except
|
||||||
|
wk.std.major_exception()
|
||||||
30
scripts/mount-backup-shares
Executable file
30
scripts/mount-backup-shares
Executable file
|
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Wizard Kit: Mount Backup Shares"""
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import wk
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def main():
|
||||||
|
"""Attempt to mount backup shares and print report."""
|
||||||
|
wk.std.print_info('Mounting Backup Shares')
|
||||||
|
report = wk.net.mount_backup_shares()
|
||||||
|
for line in report:
|
||||||
|
color = 'GREEN'
|
||||||
|
line = f' {line}'
|
||||||
|
if 'Failed' in line:
|
||||||
|
color = 'RED'
|
||||||
|
elif 'Already' in line:
|
||||||
|
color = 'YELLOW'
|
||||||
|
print(wk.std.color_string(line, color))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except: #pylint: disable=bare-except
|
||||||
|
wk.std.major_exception()
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
# Wizard Kit: Activate Windows using various methods
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Init
|
|
||||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
from functions.activation import *
|
|
||||||
init_global_vars()
|
|
||||||
os.system('title {}: Windows Activation Tool'.format(KIT_NAME_FULL))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
stay_awake()
|
|
||||||
clear_screen()
|
|
||||||
print_info('{}: Windows Activation Tool\n'.format(KIT_NAME_FULL))
|
|
||||||
# Bail early if already activated
|
|
||||||
if windows_is_activated():
|
|
||||||
print_info('This system is already activated')
|
|
||||||
sleep(5)
|
|
||||||
exit_script()
|
|
||||||
other_results = {
|
|
||||||
'Error': {
|
|
||||||
'BIOSKeyNotFoundError': 'BIOS key not found.',
|
|
||||||
}}
|
|
||||||
|
|
||||||
# Determine activation method
|
|
||||||
activation_methods = [
|
|
||||||
{'Name': 'Activate with BIOS key', 'Function': activate_with_bios},
|
|
||||||
]
|
|
||||||
if global_vars['OS']['Version'] not in ('8', '8.1', '10'):
|
|
||||||
activation_methods[0]['Disabled'] = True
|
|
||||||
actions = [
|
|
||||||
{'Name': 'Quit', 'Letter': 'Q'},
|
|
||||||
]
|
|
||||||
|
|
||||||
while True:
|
|
||||||
selection = menu_select(
|
|
||||||
'{}: Windows Activation Menu'.format(KIT_NAME_FULL),
|
|
||||||
main_entries=activation_methods, action_entries=actions)
|
|
||||||
|
|
||||||
if (selection.isnumeric()):
|
|
||||||
result = try_and_print(
|
|
||||||
message = activation_methods[int(selection)-1]['Name'],
|
|
||||||
function = activation_methods[int(selection)-1]['Function'],
|
|
||||||
other_results=other_results)
|
|
||||||
if result['CS']:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
sleep(2)
|
|
||||||
elif selection == 'Q':
|
|
||||||
exit_script()
|
|
||||||
|
|
||||||
# Done
|
|
||||||
print_success('\nDone.')
|
|
||||||
pause("Press Enter to exit...")
|
|
||||||
exit_script()
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except:
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
#!/bin/env python3
|
|
||||||
#
|
|
||||||
# pylint: disable=no-name-in-module,wildcard-import,wrong-import-position
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
"""Wizard Kit: UFD build tool"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Init
|
|
||||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
from docopt import docopt
|
|
||||||
from functions.common import *
|
|
||||||
from functions.ufd import *
|
|
||||||
from settings.ufd import *
|
|
||||||
init_global_vars(silent=True)
|
|
||||||
|
|
||||||
# Main section
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
# Set log
|
|
||||||
try:
|
|
||||||
global_vars['LogDir'] = '{}/Logs'.format(
|
|
||||||
get_user_home(get_user_name()))
|
|
||||||
set_log_file('Build UFD ({Date-Time}).log'.format(**global_vars))
|
|
||||||
except: # pylint: disable=bare-except
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
# Header
|
|
||||||
print_success(KIT_NAME_FULL)
|
|
||||||
print_standard('UFD Build Tool')
|
|
||||||
print_standard(' ')
|
|
||||||
|
|
||||||
# Check if running as root
|
|
||||||
if not running_as_root():
|
|
||||||
print_error('ERROR: This script is meant to be run as root.')
|
|
||||||
abort(False)
|
|
||||||
|
|
||||||
# Docopt
|
|
||||||
try:
|
|
||||||
args = docopt(DOCSTRING)
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
# Catch docopt exits
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except: # pylint: disable=bare-except
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Verify selections
|
|
||||||
ufd_dev = verify_ufd(args['--ufd-device'])
|
|
||||||
sources = verify_sources(args, UFD_SOURCES)
|
|
||||||
show_selections(args, sources, ufd_dev, UFD_SOURCES)
|
|
||||||
if not args['--force']:
|
|
||||||
confirm_selections(args)
|
|
||||||
|
|
||||||
# Prep UFD
|
|
||||||
if not args['--update']:
|
|
||||||
print_info('Prep UFD')
|
|
||||||
prep_device(ufd_dev, UFD_LABEL, use_mbr=args['--use-mbr'])
|
|
||||||
|
|
||||||
# Mount UFD
|
|
||||||
try_and_print(
|
|
||||||
indent=2,
|
|
||||||
message='Mounting UFD...',
|
|
||||||
function=mount,
|
|
||||||
mount_source=find_first_partition(ufd_dev),
|
|
||||||
mount_point='/mnt/UFD',
|
|
||||||
read_write=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Remove Arch folder
|
|
||||||
if args['--update']:
|
|
||||||
try_and_print(
|
|
||||||
indent=2,
|
|
||||||
message='Removing Linux...',
|
|
||||||
function=remove_arch,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Copy sources
|
|
||||||
print_standard(' ')
|
|
||||||
print_info('Copy Sources')
|
|
||||||
for s_label, s_path in sources.items():
|
|
||||||
try_and_print(
|
|
||||||
indent=2,
|
|
||||||
message='Copying {}...'.format(s_label),
|
|
||||||
function=copy_source,
|
|
||||||
source=s_path,
|
|
||||||
items=ITEMS[s_label],
|
|
||||||
overwrite=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update boot entries
|
|
||||||
print_standard(' ')
|
|
||||||
print_info('Boot Setup')
|
|
||||||
try_and_print(
|
|
||||||
indent=2,
|
|
||||||
message='Updating boot entries...',
|
|
||||||
function=update_boot_entries,
|
|
||||||
boot_entries=BOOT_ENTRIES,
|
|
||||||
boot_files=BOOT_FILES,
|
|
||||||
iso_label=ISO_LABEL,
|
|
||||||
ufd_label=UFD_LABEL,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Install syslinux (to partition)
|
|
||||||
try_and_print(
|
|
||||||
indent=2,
|
|
||||||
message='Syslinux (partition)...',
|
|
||||||
function=install_syslinux_to_partition,
|
|
||||||
partition=find_first_partition(ufd_dev),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Unmount UFD
|
|
||||||
try_and_print(
|
|
||||||
indent=2,
|
|
||||||
message='Unmounting UFD...',
|
|
||||||
function=unmount,
|
|
||||||
mount_point='/mnt/UFD',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Install syslinux (to device)
|
|
||||||
try_and_print(
|
|
||||||
indent=2,
|
|
||||||
message='Syslinux (device)...',
|
|
||||||
function=install_syslinux_to_dev,
|
|
||||||
ufd_dev=ufd_dev,
|
|
||||||
use_mbr=args['--use-mbr'],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Hide items
|
|
||||||
print_standard(' ')
|
|
||||||
print_info('Final Touches')
|
|
||||||
try_and_print(
|
|
||||||
indent=2,
|
|
||||||
message='Hiding items...',
|
|
||||||
function=hide_items,
|
|
||||||
ufd_dev=ufd_dev,
|
|
||||||
items=ITEMS_HIDDEN,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Done
|
|
||||||
if not args['--force']:
|
|
||||||
print_standard('\nDone.')
|
|
||||||
pause('Press Enter to exit...')
|
|
||||||
exit_script()
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except: # pylint: disable=bare-except
|
|
||||||
major_exception()
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
# Wizard Kit: Backup CBS Logs and prep CBS temp data for deletion
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Init
|
|
||||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
from functions.cleanup import *
|
|
||||||
from functions.data import *
|
|
||||||
init_global_vars()
|
|
||||||
os.system('title {}: CBS Cleanup'.format(KIT_NAME_FULL))
|
|
||||||
set_log_file('CBS Cleanup.log')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
# Prep
|
|
||||||
stay_awake()
|
|
||||||
clear_screen()
|
|
||||||
folder_path = r'{}\Backups'.format(KIT_NAME_SHORT)
|
|
||||||
dest = select_destination(folder_path=folder_path,
|
|
||||||
prompt='Which disk are we using for temp data and backup?')
|
|
||||||
|
|
||||||
# Show details
|
|
||||||
print_info('{}: CBS Cleanup Tool\n'.format(KIT_NAME_FULL))
|
|
||||||
show_data('Backup / Temp path:', dest)
|
|
||||||
print_standard('\n')
|
|
||||||
if (not ask('Proceed with CBS cleanup?')):
|
|
||||||
abort()
|
|
||||||
|
|
||||||
# Run Cleanup
|
|
||||||
try_and_print(message='Running cleanup...', function=cleanup_cbs,
|
|
||||||
cs='Done', dest_folder=dest)
|
|
||||||
|
|
||||||
# Done
|
|
||||||
print_standard('\nDone.')
|
|
||||||
pause("Press Enter to exit...")
|
|
||||||
exit_script()
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except:
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
## Wizard Kit: ddrescue TUI Launcher
|
|
||||||
|
|
||||||
source launch-in-tmux
|
|
||||||
|
|
||||||
SESSION_NAME="ddrescue-tui"
|
|
||||||
WINDOW_NAME="ddrescue TUI"
|
|
||||||
TMUX_CMD="ddrescue-tui-menu"
|
|
||||||
|
|
||||||
launch_in_tmux "$@"
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
#!/bin/python3
|
|
||||||
#
|
|
||||||
## Wizard Kit: TUI for ddrescue cloning and imaging
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Init
|
|
||||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
from functions.ddrescue import *
|
|
||||||
from functions.hw_diags import *
|
|
||||||
init_global_vars()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
# Prep
|
|
||||||
clear_screen()
|
|
||||||
args = list(sys.argv)
|
|
||||||
run_mode = ''
|
|
||||||
source_path = None
|
|
||||||
dest_path = None
|
|
||||||
|
|
||||||
# Parse args
|
|
||||||
try:
|
|
||||||
script_name = os.path.basename(args.pop(0))
|
|
||||||
run_mode = str(args.pop(0)).lower()
|
|
||||||
source_path = args.pop(0)
|
|
||||||
dest_path = args.pop(0)
|
|
||||||
except IndexError:
|
|
||||||
# We'll set the missing paths later
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Show usage
|
|
||||||
if re.search(r'-+(h|help)', str(sys.argv), re.IGNORECASE):
|
|
||||||
show_usage(script_name)
|
|
||||||
exit_script()
|
|
||||||
|
|
||||||
# Start cloning/imaging
|
|
||||||
if run_mode in ('clone', 'image'):
|
|
||||||
menu_ddrescue(source_path, dest_path, run_mode)
|
|
||||||
else:
|
|
||||||
if not re.search(r'^-*(h|help\?)', run_mode, re.IGNORECASE):
|
|
||||||
print_error('Invalid mode.')
|
|
||||||
|
|
||||||
# Done
|
|
||||||
print_standard('\nDone.')
|
|
||||||
pause("Press Enter to exit...")
|
|
||||||
tmux_switch_client()
|
|
||||||
exit_script()
|
|
||||||
except GenericAbort:
|
|
||||||
abort()
|
|
||||||
except GenericError as ge:
|
|
||||||
msg = 'Generic Error'
|
|
||||||
if str(ge):
|
|
||||||
msg = str(ge)
|
|
||||||
print_error(msg)
|
|
||||||
abort()
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
tmux_switch_client()
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except:
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
#!/bin/python3
|
|
||||||
#
|
|
||||||
## Wizard Kit: HW Diagnostics - Audio
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Init
|
|
||||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
from functions.common import *
|
|
||||||
init_global_vars()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
# Prep
|
|
||||||
clear_screen()
|
|
||||||
print_standard('Hardware Diagnostics: Audio\n')
|
|
||||||
|
|
||||||
# Set volume
|
|
||||||
try:
|
|
||||||
run_program('amixer -q set "Master" 80% unmute'.split())
|
|
||||||
run_program('amixer -q set "PCM" 90% unmute'.split())
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
print_error('Failed to set volume')
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
for mode in ['pink', 'wav']:
|
|
||||||
run_program(
|
|
||||||
cmd = 'speaker-test -c 2 -l 1 -t {}'.format(mode).split(),
|
|
||||||
check = False,
|
|
||||||
pipe = False)
|
|
||||||
|
|
||||||
# Done
|
|
||||||
#print_standard('\nDone.')
|
|
||||||
#pause("Press Enter to exit...")
|
|
||||||
exit_script()
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except:
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
## Wizard Kit: HW Diagnostics - Benchmarks
|
|
||||||
|
|
||||||
function usage {
|
|
||||||
echo "Usage: ${0} device log-file"
|
|
||||||
echo " e.g. ${0} /dev/sda /tmp/tmp.XXXXXXX/benchmarks.log"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Bail early
|
|
||||||
if [ ! -b "${1}" ]; then
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run Benchmarks
|
|
||||||
echo 3 | sudo tee -a /proc/sys/vm/drop_caches >/dev/null 2>&1
|
|
||||||
sudo dd bs=4M if="${1}" of=/dev/null status=progress 2>&1 | tee -a "${2}"
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
#!/bin/python3
|
|
||||||
#
|
|
||||||
## Wizard Kit: HW Diagnostics - Menu
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Init
|
|
||||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
from functions.hw_diags import *
|
|
||||||
from functions.tmux import *
|
|
||||||
init_global_vars()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Show menu
|
|
||||||
try:
|
|
||||||
state = State()
|
|
||||||
menu_diags(state, sys.argv)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print_standard(' ')
|
|
||||||
print_warning('Aborted')
|
|
||||||
print_standard(' ')
|
|
||||||
sleep(1)
|
|
||||||
pause('Press Enter to exit...')
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
tmux_switch_client()
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except:
|
|
||||||
# Cleanup
|
|
||||||
tmux_kill_all_panes()
|
|
||||||
|
|
||||||
if DEBUG_MODE:
|
|
||||||
# Custom major exception
|
|
||||||
print_standard(' ')
|
|
||||||
print_error('Major exception')
|
|
||||||
print_warning(SUPPORT_MESSAGE)
|
|
||||||
print(traceback.format_exc())
|
|
||||||
print_log(traceback.format_exc())
|
|
||||||
|
|
||||||
# Save debug reports and upload data
|
|
||||||
try_and_print(
|
|
||||||
message='Saving debug reports...',
|
|
||||||
function=save_debug_reports,
|
|
||||||
state=state, global_vars=global_vars)
|
|
||||||
question = 'Upload crash details to {}?'.format(CRASH_SERVER['Name'])
|
|
||||||
if ENABLED_UPLOAD_DATA and ask(question):
|
|
||||||
try_and_print(
|
|
||||||
message='Uploading Data...',
|
|
||||||
function=upload_logdir,
|
|
||||||
global_vars=global_vars)
|
|
||||||
|
|
||||||
# Done
|
|
||||||
sleep(1)
|
|
||||||
pause('Press Enter to exit...')
|
|
||||||
exit_script(1)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# "Normal" major exception
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
# Done
|
|
||||||
tmux_kill_all_panes()
|
|
||||||
tmux_switch_client()
|
|
||||||
exit_script()
|
|
||||||
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
#!/bin/python3
|
|
||||||
#
|
|
||||||
## Wizard Kit: HW Diagnostics - Network
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Init
|
|
||||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
from functions.network import *
|
|
||||||
|
|
||||||
|
|
||||||
def check_connection():
|
|
||||||
if not is_connected():
|
|
||||||
# Raise to cause NS in try_and_print()
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
# Prep
|
|
||||||
clear_screen()
|
|
||||||
print_standard('Hardware Diagnostics: Network\n')
|
|
||||||
|
|
||||||
# Connect
|
|
||||||
print_standard('Initializing...')
|
|
||||||
connect_to_network()
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
try_and_print(
|
|
||||||
message='Network connection:', function=check_connection, cs='OK')
|
|
||||||
show_valid_addresses()
|
|
||||||
try_and_print(message='Internet connection:', function=ping,
|
|
||||||
addr='8.8.8.8', cs='OK')
|
|
||||||
try_and_print(message='DNS Resolution:', function=ping, cs='OK')
|
|
||||||
try_and_print(message='Speedtest:', function=speedtest,
|
|
||||||
print_return=True)
|
|
||||||
|
|
||||||
# Done
|
|
||||||
print_standard('\nDone.')
|
|
||||||
#pause("Press Enter to exit...")
|
|
||||||
exit_script()
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except:
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
## Wizard Kit: HW Diagnostics - Prime95
|
|
||||||
|
|
||||||
function usage {
|
|
||||||
echo "Usage: $0 log-dir"
|
|
||||||
echo " e.g. $0 /tmp/tmp.7Mh5f1RhSL9001"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Bail early
|
|
||||||
if [ ! -d "$1" ]; then
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run Prime95
|
|
||||||
cd "$1"
|
|
||||||
mprime -t | grep -iv --line-buffered 'stress.txt' | tee -a "prime.log"
|
|
||||||
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
|
|
||||||
BLUE='\033[34m'
|
|
||||||
CLEAR='\033[0m'
|
|
||||||
IFS=$'\n'
|
|
||||||
|
|
||||||
# List devices
|
|
||||||
for line in $(lsblk -do NAME,TRAN,SIZE,VENDOR,MODEL,SERIAL); do
|
|
||||||
if [[ "${line:0:4}" == "NAME" ]]; then
|
|
||||||
echo -e "${BLUE}${line}${CLEAR}"
|
|
||||||
else
|
|
||||||
echo "${line}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# List loopback devices
|
|
||||||
if [[ "$(losetup -l | wc -l)" > 0 ]]; then
|
|
||||||
for line in $(losetup -lO NAME,PARTSCAN,RO,BACK-FILE); do
|
|
||||||
if [[ "${line:0:4}" == "NAME" ]]; then
|
|
||||||
echo -e "${BLUE}${line}${CLEAR}"
|
|
||||||
else
|
|
||||||
echo "${line}" | sed -r 's#/dev/(loop[0-9]+)#\1 #'
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# List partitions
|
|
||||||
for line in $(lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT); do
|
|
||||||
if [[ "${line:0:4}" == "NAME" ]]; then
|
|
||||||
echo -e "${BLUE}${line}${CLEAR}"
|
|
||||||
else
|
|
||||||
echo "${line}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
## Wizard Kit: Sensor monitoring tool
|
|
||||||
|
|
||||||
WINDOW_NAME="Hardware Sensors"
|
|
||||||
MONITOR="hw-sensors-monitor"
|
|
||||||
|
|
||||||
# Start session
|
|
||||||
tmux new-session -n "$WINDOW_NAME" "$MONITOR"
|
|
||||||
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
#!/bin/python3
|
|
||||||
#
|
|
||||||
## Wizard Kit: Sensor monitoring tool
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Init
|
|
||||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
from functions.sensors import *
|
|
||||||
from functions.tmux import *
|
|
||||||
init_global_vars(silent=True)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
background = False
|
|
||||||
try:
|
|
||||||
if len(sys.argv) > 1 and os.path.exists(sys.argv[1]):
|
|
||||||
background = True
|
|
||||||
monitor_file = sys.argv[1]
|
|
||||||
monitor_pane = None
|
|
||||||
else:
|
|
||||||
result = run_program(['mktemp'])
|
|
||||||
monitor_file = result.stdout.decode().strip()
|
|
||||||
if not background:
|
|
||||||
monitor_pane = tmux_split_window(
|
|
||||||
percent=1, vertical=True, watch=monitor_file)
|
|
||||||
cmd = ['tmux', 'resize-pane', '-Z', '-t', monitor_pane]
|
|
||||||
run_program(cmd, check=False)
|
|
||||||
monitor_sensors(monitor_pane, monitor_file)
|
|
||||||
exit_script()
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except:
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
#!/bin/python3
|
|
||||||
#
|
|
||||||
## Wizard Kit: Volume mount tool
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Init
|
|
||||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
from functions.data import *
|
|
||||||
init_global_vars()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
# Prep
|
|
||||||
clear_screen()
|
|
||||||
print_standard('{}: Volume mount tool'.format(KIT_NAME_FULL))
|
|
||||||
|
|
||||||
# Mount volumes
|
|
||||||
report = mount_volumes(all_devices=True)
|
|
||||||
|
|
||||||
# Print report
|
|
||||||
print_info('\nResults')
|
|
||||||
for vol_name, vol_data in sorted(report.items()):
|
|
||||||
show_data(indent=4, width=20, **vol_data['show_data'])
|
|
||||||
|
|
||||||
# Done
|
|
||||||
print_standard('\nDone.')
|
|
||||||
if 'gui' in sys.argv:
|
|
||||||
pause("Press Enter to exit...")
|
|
||||||
popen_program(['nohup', 'thunar', '/media'], pipe=True)
|
|
||||||
exit_script()
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except:
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
#!/bin/python3
|
|
||||||
#
|
|
||||||
## Wizard Kit: Backup share mount tool
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Init
|
|
||||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
from functions.data import *
|
|
||||||
from functions.network import *
|
|
||||||
init_global_vars()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
# Prep
|
|
||||||
clear_screen()
|
|
||||||
|
|
||||||
# Mount
|
|
||||||
if is_connected():
|
|
||||||
mount_backup_shares(read_write=True)
|
|
||||||
else:
|
|
||||||
# Couldn't connect
|
|
||||||
print_error('ERROR: No network connectivity.')
|
|
||||||
|
|
||||||
# Done
|
|
||||||
print_standard('\nDone.')
|
|
||||||
#pause("Press Enter to exit...")
|
|
||||||
exit_script()
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except:
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
# Wizard Kit: Enter SafeMode by editing the BCD
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Init
|
|
||||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
from functions.safemode import *
|
|
||||||
init_global_vars()
|
|
||||||
os.system('title {}: SafeMode Tool'.format(KIT_NAME_FULL))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
clear_screen()
|
|
||||||
print_info('{}: SafeMode Tool\n'.format(KIT_NAME_FULL))
|
|
||||||
other_results = {
|
|
||||||
'Error': {'CalledProcessError': 'Unknown Error'},
|
|
||||||
'Warning': {}}
|
|
||||||
|
|
||||||
if not ask('Enable booting to SafeMode (with Networking)?'):
|
|
||||||
abort()
|
|
||||||
|
|
||||||
# Configure SafeMode
|
|
||||||
try_and_print(message='Set BCD option...',
|
|
||||||
function=enable_safemode, other_results=other_results)
|
|
||||||
try_and_print(message='Enable MSI in SafeMode...',
|
|
||||||
function=enable_safemode_msi, other_results=other_results)
|
|
||||||
|
|
||||||
# Done
|
|
||||||
print_standard('\nDone.')
|
|
||||||
pause('Press Enter to reboot...')
|
|
||||||
reboot()
|
|
||||||
exit_script()
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except:
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
# Wizard Kit: Exit SafeMode by editing the BCD
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Init
|
|
||||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
from functions.safemode import *
|
|
||||||
init_global_vars()
|
|
||||||
os.system('title {}: SafeMode Tool'.format(KIT_NAME_FULL))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
clear_screen()
|
|
||||||
print_info('{}: SafeMode Tool\n'.format(KIT_NAME_FULL))
|
|
||||||
other_results = {
|
|
||||||
'Error': {'CalledProcessError': 'Unknown Error'},
|
|
||||||
'Warning': {}}
|
|
||||||
|
|
||||||
if not ask('Disable booting to SafeMode?'):
|
|
||||||
abort()
|
|
||||||
|
|
||||||
# Configure SafeMode
|
|
||||||
try_and_print(message='Remove BCD option...',
|
|
||||||
function=disable_safemode, other_results=other_results)
|
|
||||||
try_and_print(message='Disable MSI in SafeMode...',
|
|
||||||
function=disable_safemode_msi, other_results=other_results)
|
|
||||||
|
|
||||||
# Done
|
|
||||||
print_standard('\nDone.')
|
|
||||||
pause('Press Enter to reboot...')
|
|
||||||
reboot()
|
|
||||||
exit_script()
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except:
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
# Wizard Kit: Check, and possibly repair, system file health via SFC
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Init
|
|
||||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
from functions.repairs import *
|
|
||||||
init_global_vars()
|
|
||||||
os.system('title {}: SFC Tool'.format(KIT_NAME_FULL))
|
|
||||||
set_log_file('SFC Tool.log')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
stay_awake()
|
|
||||||
clear_screen()
|
|
||||||
print_info('{}: SFC Tool\n'.format(KIT_NAME_FULL))
|
|
||||||
other_results = {
|
|
||||||
'Error': {
|
|
||||||
'CalledProcessError': 'Unknown Error',
|
|
||||||
},
|
|
||||||
'Warning': {
|
|
||||||
'GenericRepair': 'Repaired',
|
|
||||||
}}
|
|
||||||
if ask('Run a SFC scan now?'):
|
|
||||||
try_and_print(message='SFC scan...',
|
|
||||||
function=run_sfc_scan, other_results=other_results)
|
|
||||||
else:
|
|
||||||
abort()
|
|
||||||
|
|
||||||
# Done
|
|
||||||
print_standard('\nDone.')
|
|
||||||
pause('Press Enter to exit...')
|
|
||||||
exit_script()
|
|
||||||
except SystemExit as sys_exit:
|
|
||||||
exit_script(sys_exit.code)
|
|
||||||
except:
|
|
||||||
major_exception()
|
|
||||||
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
37
scripts/safemode_enter.py
Normal file
37
scripts/safemode_enter.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
"""Wizard Kit: Enter SafeMode by editing the BCD"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import wk
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Prompt user to enter safe mode."""
|
||||||
|
title = f'{wk.cfg.main.KIT_NAME_FULL}: SafeMode Tool'
|
||||||
|
try_print = wk.std.TryAndPrint()
|
||||||
|
wk.std.clear_screen()
|
||||||
|
wk.std.set_title(title)
|
||||||
|
wk.std.print_info(title)
|
||||||
|
print('')
|
||||||
|
|
||||||
|
# Ask
|
||||||
|
if not wk.std.ask('Enable booting to SafeMode (with Networking)?'):
|
||||||
|
wk.std.abort()
|
||||||
|
print('')
|
||||||
|
|
||||||
|
# Configure SafeMode
|
||||||
|
try_print.run('Set BCD option...', wk.os.win.enable_safemode)
|
||||||
|
try_print.run('Enable MSI in SafeMode...', wk.os.win.enable_safemode_msi)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
print('Done.')
|
||||||
|
wk.std.pause('Press Enter to reboot...')
|
||||||
|
wk.exe.run_program('shutdown -r -t 3'.split(), check=False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except: #pylint: disable=bare-except
|
||||||
|
wk.std.major_exception()
|
||||||
37
scripts/safemode_exit.py
Normal file
37
scripts/safemode_exit.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
"""Wizard Kit: Exit SafeMode by editing the BCD"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import wk
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Prompt user to exit safe mode."""
|
||||||
|
title = f'{wk.cfg.main.KIT_NAME_FULL}: SafeMode Tool'
|
||||||
|
try_print = wk.std.TryAndPrint()
|
||||||
|
wk.std.clear_screen()
|
||||||
|
wk.std.set_title(title)
|
||||||
|
wk.std.print_info(title)
|
||||||
|
print('')
|
||||||
|
|
||||||
|
# Ask
|
||||||
|
if not wk.std.ask('Disable booting to SafeMode?'):
|
||||||
|
wk.std.abort()
|
||||||
|
print('')
|
||||||
|
|
||||||
|
# Configure SafeMode
|
||||||
|
try_print.run('Remove BCD option...', wk.os.win.disable_safemode)
|
||||||
|
try_print.run('Disable MSI in SafeMode...', wk.os.win.disable_safemode_msi)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
print('Done.')
|
||||||
|
wk.std.pause('Press Enter to reboot...')
|
||||||
|
wk.exe.run_program('shutdown -r -t 3'.split(), check=False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except: #pylint: disable=bare-except
|
||||||
|
wk.std.major_exception()
|
||||||
35
scripts/sfc_scan.py
Normal file
35
scripts/sfc_scan.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""Wizard Kit: Check, and possibly repair, system file health via SFC"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import wk
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run SFC and report result."""
|
||||||
|
title = f'{wk.cfg.main.KIT_NAME_FULL}: SFC Tool'
|
||||||
|
try_print = wk.std.TryAndPrint()
|
||||||
|
wk.std.clear_screen()
|
||||||
|
wk.std.set_title(title)
|
||||||
|
wk.std.print_info(title)
|
||||||
|
print('')
|
||||||
|
|
||||||
|
# Ask
|
||||||
|
if not wk.std.ask('Run a SFC scan now?'):
|
||||||
|
wk.std.abort()
|
||||||
|
print('')
|
||||||
|
|
||||||
|
# Run
|
||||||
|
try_print.run('SFC scan...', wk.os.win.run_sfc_scan)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
print('Done')
|
||||||
|
wk.std.pause('Press Enter to exit...')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except: #pylint: disable=bare-except
|
||||||
|
wk.std.major_exception()
|
||||||
28
scripts/unmount-backup-shares
Executable file
28
scripts/unmount-backup-shares
Executable file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Wizard Kit: Unmount Backup Shares"""
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import wk
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def main():
|
||||||
|
"""Attempt to mount backup shares and print report."""
|
||||||
|
wk.std.print_info('Unmounting Backup Shares')
|
||||||
|
report = wk.net.unmount_backup_shares()
|
||||||
|
for line in report:
|
||||||
|
color = 'GREEN'
|
||||||
|
line = f' {line}'
|
||||||
|
if 'Not mounted' in line:
|
||||||
|
color = 'YELLOW'
|
||||||
|
print(wk.std.color_string(line, color))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except: #pylint: disable=bare-except
|
||||||
|
wk.std.major_exception()
|
||||||
11
scripts/watch-mac
Executable file
11
scripts/watch-mac
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/zsh
|
||||||
|
#
|
||||||
|
## watch-like utility
|
||||||
|
|
||||||
|
WATCH_FILE="${1}"
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
echo -n "\e[100A"
|
||||||
|
cat "${WATCH_FILE}"
|
||||||
|
sleep 1s
|
||||||
|
done
|
||||||
|
|
@ -1,471 +0,0 @@
|
||||||
"""Wizard Kit: Functions - UFD"""
|
|
||||||
# pylint: disable=broad-except,wildcard-import
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import pathlib
|
|
||||||
from collections import OrderedDict
|
|
||||||
from functions.common import *
|
|
||||||
|
|
||||||
|
|
||||||
def case_insensitive_search(path, item):
|
|
||||||
"""Search path for item case insensitively, returns str."""
|
|
||||||
regex_match = '^{}$'.format(item)
|
|
||||||
real_path = ''
|
|
||||||
|
|
||||||
# Quick check first
|
|
||||||
if os.path.exists('{}/{}'.format(path, item)):
|
|
||||||
real_path = '{}{}{}'.format(
|
|
||||||
path,
|
|
||||||
'' if path == '/' else '/',
|
|
||||||
item,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check all items in dir
|
|
||||||
for entry in os.scandir(path):
|
|
||||||
if re.match(regex_match, entry.name, re.IGNORECASE):
|
|
||||||
real_path = '{}{}{}'.format(
|
|
||||||
path,
|
|
||||||
'' if path == '/' else '/',
|
|
||||||
entry.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Done
|
|
||||||
if not real_path:
|
|
||||||
raise FileNotFoundError('{}/{}'.format(path, item))
|
|
||||||
|
|
||||||
return real_path
|
|
||||||
|
|
||||||
|
|
||||||
def confirm_selections(args):
|
|
||||||
"""Ask tech to confirm selections, twice if necessary."""
|
|
||||||
if not ask('Is the above information correct?'):
|
|
||||||
abort(False)
|
|
||||||
## Safety check
|
|
||||||
if not args['--update']:
|
|
||||||
print_standard(' ')
|
|
||||||
print_warning('SAFETY CHECK')
|
|
||||||
print_standard(
|
|
||||||
'All data will be DELETED from the disk and partition(s) listed above.')
|
|
||||||
print_standard(
|
|
||||||
'This is irreversible and will lead to {RED}DATA LOSS.{CLEAR}'.format(
|
|
||||||
**COLORS))
|
|
||||||
if not ask('Asking again to confirm, is this correct?'):
|
|
||||||
abort(False)
|
|
||||||
|
|
||||||
print_standard(' ')
|
|
||||||
|
|
||||||
|
|
||||||
def copy_source(source, items, overwrite=False):
|
|
||||||
"""Copy source items to /mnt/UFD."""
|
|
||||||
is_image = source.is_file()
|
|
||||||
|
|
||||||
# Mount source if necessary
|
|
||||||
if is_image:
|
|
||||||
mount(source, '/mnt/Source')
|
|
||||||
|
|
||||||
# Copy items
|
|
||||||
for i_source, i_dest in items:
|
|
||||||
i_source = '{}{}'.format(
|
|
||||||
'/mnt/Source' if is_image else source,
|
|
||||||
i_source,
|
|
||||||
)
|
|
||||||
i_dest = '/mnt/UFD{}'.format(i_dest)
|
|
||||||
try:
|
|
||||||
recursive_copy(i_source, i_dest, overwrite=overwrite)
|
|
||||||
except FileNotFoundError:
|
|
||||||
# Going to assume (hope) that this is fine
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Unmount source if necessary
|
|
||||||
if is_image:
|
|
||||||
unmount('/mnt/Source')
|
|
||||||
|
|
||||||
|
|
||||||
def find_first_partition(dev_path):
|
|
||||||
"""Find path to first partition of dev, returns str."""
|
|
||||||
cmd = [
|
|
||||||
'lsblk',
|
|
||||||
'--list',
|
|
||||||
'--noheadings',
|
|
||||||
'--output', 'name',
|
|
||||||
'--paths',
|
|
||||||
dev_path,
|
|
||||||
]
|
|
||||||
result = run_program(cmd, encoding='utf-8', errors='ignore')
|
|
||||||
part_path = result.stdout.splitlines()[-1].strip()
|
|
||||||
|
|
||||||
return part_path
|
|
||||||
|
|
||||||
|
|
||||||
def find_path(path):
|
|
||||||
"""Find path case-insensitively, returns pathlib.Path obj."""
|
|
||||||
path_obj = pathlib.Path(path).resolve()
|
|
||||||
|
|
||||||
# Quick check first
|
|
||||||
if path_obj.exists():
|
|
||||||
return path_obj
|
|
||||||
|
|
||||||
# Fix case
|
|
||||||
parts = path_obj.relative_to('/').parts
|
|
||||||
real_path = '/'
|
|
||||||
for part in parts:
|
|
||||||
try:
|
|
||||||
real_path = case_insensitive_search(real_path, part)
|
|
||||||
except NotADirectoryError:
|
|
||||||
# Reclassify error
|
|
||||||
raise FileNotFoundError(path)
|
|
||||||
|
|
||||||
# Raise error if path doesn't exist
|
|
||||||
path_obj = pathlib.Path(real_path)
|
|
||||||
if not path_obj.exists():
|
|
||||||
raise FileNotFoundError(path_obj)
|
|
||||||
|
|
||||||
# Done
|
|
||||||
return path_obj
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_home(user):
|
|
||||||
"""Get path to user's home dir, returns str."""
|
|
||||||
home_dir = None
|
|
||||||
cmd = ['getent', 'passwd', user]
|
|
||||||
result = run_program(cmd, encoding='utf-8', errors='ignore', check=False)
|
|
||||||
try:
|
|
||||||
home_dir = result.stdout.split(':')[5]
|
|
||||||
except Exception:
|
|
||||||
# Just use HOME from ENV (or '/root' if that fails)
|
|
||||||
home_dir = os.environ.get('HOME', '/root')
|
|
||||||
|
|
||||||
return home_dir
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_name():
|
|
||||||
"""Get real user name, returns str."""
|
|
||||||
user = None
|
|
||||||
if 'SUDO_USER' in os.environ:
|
|
||||||
user = os.environ.get('SUDO_USER', 'Unknown')
|
|
||||||
else:
|
|
||||||
user = os.environ.get('USER', 'Unknown')
|
|
||||||
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
def hide_items(ufd_dev, items):
|
|
||||||
"""Set FAT32 hidden flag for items."""
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
with open('/root/.mtoolsrc', 'w') as f:
|
|
||||||
f.write('drive U: file="{}"\n'.format(
|
|
||||||
find_first_partition(ufd_dev)))
|
|
||||||
f.write('mtools_skip_check=1\n')
|
|
||||||
|
|
||||||
# Hide items
|
|
||||||
for item in items:
|
|
||||||
cmd = ['yes | mattrib +h "U:/{}"'.format(item)]
|
|
||||||
run_program(cmd, check=False, shell=True)
|
|
||||||
|
|
||||||
|
|
||||||
def install_syslinux_to_dev(ufd_dev, use_mbr):
|
|
||||||
"""Install Syslinux to UFD (dev)."""
|
|
||||||
cmd = [
|
|
||||||
'dd',
|
|
||||||
'bs=440',
|
|
||||||
'count=1',
|
|
||||||
'if=/usr/lib/syslinux/bios/{}.bin'.format(
|
|
||||||
'mbr' if use_mbr else 'gptmbr',
|
|
||||||
),
|
|
||||||
'of={}'.format(ufd_dev),
|
|
||||||
]
|
|
||||||
run_program(cmd)
|
|
||||||
|
|
||||||
|
|
||||||
def install_syslinux_to_partition(partition):
|
|
||||||
"""Install Syslinux to UFD (partition)."""
|
|
||||||
cmd = [
|
|
||||||
'syslinux',
|
|
||||||
'--install',
|
|
||||||
'--directory',
|
|
||||||
'/arch/boot/syslinux/',
|
|
||||||
partition,
|
|
||||||
]
|
|
||||||
run_program(cmd)
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_path(path_obj, path_type):
|
|
||||||
"""Verify path_obj is valid by type, returns bool."""
|
|
||||||
valid_path = False
|
|
||||||
if path_type == 'DIR':
|
|
||||||
valid_path = path_obj.is_dir()
|
|
||||||
elif path_type == 'KIT':
|
|
||||||
valid_path = path_obj.is_dir() and path_obj.joinpath('.bin').exists()
|
|
||||||
elif path_type == 'IMG':
|
|
||||||
valid_path = path_obj.is_file() and path_obj.suffix.lower() == '.img'
|
|
||||||
elif path_type == 'ISO':
|
|
||||||
valid_path = path_obj.is_file() and path_obj.suffix.lower() == '.iso'
|
|
||||||
elif path_type == 'UFD':
|
|
||||||
valid_path = path_obj.is_block_device()
|
|
||||||
|
|
||||||
return valid_path
|
|
||||||
|
|
||||||
|
|
||||||
def mount(mount_source, mount_point, read_write=False):
|
|
||||||
"""Mount mount_source on mount_point."""
|
|
||||||
os.makedirs(mount_point, exist_ok=True)
|
|
||||||
cmd = [
|
|
||||||
'mount',
|
|
||||||
mount_source,
|
|
||||||
mount_point,
|
|
||||||
'-o',
|
|
||||||
'rw' if read_write else 'ro',
|
|
||||||
]
|
|
||||||
run_program(cmd)
|
|
||||||
|
|
||||||
|
|
||||||
def prep_device(dev_path, label, use_mbr=False, indent=2):
|
|
||||||
"""Format device in preparation for applying the WizardKit components
|
|
||||||
|
|
||||||
This is done is four steps:
|
|
||||||
1. Zero-out first 64MB (this deletes the partition table and/or bootloader)
|
|
||||||
2. Create a new partition table (GPT by default, optionally MBR)
|
|
||||||
3. Set boot flag
|
|
||||||
4. Format partition (FAT32, 4K aligned)
|
|
||||||
"""
|
|
||||||
# Zero-out first 64MB
|
|
||||||
cmd = 'dd bs=4M count=16 if=/dev/zero of={}'.format(dev_path).split()
|
|
||||||
try_and_print(
|
|
||||||
indent=indent,
|
|
||||||
message='Zeroing first 64MB...',
|
|
||||||
function=run_program,
|
|
||||||
cmd=cmd,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create partition table
|
|
||||||
cmd = 'parted {} --script -- mklabel {} mkpart primary fat32 4MiB {}'.format(
|
|
||||||
dev_path,
|
|
||||||
'msdos' if use_mbr else 'gpt',
|
|
||||||
'-1s' if use_mbr else '-4MiB',
|
|
||||||
).split()
|
|
||||||
try_and_print(
|
|
||||||
indent=indent,
|
|
||||||
message='Creating partition table...',
|
|
||||||
function=run_program,
|
|
||||||
cmd=cmd,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set boot flag
|
|
||||||
cmd = 'parted {} set 1 {} on'.format(
|
|
||||||
dev_path,
|
|
||||||
'boot' if use_mbr else 'legacy_boot',
|
|
||||||
).split()
|
|
||||||
try_and_print(
|
|
||||||
indent=indent,
|
|
||||||
message='Setting boot flag...',
|
|
||||||
function=run_program,
|
|
||||||
cmd=cmd,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Format partition
|
|
||||||
cmd = [
|
|
||||||
'mkfs.vfat', '-F', '32',
|
|
||||||
'-n', label,
|
|
||||||
find_first_partition(dev_path),
|
|
||||||
]
|
|
||||||
try_and_print(
|
|
||||||
indent=indent,
|
|
||||||
message='Formatting partition...',
|
|
||||||
function=run_program,
|
|
||||||
cmd=cmd,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def recursive_copy(source, dest, overwrite=False):
|
|
||||||
"""Copy source to dest recursively.
|
|
||||||
|
|
||||||
NOTE: This uses rsync style source/dest syntax.
|
|
||||||
If the source has a trailing slash then it's contents are copied,
|
|
||||||
otherwise the source itself is copied.
|
|
||||||
|
|
||||||
Examples assuming "ExDir/ExFile.txt" exists:
|
|
||||||
recursive_copy("ExDir", "Dest/") results in "Dest/ExDir/ExFile.txt"
|
|
||||||
recursive_copy("ExDir/", "Dest/") results in "Dest/ExFile.txt"
|
|
||||||
|
|
||||||
NOTE 2: dest does not use find_path because it might not exist.
|
|
||||||
"""
|
|
||||||
copy_contents = source.endswith('/')
|
|
||||||
source = find_path(source)
|
|
||||||
dest = pathlib.Path(dest).resolve().joinpath(source.name)
|
|
||||||
os.makedirs(dest.parent, exist_ok=True)
|
|
||||||
|
|
||||||
if source.is_dir():
|
|
||||||
if copy_contents:
|
|
||||||
# Trailing slash syntax
|
|
||||||
for item in os.scandir(source):
|
|
||||||
recursive_copy(item.path, dest.parent, overwrite=overwrite)
|
|
||||||
elif not dest.exists():
|
|
||||||
# No conflict, copying whole tree (no merging needed)
|
|
||||||
shutil.copytree(source, dest)
|
|
||||||
elif not dest.is_dir():
|
|
||||||
# Refusing to replace file with dir
|
|
||||||
raise FileExistsError('Refusing to replace file: {}'.format(dest))
|
|
||||||
else:
|
|
||||||
# Dest exists and is a dir, merge dirs
|
|
||||||
for item in os.scandir(source):
|
|
||||||
recursive_copy(item.path, dest, overwrite=overwrite)
|
|
||||||
elif source.is_file():
|
|
||||||
if not dest.exists():
|
|
||||||
# No conflict, copying file
|
|
||||||
shutil.copy2(source, dest)
|
|
||||||
elif not dest.is_file():
|
|
||||||
# Refusing to replace dir with file
|
|
||||||
raise FileExistsError('Refusing to replace dir: {}'.format(dest))
|
|
||||||
elif overwrite:
|
|
||||||
# Dest file exists, deleting and replacing file
|
|
||||||
os.remove(dest)
|
|
||||||
shutil.copy2(source, dest)
|
|
||||||
else:
|
|
||||||
# Refusing to delete file when overwrite=False
|
|
||||||
raise FileExistsError('Refusing to delete file: {}'.format(dest))
|
|
||||||
|
|
||||||
|
|
||||||
def remove_arch():
|
|
||||||
"""Remove arch dir from UFD.
|
|
||||||
|
|
||||||
This ensures a clean installation to the UFD and resets the boot files
|
|
||||||
"""
|
|
||||||
shutil.rmtree(find_path('/mnt/UFD/arch'))
|
|
||||||
|
|
||||||
|
|
||||||
def running_as_root():
|
|
||||||
"""Check if running with effective UID of 0, returns bool."""
|
|
||||||
return os.geteuid() == 0
|
|
||||||
|
|
||||||
|
|
||||||
def show_selections(args, sources, ufd_dev, ufd_sources):
|
|
||||||
"""Show selections including non-specified options."""
|
|
||||||
|
|
||||||
# Sources
|
|
||||||
print_info('Sources')
|
|
||||||
for label in ufd_sources.keys():
|
|
||||||
if label in sources:
|
|
||||||
print_standard(' {label:<18} {path}'.format(
|
|
||||||
label=label+':',
|
|
||||||
path=sources[label],
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
print_standard(' {label:<18} {YELLOW}Not Specified{CLEAR}'.format(
|
|
||||||
label=label+':',
|
|
||||||
**COLORS,
|
|
||||||
))
|
|
||||||
print_standard(' ')
|
|
||||||
|
|
||||||
# Destination
|
|
||||||
print_info('Destination')
|
|
||||||
cmd = [
|
|
||||||
'lsblk', '--nodeps', '--noheadings', '--paths',
|
|
||||||
'--output', 'NAME,FSTYPE,TRAN,SIZE,VENDOR,MODEL,SERIAL',
|
|
||||||
ufd_dev,
|
|
||||||
]
|
|
||||||
result = run_program(cmd, check=False, encoding='utf-8', errors='ignore')
|
|
||||||
print_standard(result.stdout.strip())
|
|
||||||
cmd = [
|
|
||||||
'lsblk', '--noheadings', '--paths',
|
|
||||||
'--output', 'NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT',
|
|
||||||
ufd_dev,
|
|
||||||
]
|
|
||||||
result = run_program(cmd, check=False, encoding='utf-8', errors='ignore')
|
|
||||||
for line in result.stdout.splitlines()[1:]:
|
|
||||||
print_standard(line)
|
|
||||||
|
|
||||||
# Notes
|
|
||||||
if args['--update']:
|
|
||||||
print_warning('Updating kit in-place')
|
|
||||||
elif args['--use-mbr']:
|
|
||||||
print_warning('Formatting using legacy MBR')
|
|
||||||
print_standard(' ')
|
|
||||||
|
|
||||||
|
|
||||||
def unmount(mount_point):
|
|
||||||
"""Unmount mount_point."""
|
|
||||||
cmd = ['umount', mount_point]
|
|
||||||
run_program(cmd)
|
|
||||||
|
|
||||||
|
|
||||||
def update_boot_entries(boot_entries, boot_files, iso_label, ufd_label):
|
|
||||||
"""Update boot files for UFD usage"""
|
|
||||||
configs = []
|
|
||||||
|
|
||||||
# Find config files
|
|
||||||
for c_path, c_ext in boot_files.items():
|
|
||||||
c_path = find_path('/mnt/UFD{}'.format(c_path))
|
|
||||||
for item in os.scandir(c_path):
|
|
||||||
if item.name.lower().endswith(c_ext.lower()):
|
|
||||||
configs.append(item.path)
|
|
||||||
|
|
||||||
# Update Linux labels
|
|
||||||
cmd = [
|
|
||||||
'sed',
|
|
||||||
'--in-place',
|
|
||||||
'--regexp-extended',
|
|
||||||
's/(eSysRescueLiveCD|{})/{}/'.format(iso_label, ufd_label),
|
|
||||||
*configs,
|
|
||||||
]
|
|
||||||
run_program(cmd)
|
|
||||||
|
|
||||||
# Uncomment extra entries if present
|
|
||||||
for b_path, b_comment in boot_entries.items():
|
|
||||||
try:
|
|
||||||
find_path('/mnt/UFD{}'.format(b_path))
|
|
||||||
except (FileNotFoundError, NotADirectoryError):
|
|
||||||
# Entry not found, continue to next entry
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Entry found, update config files
|
|
||||||
cmd = [
|
|
||||||
'sed',
|
|
||||||
'--in-place',
|
|
||||||
's/#{}#//'.format(b_comment),
|
|
||||||
*configs,
|
|
||||||
]
|
|
||||||
run_program(cmd, check=False)
|
|
||||||
|
|
||||||
|
|
||||||
def verify_sources(args, ufd_sources):
|
|
||||||
"""Check all sources and abort if necessary, returns dict."""
|
|
||||||
sources = OrderedDict()
|
|
||||||
|
|
||||||
for label, data in ufd_sources.items():
|
|
||||||
s_path = args[data['Arg']]
|
|
||||||
if s_path:
|
|
||||||
try:
|
|
||||||
s_path_obj = find_path(s_path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
print_error('ERROR: {} not found: {}'.format(label, s_path))
|
|
||||||
abort(False)
|
|
||||||
if not is_valid_path(s_path_obj, data['Type']):
|
|
||||||
print_error('ERROR: Invalid {} source: {}'.format(label, s_path))
|
|
||||||
abort(False)
|
|
||||||
sources[label] = s_path_obj
|
|
||||||
|
|
||||||
return sources
|
|
||||||
|
|
||||||
|
|
||||||
def verify_ufd(dev_path):
|
|
||||||
"""Check that dev_path is a valid UFD, returns pathlib.Path obj."""
|
|
||||||
ufd_dev = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
ufd_dev = find_path(dev_path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
print_error('ERROR: UFD device not found: {}'.format(dev_path))
|
|
||||||
abort(False)
|
|
||||||
|
|
||||||
if not is_valid_path(ufd_dev, 'UFD'):
|
|
||||||
print_error('ERROR: Invalid UFD device: {}'.format(ufd_dev))
|
|
||||||
abort(False)
|
|
||||||
|
|
||||||
return ufd_dev
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print("This file is not meant to be called directly.")
|
|
||||||
|
|
@ -12,7 +12,7 @@ SOURCE_URLS = {
|
||||||
'BlueScreenView32': 'http://www.nirsoft.net/utils/bluescreenview.zip',
|
'BlueScreenView32': 'http://www.nirsoft.net/utils/bluescreenview.zip',
|
||||||
'BlueScreenView64': 'http://www.nirsoft.net/utils/bluescreenview-x64.zip',
|
'BlueScreenView64': 'http://www.nirsoft.net/utils/bluescreenview-x64.zip',
|
||||||
'Caffeine': 'http://www.zhornsoftware.co.uk/caffeine/caffeine.zip',
|
'Caffeine': 'http://www.zhornsoftware.co.uk/caffeine/caffeine.zip',
|
||||||
'ClassicStartSkin': 'http://www.classicshell.net/forum/download/file.php?id=3001&sid=9a195960d98fd754867dcb63d9315335',
|
'ClassicStartSkin': 'https://coddec.github.io/Classic-Shell/www.classicshell.net/forum/download/fileb1ba.php?id=3001',
|
||||||
'Du': 'https://download.sysinternals.com/files/DU.zip',
|
'Du': 'https://download.sysinternals.com/files/DU.zip',
|
||||||
'ERUNT': 'http://www.aumha.org/downloads/erunt.zip',
|
'ERUNT': 'http://www.aumha.org/downloads/erunt.zip',
|
||||||
'ESET AVRemover32': 'https://download.eset.com/com/eset/tools/installers/av_remover/latest/avremover_nt32_enu.exe',
|
'ESET AVRemover32': 'https://download.eset.com/com/eset/tools/installers/av_remover/latest/avremover_nt32_enu.exe',
|
||||||
|
|
|
||||||
36
scripts/wk/__init__.py
Normal file
36
scripts/wk/__init__.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
"""WizardKit: wk module init"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
from sys import version_info as version
|
||||||
|
|
||||||
|
from wk import cfg
|
||||||
|
from wk import debug
|
||||||
|
from wk import exe
|
||||||
|
from wk import graph
|
||||||
|
from wk import hw
|
||||||
|
from wk import io
|
||||||
|
from wk import kit
|
||||||
|
from wk import log
|
||||||
|
from wk import net
|
||||||
|
from wk import os
|
||||||
|
from wk import std
|
||||||
|
from wk import sw
|
||||||
|
from wk import tmux
|
||||||
|
|
||||||
|
|
||||||
|
# Check env
|
||||||
|
if version < (3, 7):
|
||||||
|
# Unsupported
|
||||||
|
raise RuntimeError(
|
||||||
|
f'This package is unsupported on Python {version.major}.{version.minor}'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Init
|
||||||
|
try:
|
||||||
|
log.start()
|
||||||
|
except UserWarning as err:
|
||||||
|
std.print_warning(err)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
8
scripts/wk/cfg/__init__.py
Normal file
8
scripts/wk/cfg/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
"""WizardKit: cfg module init"""
|
||||||
|
|
||||||
|
from wk.cfg import ddrescue
|
||||||
|
from wk.cfg import hw
|
||||||
|
from wk.cfg import log
|
||||||
|
from wk.cfg import main
|
||||||
|
from wk.cfg import net
|
||||||
|
from wk.cfg import ufd
|
||||||
67
scripts/wk/cfg/ddrescue.py
Normal file
67
scripts/wk/cfg/ddrescue.py
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
"""WizardKit: Config - ddrescue"""
|
||||||
|
# pylint: disable=bad-whitespace,line-too-long
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
|
# Layout
|
||||||
|
TMUX_SIDE_WIDTH = 21
|
||||||
|
TMUX_LAYOUT = OrderedDict({
|
||||||
|
'Source': {'height': 2, 'Check': True},
|
||||||
|
'Started': {'width': TMUX_SIDE_WIDTH, 'Check': True},
|
||||||
|
'Progress': {'width': TMUX_SIDE_WIDTH, 'Check': True},
|
||||||
|
})
|
||||||
|
|
||||||
|
# ddrescue
|
||||||
|
AUTO_PASS_THRESHOLDS = {
|
||||||
|
# NOTE: The scrape key is set to infinity to force a break
|
||||||
|
'read': 95,
|
||||||
|
'trim': 98,
|
||||||
|
'scrape': float('inf'),
|
||||||
|
}
|
||||||
|
DDRESCUE_SETTINGS = {
|
||||||
|
'Default': {
|
||||||
|
'--binary-prefixes': {'Selected': True, 'Hidden': True, },
|
||||||
|
'--data-preview': {'Selected': True, 'Value': '5', 'Hidden': True, },
|
||||||
|
'--idirect': {'Selected': True, },
|
||||||
|
'--odirect': {'Selected': True, },
|
||||||
|
'--max-error-rate': {'Selected': True, 'Value': '100MiB', },
|
||||||
|
'--max-read-rate': {'Selected': False, 'Value': '1MiB', },
|
||||||
|
'--min-read-rate': {'Selected': True, 'Value': '64KiB', },
|
||||||
|
'--reopen-on-error': {'Selected': True, },
|
||||||
|
'--retry-passes': {'Selected': True, 'Value': '0', },
|
||||||
|
'--reverse': {'Selected': False, },
|
||||||
|
'--test-mode': {'Selected': False, 'Value': 'test.map', },
|
||||||
|
'--timeout': {'Selected': True, 'Value': '30m', },
|
||||||
|
'-vvvv': {'Selected': True, 'Hidden': True, },
|
||||||
|
},
|
||||||
|
'Fast': {
|
||||||
|
'--max-error-rate': {'Selected': True, 'Value': '32MiB', },
|
||||||
|
'--min-read-rate': {'Selected': True, 'Value': '1MiB', },
|
||||||
|
'--reopen-on-error': {'Selected': False, },
|
||||||
|
'--timeout': {'Selected': True, 'Value': '5m', },
|
||||||
|
},
|
||||||
|
'Safe': {
|
||||||
|
'--max-read-rate': {'Selected': True, 'Value': '64MiB', },
|
||||||
|
'--min-read-rate': {'Selected': True, 'Value': '1KiB', },
|
||||||
|
'--reopen-on-error': {'Selected': True, },
|
||||||
|
'--timeout': {'Selected': False, 'Value': '30m', },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
PARTITION_TYPES = {
|
||||||
|
'GPT': {
|
||||||
|
'NTFS': 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7', # Basic Data Partition
|
||||||
|
'VFAT': 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7', # Basic Data Partition
|
||||||
|
'EXFAT': 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7', # Basic Data Partition
|
||||||
|
},
|
||||||
|
'MBR': {
|
||||||
|
'EXFAT': 7, # 0x7
|
||||||
|
'NTFS': 7, # 0x7
|
||||||
|
'VFAT': 11, # 0xb
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
140
scripts/wk/cfg/hw.py
Normal file
140
scripts/wk/cfg/hw.py
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
"""WizardKit: Config - Hardware"""
|
||||||
|
# pylint: disable=bad-whitespace,line-too-long
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
ATTRIBUTE_COLORS = (
|
||||||
|
# NOTE: Ordered by ascending importance
|
||||||
|
('Warning', 'YELLOW'),
|
||||||
|
('Error', 'RED'),
|
||||||
|
('Maximum', 'PURPLE'),
|
||||||
|
)
|
||||||
|
# NOTE: Force 4K read block size for disks >= 3TB
|
||||||
|
BADBLOCKS_LARGE_DISK = 3 * 1024**4
|
||||||
|
CPU_CRITICAL_TEMP = 99
|
||||||
|
CPU_FAILURE_TEMP = 90
|
||||||
|
CPU_TEST_MINUTES = 7
|
||||||
|
KEY_NVME = 'nvme_smart_health_information_log'
|
||||||
|
KEY_SMART = 'ata_smart_attributes'
|
||||||
|
KNOWN_DISK_ATTRIBUTES = {
|
||||||
|
# NVMe
|
||||||
|
'critical_warning': {'Blocking': True, 'Warning': None, 'Error': 1, 'Maximum': None, },
|
||||||
|
'media_errors': {'Blocking': False, 'Warning': None, 'Error': 1, 'Maximum': None, },
|
||||||
|
'power_on_hours': {'Blocking': False, 'Warning': 17532, 'Error': 26298, 'Maximum': 100000,},
|
||||||
|
'unsafe_shutdowns': {'Blocking': False, 'Warning': 1, 'Error': None, 'Maximum': None, },
|
||||||
|
# SMART
|
||||||
|
5: {'Hex': '05', 'Blocking': True, 'Warning': None, 'Error': 1, 'Maximum': None, },
|
||||||
|
9: {'Hex': '09', 'Blocking': False, 'Warning': 17532, 'Error': 26298, 'Maximum': 100000,},
|
||||||
|
10: {'Hex': '10', 'Blocking': False, 'Warning': 1, 'Error': 10, 'Maximum': 10000, },
|
||||||
|
184: {'Hex': 'B8', 'Blocking': False, 'Warning': 1, 'Error': 10, 'Maximum': 10000, },
|
||||||
|
187: {'Hex': 'BB', 'Blocking': False, 'Warning': 1, 'Error': 10, 'Maximum': 10000, },
|
||||||
|
188: {'Hex': 'BC', 'Blocking': False, 'Warning': 1, 'Error': 10, 'Maximum': 10000, },
|
||||||
|
196: {'Hex': 'C4', 'Blocking': False, 'Warning': 1, 'Error': 10, 'Maximum': 10000, },
|
||||||
|
197: {'Hex': 'C5', 'Blocking': True, 'Warning': None, 'Error': 1, 'Maximum': None, },
|
||||||
|
198: {'Hex': 'C6', 'Blocking': True, 'Warning': None, 'Error': 1, 'Maximum': None, },
|
||||||
|
199: {'Hex': 'C7', 'Blocking': False, 'Warning': None, 'Error': 1, 'Maximum': None, },
|
||||||
|
201: {'Hex': 'C9', 'Blocking': False, 'Warning': None, 'Error': 1, 'Maximum': 10000, },
|
||||||
|
}
|
||||||
|
KNOWN_DISK_MODELS = {
|
||||||
|
# model_regex: model_attributes
|
||||||
|
r'CT(250|500|1000|2000)MX500SSD(1|4)': {
|
||||||
|
197: {'Warning': 1, 'Error': 2, 'Note': '(MX500 thresholds)',},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
KNOWN_RAM_VENDOR_IDS = {
|
||||||
|
# https://github.com/hewigovens/hewigovens.github.com/wiki/Memory-vendor-code
|
||||||
|
'0x014F': 'Transcend',
|
||||||
|
'0x2C00': 'Micron',
|
||||||
|
'0x802C': 'Micron',
|
||||||
|
'0x80AD': 'Hynix',
|
||||||
|
'0x80CE': 'Samsung',
|
||||||
|
'0xAD00': 'Hynix',
|
||||||
|
'0xCE00': 'Samsung',
|
||||||
|
}
|
||||||
|
REGEX_POWER_ON_TIME = re.compile(
|
||||||
|
r'^(\d+)([Hh].*|\s+\(\d+\s+\d+\s+\d+\).*)'
|
||||||
|
)
|
||||||
|
SMC_IDS = {
|
||||||
|
# Sources: https://github.com/beltex/SMCKit/blob/master/SMCKit/SMC.swift
|
||||||
|
# http://www.opensource.apple.com/source/net_snmp/
|
||||||
|
# https://github.com/jedda/OSX-Monitoring-Tools
|
||||||
|
'TA0P': {'CPU Temp': False, 'Source': 'Ambient temp'},
|
||||||
|
'TA0S': {'CPU Temp': False, 'Source': 'PCIE Slot 1 Ambient'},
|
||||||
|
'TA1P': {'CPU Temp': False, 'Source': 'Ambient temp'},
|
||||||
|
'TA1S': {'CPU Temp': False, 'Source': 'PCIE Slot 1 PCB'},
|
||||||
|
'TA2S': {'CPU Temp': False, 'Source': 'PCIE Slot 2 Ambient'},
|
||||||
|
'TA3S': {'CPU Temp': False, 'Source': 'PCIE Slot 2 PCB'},
|
||||||
|
'TC0C': {'CPU Temp': True, 'Source': 'CPU Core 0'},
|
||||||
|
'TC0D': {'CPU Temp': True, 'Source': 'CPU die temp'},
|
||||||
|
'TC0H': {'CPU Temp': True, 'Source': 'CPU heatsink temp'},
|
||||||
|
'TC0P': {'CPU Temp': True, 'Source': 'CPU Ambient 1'},
|
||||||
|
'TC1C': {'CPU Temp': True, 'Source': 'CPU Core 1'},
|
||||||
|
'TC1P': {'CPU Temp': True, 'Source': 'CPU Ambient 2'},
|
||||||
|
'TC2C': {'CPU Temp': True, 'Source': 'CPU B Core 0'},
|
||||||
|
'TC2P': {'CPU Temp': True, 'Source': 'CPU B Ambient 1'},
|
||||||
|
'TC3C': {'CPU Temp': True, 'Source': 'CPU B Core 1'},
|
||||||
|
'TC3P': {'CPU Temp': True, 'Source': 'CPU B Ambient 2'},
|
||||||
|
'TCAC': {'CPU Temp': True, 'Source': 'CPU core from PCECI'},
|
||||||
|
'TCAH': {'CPU Temp': True, 'Source': 'CPU HeatSink'},
|
||||||
|
'TCBC': {'CPU Temp': True, 'Source': 'CPU B core from PCECI'},
|
||||||
|
'TCBH': {'CPU Temp': True, 'Source': 'CPU HeatSink'},
|
||||||
|
'Te1P': {'CPU Temp': False, 'Source': 'PCIE ambient temp'},
|
||||||
|
'Te1S': {'CPU Temp': False, 'Source': 'PCIE slot 1'},
|
||||||
|
'Te2S': {'CPU Temp': False, 'Source': 'PCIE slot 2'},
|
||||||
|
'Te3S': {'CPU Temp': False, 'Source': 'PCIE slot 3'},
|
||||||
|
'Te4S': {'CPU Temp': False, 'Source': 'PCIE slot 4'},
|
||||||
|
'TG0C': {'CPU Temp': False, 'Source': 'Mezzanine GPU Core'},
|
||||||
|
'TG0P': {'CPU Temp': False, 'Source': 'Mezzanine GPU Exhaust'},
|
||||||
|
'TH0P': {'CPU Temp': False, 'Source': 'Drive Bay 0'},
|
||||||
|
'TH1P': {'CPU Temp': False, 'Source': 'Drive Bay 1'},
|
||||||
|
'TH2P': {'CPU Temp': False, 'Source': 'Drive Bay 2'},
|
||||||
|
'TH3P': {'CPU Temp': False, 'Source': 'Drive Bay 3'},
|
||||||
|
'TH4P': {'CPU Temp': False, 'Source': 'Drive Bay 4'},
|
||||||
|
'TM0P': {'CPU Temp': False, 'Source': 'CPU DIMM Exit Ambient'},
|
||||||
|
'Tp0C': {'CPU Temp': False, 'Source': 'PSU1 Inlet Ambient'},
|
||||||
|
'Tp0P': {'CPU Temp': False, 'Source': 'PSU1 Inlet Ambient'},
|
||||||
|
'Tp1C': {'CPU Temp': False, 'Source': 'PSU1 Secondary Component'},
|
||||||
|
'Tp1P': {'CPU Temp': False, 'Source': 'PSU1 Primary Component'},
|
||||||
|
'Tp2P': {'CPU Temp': False, 'Source': 'PSU1 Secondary Component'},
|
||||||
|
'Tp3P': {'CPU Temp': False, 'Source': 'PSU2 Inlet Ambient'},
|
||||||
|
'Tp4P': {'CPU Temp': False, 'Source': 'PSU2 Primary Component'},
|
||||||
|
'Tp5P': {'CPU Temp': False, 'Source': 'PSU2 Secondary Component'},
|
||||||
|
'TS0C': {'CPU Temp': False, 'Source': 'CPU B DIMM Exit Ambient'},
|
||||||
|
}
|
||||||
|
TEMP_COLORS = {
|
||||||
|
float('-inf'): 'CYAN',
|
||||||
|
00: 'BLUE',
|
||||||
|
60: 'GREEN',
|
||||||
|
70: 'YELLOW',
|
||||||
|
80: 'ORANGE',
|
||||||
|
90: 'RED',
|
||||||
|
100: 'ORANGE_RED',
|
||||||
|
}
|
||||||
|
# THRESHOLDS: Rates used to determine HDD/SSD pass/fail
|
||||||
|
THRESH_HDD_MIN = 50 * 1024**2
|
||||||
|
THRESH_HDD_AVG_HIGH = 75 * 1024**2
|
||||||
|
THRESH_HDD_AVG_LOW = 65 * 1024**2
|
||||||
|
THRESH_SSD_MIN = 90 * 1024**2
|
||||||
|
THRESH_SSD_AVG_HIGH = 135 * 1024**2
|
||||||
|
THRESH_SSD_AVG_LOW = 100 * 1024**2
|
||||||
|
TMUX_SIDE_WIDTH = 20
|
||||||
|
TMUX_LAYOUT = OrderedDict({
|
||||||
|
'Top': {'height': 2, 'Check': True},
|
||||||
|
'Started': {'width': TMUX_SIDE_WIDTH, 'Check': True},
|
||||||
|
'Progress': {'width': TMUX_SIDE_WIDTH, 'Check': True},
|
||||||
|
# Testing panes
|
||||||
|
'Temps': {'height': 1000, 'Check': False},
|
||||||
|
'Prime95': {'height': 11, 'Check': False},
|
||||||
|
'SMART': {'height': 3, 'Check': True},
|
||||||
|
'badblocks': {'height': 5, 'Check': True},
|
||||||
|
'I/O Benchmark': {'height': 1000, 'Check': False},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
18
scripts/wk/cfg/log.py
Normal file
18
scripts/wk/cfg/log.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
"""WizardKit: Config - Log"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG = {
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'format': '[%(asctime)s %(levelname)s] [%(name)s.%(funcName)s] %(message)s',
|
||||||
|
'datefmt': '%Y-%m-%d %H%M%S%z',
|
||||||
|
}
|
||||||
|
DEFAULT = {
|
||||||
|
'level': 'INFO',
|
||||||
|
'format': '[%(asctime)s %(levelname)s] %(message)s',
|
||||||
|
'datefmt': '%Y-%m-%d %H%M%z',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
36
scripts/wk/cfg/main.py
Normal file
36
scripts/wk/cfg/main.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
"""WizardKit: Config - Main
|
||||||
|
|
||||||
|
NOTE: Non-standard formating is used for BASH/BATCH/PYTHON compatibility
|
||||||
|
"""
|
||||||
|
# pylint: disable=bad-whitespace
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
|
||||||
|
# Features
|
||||||
|
ENABLED_OPEN_LOGS=False
|
||||||
|
ENABLED_TICKET_NUMBERS=False
|
||||||
|
ENABLED_UPLOAD_DATA=False
|
||||||
|
|
||||||
|
# Main Kit
|
||||||
|
ARCHIVE_PASSWORD='Abracadabra'
|
||||||
|
KIT_NAME_FULL='WizardKit'
|
||||||
|
KIT_NAME_SHORT='WK'
|
||||||
|
SUPPORT_MESSAGE='Please let 2Shirt know by opening an issue on GitHub'
|
||||||
|
|
||||||
|
# Text Formatting
|
||||||
|
INDENT=4
|
||||||
|
WIDTH=32
|
||||||
|
|
||||||
|
# Live Linux
|
||||||
|
ROOT_PASSWORD='Abracadabra'
|
||||||
|
TECH_PASSWORD='Abracadabra'
|
||||||
|
|
||||||
|
# Time Zones
|
||||||
|
## See 'timedatectl list-timezones' for valid Linux values
|
||||||
|
## See 'tzutil /l' for valid Windows values
|
||||||
|
LINUX_TIME_ZONE='America/Denver'
|
||||||
|
WINDOWS_TIME_ZONE='Mountain Standard Time'
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
35
scripts/wk/cfg/net.py
Normal file
35
scripts/wk/cfg/net.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""WizardKit: Config - Net"""
|
||||||
|
# pylint: disable=bad-whitespace
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
|
||||||
|
# Servers
|
||||||
|
BACKUP_SERVERS = {
|
||||||
|
#'Server One': {
|
||||||
|
# 'Address': '10.0.0.10',
|
||||||
|
# 'Share': 'Backups',
|
||||||
|
# 'RO-User': 'restore',
|
||||||
|
# 'RO-Pass': 'Abracadabra',
|
||||||
|
# 'RW-User': 'backup',
|
||||||
|
# 'RW-Pass': 'Abracadabra',
|
||||||
|
# },
|
||||||
|
#'Server Two': {
|
||||||
|
# 'Address': 'servertwo.example.com',
|
||||||
|
# 'Share': 'Backups',
|
||||||
|
# 'RO-User': 'restore',
|
||||||
|
# 'RO-Pass': 'Abracadabra',
|
||||||
|
# 'RW-User': 'backup',
|
||||||
|
# 'RW-Pass': 'Abracadabra',
|
||||||
|
# },
|
||||||
|
}
|
||||||
|
CRASH_SERVER = {
|
||||||
|
#'Name': 'CrashServer',
|
||||||
|
#'Url': '',
|
||||||
|
#'User': '',
|
||||||
|
#'Pass': '',
|
||||||
|
#'Headers': {'X-Requested-With': 'XMLHttpRequest'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
|
|
@ -1,44 +1,15 @@
|
||||||
'''Wizard Kit: Settings - UFD'''
|
"""WizardKit: Config - UFD"""
|
||||||
# pylint: disable=C0326,E0611
|
# pylint: disable=bad-whitespace
|
||||||
# vim: sts=2 sw=2 ts=2
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from settings.main import KIT_NAME_FULL,KIT_NAME_SHORT
|
|
||||||
|
from wk.cfg.main import KIT_NAME_FULL
|
||||||
|
|
||||||
|
|
||||||
# General
|
# General
|
||||||
DOCSTRING = '''WizardKit: Build UFD
|
SOURCES = OrderedDict({
|
||||||
|
|
||||||
Usage:
|
|
||||||
build-ufd [options] --ufd-device PATH --linux PATH
|
|
||||||
[--linux-minimal PATH]
|
|
||||||
[--main-kit PATH]
|
|
||||||
[--winpe PATH]
|
|
||||||
[--eset PATH]
|
|
||||||
[--hdclone PATH]
|
|
||||||
[--extra-dir PATH]
|
|
||||||
build-ufd (-h | --help)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-c PATH, --hdclone PATH
|
|
||||||
-d PATH, --linux-dgpu PATH
|
|
||||||
-e PATH, --extra-dir PATH
|
|
||||||
-k PATH, --main-kit PATH
|
|
||||||
-l PATH, --linux PATH
|
|
||||||
-m PATH, --linux-minimal PATH
|
|
||||||
-s PATH, --eset PATH
|
|
||||||
-u PATH, --ufd-device PATH
|
|
||||||
-w PATH, --winpe PATH
|
|
||||||
|
|
||||||
-h --help Show this page
|
|
||||||
-M --use-mbr Use real MBR instead of GPT w/ Protective MBR
|
|
||||||
-F --force Bypass all confirmation messages. USE WITH EXTREME CAUTION!
|
|
||||||
-U --update Don't format device, just update
|
|
||||||
'''
|
|
||||||
ISO_LABEL = '{}_LINUX'.format(KIT_NAME_SHORT)
|
|
||||||
UFD_LABEL = '{}_UFD'.format(KIT_NAME_SHORT)
|
|
||||||
UFD_SOURCES = OrderedDict({
|
|
||||||
'Linux': {'Arg': '--linux', 'Type': 'ISO'},
|
'Linux': {'Arg': '--linux', 'Type': 'ISO'},
|
||||||
'Linux (dGPU)': {'Arg': '--linux-dgpu', 'Type': 'ISO'},
|
|
||||||
'Linux (Minimal)': {'Arg': '--linux-minimal', 'Type': 'ISO'},
|
'Linux (Minimal)': {'Arg': '--linux-minimal', 'Type': 'ISO'},
|
||||||
'WinPE': {'Arg': '--winpe', 'Type': 'ISO'},
|
'WinPE': {'Arg': '--winpe', 'Type': 'ISO'},
|
||||||
'ESET SysRescue': {'Arg': '--eset', 'Type': 'IMG'},
|
'ESET SysRescue': {'Arg': '--eset', 'Type': 'IMG'},
|
||||||
|
|
@ -52,7 +23,6 @@ BOOT_ENTRIES = {
|
||||||
# Path to check: Comment to remove
|
# Path to check: Comment to remove
|
||||||
'/arch_minimal': 'UFD-MINIMAL',
|
'/arch_minimal': 'UFD-MINIMAL',
|
||||||
'/casper': 'UFD-ESET',
|
'/casper': 'UFD-ESET',
|
||||||
'/dgpu': 'UFD-DGPU',
|
|
||||||
'/kernel.map': 'UFD-HDCLONE',
|
'/kernel.map': 'UFD-HDCLONE',
|
||||||
'/sources/boot.wim': 'UFD-WINPE',
|
'/sources/boot.wim': 'UFD-WINPE',
|
||||||
}
|
}
|
||||||
|
|
@ -87,12 +57,6 @@ ITEMS = {
|
||||||
('/EFI/boot', '/EFI/'),
|
('/EFI/boot', '/EFI/'),
|
||||||
('/EFI/memtest86', '/EFI/'),
|
('/EFI/memtest86', '/EFI/'),
|
||||||
),
|
),
|
||||||
'Linux (dGPU)': (
|
|
||||||
('/arch/boot/x86_64/archiso.img', '/dgpu/'),
|
|
||||||
('/arch/boot/x86_64/vmlinuz', '/dgpu/'),
|
|
||||||
('/arch/pkglist.x86_64.txt', '/dgpu/'),
|
|
||||||
('/arch/x86_64', '/dgpu/'),
|
|
||||||
),
|
|
||||||
'Linux (Minimal)': (
|
'Linux (Minimal)': (
|
||||||
('/arch/boot/x86_64/archiso.img', '/arch_minimal/'),
|
('/arch/boot/x86_64/archiso.img', '/arch_minimal/'),
|
||||||
('/arch/boot/x86_64/vmlinuz', '/arch_minimal/'),
|
('/arch/boot/x86_64/vmlinuz', '/arch_minimal/'),
|
||||||
|
|
@ -100,7 +64,7 @@ ITEMS = {
|
||||||
('/arch/x86_64', '/arch_minimal/'),
|
('/arch/x86_64', '/arch_minimal/'),
|
||||||
),
|
),
|
||||||
'Main Kit': (
|
'Main Kit': (
|
||||||
('/', '/{}/'.format(KIT_NAME_FULL)),
|
('/', f'/{KIT_NAME_FULL}/'),
|
||||||
),
|
),
|
||||||
'WinPE': (
|
'WinPE': (
|
||||||
('/bootmgr', '/'),
|
('/bootmgr', '/'),
|
||||||
|
|
@ -124,12 +88,11 @@ ITEMS_HIDDEN = (
|
||||||
# Linux (all versions)
|
# Linux (all versions)
|
||||||
'arch',
|
'arch',
|
||||||
'arch_minimal',
|
'arch_minimal',
|
||||||
'dgpu',
|
|
||||||
'EFI',
|
'EFI',
|
||||||
'isolinux',
|
'isolinux',
|
||||||
# Main Kit
|
# Main Kit
|
||||||
'{}/.bin'.format(KIT_NAME_FULL),
|
f'{KIT_NAME_FULL}/.bin',
|
||||||
'{}/.cbin'.format(KIT_NAME_FULL),
|
f'{KIT_NAME_FULL}/.cbin',
|
||||||
# WinPE
|
# WinPE
|
||||||
'boot',
|
'boot',
|
||||||
'bootmgr',
|
'bootmgr',
|
||||||
|
|
@ -139,5 +102,6 @@ ITEMS_HIDDEN = (
|
||||||
'sources',
|
'sources',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print("This file is not meant to be called directly.")
|
print("This file is not meant to be called directly.")
|
||||||
45
scripts/wk/debug.py
Normal file
45
scripts/wk/debug.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
"""WizardKit: Debug Functions"""
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
|
||||||
|
# Classes
|
||||||
|
class Debug():
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
"""Object used when dumping debug data."""
|
||||||
|
def method(self):
|
||||||
|
"""Dummy method used to identify functions vs data."""
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
DEBUG_CLASS = Debug()
|
||||||
|
METHOD_TYPE = type(DEBUG_CLASS.method)
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def generate_object_report(obj, indent=0):
|
||||||
|
"""Generate debug report for obj, returns list."""
|
||||||
|
report = []
|
||||||
|
|
||||||
|
# Dump object data
|
||||||
|
for name in dir(obj):
|
||||||
|
attr = getattr(obj, name)
|
||||||
|
|
||||||
|
# Skip methods and private attributes
|
||||||
|
if isinstance(attr, METHOD_TYPE) or name.startswith('_'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add attribute to report (expanded if necessary)
|
||||||
|
if isinstance(attr, dict):
|
||||||
|
report.append(f'{name}:')
|
||||||
|
for key, value in sorted(attr.items()):
|
||||||
|
report.append(f'{" "*(indent+1)}{key}: {str(value)}')
|
||||||
|
else:
|
||||||
|
report.append(f'{" "*indent}{name}: {str(attr)}')
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
222
scripts/wk/exe.py
Normal file
222
scripts/wk/exe.py
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
"""WizardKit: Execution functions"""
|
||||||
|
#vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from threading import Thread
|
||||||
|
from queue import Queue, Empty
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Classes
|
||||||
|
class NonBlockingStreamReader():
|
||||||
|
"""Class to allow non-blocking reads from a stream."""
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
# Credits:
|
||||||
|
## https://gist.github.com/EyalAr/7915597
|
||||||
|
## https://stackoverflow.com/a/4896288
|
||||||
|
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
self.queue = Queue()
|
||||||
|
|
||||||
|
def populate_queue(stream, queue):
|
||||||
|
"""Collect lines from stream and put them in queue."""
|
||||||
|
while True:
|
||||||
|
line = stream.read(1)
|
||||||
|
if line:
|
||||||
|
queue.put(line)
|
||||||
|
|
||||||
|
self.thread = start_thread(
|
||||||
|
populate_queue,
|
||||||
|
args=(self.stream, self.queue),
|
||||||
|
)
|
||||||
|
|
||||||
|
def read(self, timeout=None):
|
||||||
|
"""Read from queue if possible, returns item from queue."""
|
||||||
|
try:
|
||||||
|
return self.queue.get(block=timeout is not None, timeout=timeout)
|
||||||
|
except Empty:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save_to_file(self, proc, out_path):
|
||||||
|
"""Continuously save output to file while proc is running."""
|
||||||
|
while proc.poll() is None:
|
||||||
|
out = b''
|
||||||
|
out_bytes = b''
|
||||||
|
while out is not None:
|
||||||
|
out = self.read(0.1)
|
||||||
|
if out:
|
||||||
|
out_bytes += out
|
||||||
|
with open(out_path, 'a') as _f:
|
||||||
|
_f.write(out_bytes.decode('utf-8', errors='ignore'))
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def build_cmd_kwargs(cmd, minimized=False, pipe=True, shell=False, **kwargs):
|
||||||
|
"""Build kwargs for use by subprocess functions, returns dict.
|
||||||
|
|
||||||
|
Specifically subprocess.run() and subprocess.Popen().
|
||||||
|
NOTE: If no encoding specified then UTF-8 will be used.
|
||||||
|
"""
|
||||||
|
LOG.debug(
|
||||||
|
'cmd: %s, minimized: %s, pipe: %s, shell: %s',
|
||||||
|
cmd, minimized, pipe, shell,
|
||||||
|
)
|
||||||
|
LOG.debug('kwargs: %s', kwargs)
|
||||||
|
cmd_kwargs = {
|
||||||
|
'args': cmd,
|
||||||
|
'shell': shell,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add additional kwargs if applicable
|
||||||
|
for key in 'check cwd encoding errors stderr stdin stdout'.split():
|
||||||
|
if key in kwargs:
|
||||||
|
cmd_kwargs[key] = kwargs[key]
|
||||||
|
|
||||||
|
# Default to UTF-8 encoding
|
||||||
|
if not ('encoding' in cmd_kwargs or 'errors' in cmd_kwargs):
|
||||||
|
cmd_kwargs['encoding'] = 'utf-8'
|
||||||
|
cmd_kwargs['errors'] = 'ignore'
|
||||||
|
|
||||||
|
# Start minimized
|
||||||
|
if minimized:
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags = subprocess.STARTF_USESHOWWINDOW
|
||||||
|
startupinfo.wShowWindow = 6
|
||||||
|
cmd_kwargs['startupinfo'] = startupinfo
|
||||||
|
|
||||||
|
|
||||||
|
# Pipe output
|
||||||
|
if pipe:
|
||||||
|
cmd_kwargs['stderr'] = subprocess.PIPE
|
||||||
|
cmd_kwargs['stdout'] = subprocess.PIPE
|
||||||
|
|
||||||
|
# Done
|
||||||
|
LOG.debug('cmd_kwargs: %s', cmd_kwargs)
|
||||||
|
return cmd_kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def get_json_from_command(cmd, check=True, encoding='utf-8', errors='ignore'):
|
||||||
|
"""Capture JSON content from cmd output, returns dict.
|
||||||
|
|
||||||
|
If the data can't be decoded then either an exception is raised
|
||||||
|
or an empty dict is returned depending on errors.
|
||||||
|
"""
|
||||||
|
json_data = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = run_program(cmd, check=check, encoding=encoding, errors=errors)
|
||||||
|
json_data = json.loads(proc.stdout)
|
||||||
|
except (subprocess.CalledProcessError, json.decoder.JSONDecodeError):
|
||||||
|
if errors != 'ignore':
|
||||||
|
raise
|
||||||
|
|
||||||
|
return json_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_procs(name, exact=True):
|
||||||
|
"""Get process object(s) based on name, returns list of proc objects."""
|
||||||
|
LOG.debug('name: %s, exact: %s', name, exact)
|
||||||
|
processes = []
|
||||||
|
regex = f'^{name}$' if exact else name
|
||||||
|
|
||||||
|
# Iterate over all processes
|
||||||
|
for proc in psutil.process_iter():
|
||||||
|
if re.search(regex, proc.name(), re.IGNORECASE):
|
||||||
|
processes.append(proc)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return processes
|
||||||
|
|
||||||
|
|
||||||
|
def kill_procs(name, exact=True, force=False, timeout=30):
|
||||||
|
"""Kill all processes matching name (case-insensitively).
|
||||||
|
|
||||||
|
NOTE: Under Posix systems this will send SIGINT to allow processes
|
||||||
|
to gracefully exit.
|
||||||
|
|
||||||
|
If force is True then it will wait until timeout specified and then
|
||||||
|
send SIGKILL to any processes still alive.
|
||||||
|
"""
|
||||||
|
LOG.debug(
|
||||||
|
'name: %s, exact: %s, force: %s, timeout: %s',
|
||||||
|
name, exact, force, timeout,
|
||||||
|
)
|
||||||
|
target_procs = get_procs(name, exact=exact)
|
||||||
|
for proc in target_procs:
|
||||||
|
proc.terminate()
|
||||||
|
|
||||||
|
# Force kill if necesary
|
||||||
|
if force:
|
||||||
|
results = psutil.wait_procs(target_procs, timeout=timeout)
|
||||||
|
for proc in results[1]: # Alive processes
|
||||||
|
proc.kill()
|
||||||
|
|
||||||
|
|
||||||
|
def popen_program(cmd, minimized=False, pipe=False, shell=False, **kwargs):
|
||||||
|
"""Run program and return a subprocess.Popen object."""
|
||||||
|
LOG.debug(
|
||||||
|
'cmd: %s, minimized: %s, pipe: %s, shell: %s',
|
||||||
|
cmd, minimized, pipe, shell,
|
||||||
|
)
|
||||||
|
LOG.debug('kwargs: %s', kwargs)
|
||||||
|
cmd_kwargs = build_cmd_kwargs(
|
||||||
|
cmd,
|
||||||
|
minimized=minimized,
|
||||||
|
pipe=pipe,
|
||||||
|
shell=shell,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
# Ready to run program
|
||||||
|
return subprocess.Popen(**cmd_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def run_program(cmd, check=True, pipe=True, shell=False, **kwargs):
|
||||||
|
# pylint: disable=subprocess-run-check
|
||||||
|
"""Run program and return a subprocess.CompletedProcess object."""
|
||||||
|
LOG.debug(
|
||||||
|
'cmd: %s, check: %s, pipe: %s, shell: %s',
|
||||||
|
cmd, check, pipe, shell,
|
||||||
|
)
|
||||||
|
LOG.debug('kwargs: %s', kwargs)
|
||||||
|
cmd_kwargs = build_cmd_kwargs(
|
||||||
|
cmd,
|
||||||
|
check=check,
|
||||||
|
pipe=pipe,
|
||||||
|
shell=shell,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
# Ready to run program
|
||||||
|
return subprocess.run(**cmd_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def start_thread(function, args=None, daemon=True):
|
||||||
|
"""Run function as thread in background, returns Thread object."""
|
||||||
|
args = args if args else []
|
||||||
|
thread = Thread(target=function, args=args, daemon=daemon)
|
||||||
|
thread.start()
|
||||||
|
return thread
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_procs(name, exact=True, timeout=None):
|
||||||
|
"""Wait for all process matching name."""
|
||||||
|
LOG.debug('name: %s, exact: %s, timeout: %s', name, exact, timeout)
|
||||||
|
target_procs = get_procs(name, exact=exact)
|
||||||
|
results = psutil.wait_procs(target_procs, timeout=timeout)
|
||||||
|
|
||||||
|
# Raise exception if necessary
|
||||||
|
if results[1]: # Alive processes
|
||||||
|
raise psutil.TimeoutExpired(name=name, seconds=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
151
scripts/wk/graph.py
Normal file
151
scripts/wk/graph.py
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
"""WizardKit: Graph Functions"""
|
||||||
|
# pylint: disable=bad-whitespace
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from wk.std import color_string
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
GRAPH_HORIZONTAL = ('▁', '▂', '▃', '▄', '▅', '▆', '▇', '█')
|
||||||
|
GRAPH_VERTICAL = (
|
||||||
|
'▏', '▎', '▍', '▌',
|
||||||
|
'▋', '▊', '▉', '█',
|
||||||
|
'█▏', '█▎', '█▍', '█▌',
|
||||||
|
'█▋', '█▊', '█▉', '██',
|
||||||
|
'██▏', '██▎', '██▍', '██▌',
|
||||||
|
'██▋', '██▊', '██▉', '███',
|
||||||
|
'███▏', '███▎', '███▍', '███▌',
|
||||||
|
'███▋', '███▊', '███▉', '████',
|
||||||
|
)
|
||||||
|
# SCALE_STEPS: These scales allow showing differences between HDDs and SSDs
|
||||||
|
# on the same graph.
|
||||||
|
SCALE_STEPS = {
|
||||||
|
8: [2**(0.56*(x+1))+(16*(x+1)) for x in range(8)],
|
||||||
|
16: [2**(0.56*(x+1))+(16*(x+1)) for x in range(16)],
|
||||||
|
32: [2**(0.56*(x+1)/2)+(16*(x+1)/2) for x in range(32)],
|
||||||
|
}
|
||||||
|
# THRESHOLDS: These are the rate_list (in MB/s) used to color graphs
|
||||||
|
THRESH_FAIL = 65 * 1024**2
|
||||||
|
THRESH_WARN = 135 * 1024**2
|
||||||
|
THRESH_GREAT = 750 * 1024**2
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def generate_horizontal_graph(rate_list, graph_width=40, oneline=False):
|
||||||
|
"""Generate horizontal graph from rate_list, returns list."""
|
||||||
|
graph = ['', '', '', '']
|
||||||
|
scale = 8 if oneline else 32
|
||||||
|
|
||||||
|
# Build graph
|
||||||
|
for rate in merge_rates(rate_list, graph_width=graph_width):
|
||||||
|
step = get_graph_step(rate, scale=scale)
|
||||||
|
|
||||||
|
# Set color
|
||||||
|
rate_color = None
|
||||||
|
if rate < THRESH_FAIL:
|
||||||
|
rate_color = 'RED'
|
||||||
|
elif rate < THRESH_WARN:
|
||||||
|
rate_color = 'YELLOW'
|
||||||
|
elif rate > THRESH_GREAT:
|
||||||
|
rate_color = 'GREEN'
|
||||||
|
|
||||||
|
# Build graph
|
||||||
|
full_block = color_string((GRAPH_HORIZONTAL[-1],), (rate_color,))
|
||||||
|
if step >= 24:
|
||||||
|
graph[0] += color_string((GRAPH_HORIZONTAL[step-24],), (rate_color,))
|
||||||
|
graph[1] += full_block
|
||||||
|
graph[2] += full_block
|
||||||
|
graph[3] += full_block
|
||||||
|
elif step >= 16:
|
||||||
|
graph[0] += ' '
|
||||||
|
graph[1] += color_string((GRAPH_HORIZONTAL[step-16],), (rate_color,))
|
||||||
|
graph[2] += full_block
|
||||||
|
graph[3] += full_block
|
||||||
|
elif step >= 8:
|
||||||
|
graph[0] += ' '
|
||||||
|
graph[1] += ' '
|
||||||
|
graph[2] += color_string((GRAPH_HORIZONTAL[step-8],), (rate_color,))
|
||||||
|
graph[3] += full_block
|
||||||
|
else:
|
||||||
|
graph[0] += ' '
|
||||||
|
graph[1] += ' '
|
||||||
|
graph[2] += ' '
|
||||||
|
graph[3] += color_string((GRAPH_HORIZONTAL[step],), (rate_color,))
|
||||||
|
|
||||||
|
# Done
|
||||||
|
if oneline:
|
||||||
|
graph = graph[-1:]
|
||||||
|
return graph
|
||||||
|
|
||||||
|
|
||||||
|
def get_graph_step(rate, scale=16):
|
||||||
|
"""Get graph step based on rate and scale, returns int."""
|
||||||
|
rate_in_mb = rate / (1024**2)
|
||||||
|
step = 0
|
||||||
|
|
||||||
|
# Iterate over scale_steps backwards
|
||||||
|
for _r in range(scale-1, -1, -1):
|
||||||
|
if rate_in_mb >= SCALE_STEPS[scale][_r]:
|
||||||
|
step = _r
|
||||||
|
break
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return step
|
||||||
|
|
||||||
|
|
||||||
|
def merge_rates(rates, graph_width=40):
|
||||||
|
"""Merge rates to have entries equal to the width, returns list."""
|
||||||
|
merged_rates = []
|
||||||
|
offset = 0
|
||||||
|
slice_width = int(len(rates) / graph_width)
|
||||||
|
|
||||||
|
# Merge rates
|
||||||
|
for _i in range(graph_width):
|
||||||
|
merged_rates.append(sum(rates[offset:offset+slice_width])/slice_width)
|
||||||
|
offset += slice_width
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return merged_rates
|
||||||
|
|
||||||
|
|
||||||
|
def vertical_graph_line(percent, rate, scale=32):
|
||||||
|
"""Build colored graph string using thresholds, returns str."""
|
||||||
|
color_bar = None
|
||||||
|
color_rate = None
|
||||||
|
step = get_graph_step(rate, scale=scale)
|
||||||
|
|
||||||
|
# Set colors
|
||||||
|
if rate < THRESH_FAIL:
|
||||||
|
color_bar = 'RED'
|
||||||
|
color_rate = 'YELLOW'
|
||||||
|
elif rate < THRESH_WARN:
|
||||||
|
color_bar = 'YELLOW'
|
||||||
|
color_rate = 'YELLOW'
|
||||||
|
elif rate > THRESH_GREAT:
|
||||||
|
color_bar = 'GREEN'
|
||||||
|
color_rate = 'GREEN'
|
||||||
|
|
||||||
|
# Build string
|
||||||
|
line = color_string(
|
||||||
|
strings=(
|
||||||
|
f'{percent:5.1f}%',
|
||||||
|
f'{GRAPH_VERTICAL[step]:<4}',
|
||||||
|
f'{rate/(1000**2):6.1f} MB/s',
|
||||||
|
),
|
||||||
|
colors=(
|
||||||
|
None,
|
||||||
|
color_bar,
|
||||||
|
color_rate,
|
||||||
|
),
|
||||||
|
sep=' ',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
6
scripts/wk/hw/__init__.py
Normal file
6
scripts/wk/hw/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
"""WizardKit: hw module init"""
|
||||||
|
|
||||||
|
from wk.hw import ddrescue
|
||||||
|
from wk.hw import diags
|
||||||
|
from wk.hw import obj
|
||||||
|
from wk.hw import sensors
|
||||||
2061
scripts/wk/hw/ddrescue.py
Normal file
2061
scripts/wk/hw/ddrescue.py
Normal file
File diff suppressed because it is too large
Load diff
1369
scripts/wk/hw/diags.py
Normal file
1369
scripts/wk/hw/diags.py
Normal file
File diff suppressed because it is too large
Load diff
844
scripts/wk/hw/obj.py
Normal file
844
scripts/wk/hw/obj.py
Normal file
|
|
@ -0,0 +1,844 @@
|
||||||
|
"""WizardKit: Hardware objects (mostly)"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import pathlib
|
||||||
|
import plistlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from wk.cfg.hw import (
|
||||||
|
ATTRIBUTE_COLORS,
|
||||||
|
KEY_NVME,
|
||||||
|
KEY_SMART,
|
||||||
|
KNOWN_DISK_ATTRIBUTES,
|
||||||
|
KNOWN_DISK_MODELS,
|
||||||
|
KNOWN_RAM_VENDOR_IDS,
|
||||||
|
REGEX_POWER_ON_TIME,
|
||||||
|
)
|
||||||
|
from wk.cfg.main import KIT_NAME_SHORT
|
||||||
|
from wk.exe import get_json_from_command, run_program
|
||||||
|
from wk.std import (
|
||||||
|
PLATFORM,
|
||||||
|
bytes_to_string,
|
||||||
|
color_string,
|
||||||
|
sleep,
|
||||||
|
string_to_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
NVME_WARNING_KEYS = (
|
||||||
|
'spare_below_threshold',
|
||||||
|
'reliability_degraded',
|
||||||
|
'volatile_memory_backup_failed',
|
||||||
|
)
|
||||||
|
WK_LABEL_REGEX = re.compile(
|
||||||
|
fr'{KIT_NAME_SHORT}_(LINUX|UFD)',
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Exception Classes
|
||||||
|
class CriticalHardwareError(RuntimeError):
|
||||||
|
"""Exception used for critical hardware failures."""
|
||||||
|
|
||||||
|
class SMARTNotSupportedError(TypeError):
|
||||||
|
"""Exception used for disks lacking SMART support."""
|
||||||
|
|
||||||
|
class SMARTSelfTestInProgressError(RuntimeError):
|
||||||
|
"""Exception used when a SMART self-test is in progress."""
|
||||||
|
|
||||||
|
|
||||||
|
# Classes
|
||||||
|
class BaseObj():
|
||||||
|
"""Base object for tracking device data."""
|
||||||
|
def __init__(self):
|
||||||
|
self.tests = OrderedDict()
|
||||||
|
|
||||||
|
def all_tests_passed(self):
|
||||||
|
"""Check if all tests passed, returns bool."""
|
||||||
|
return all([results.passed for results in self.tests.values()])
|
||||||
|
|
||||||
|
def any_test_failed(self):
|
||||||
|
"""Check if any test failed, returns bool."""
|
||||||
|
return any([results.failed for results in self.tests.values()])
|
||||||
|
|
||||||
|
|
||||||
|
class CpuRam(BaseObj):
|
||||||
|
"""Object for tracking CPU & RAM specific data."""
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.description = 'Unknown'
|
||||||
|
self.details = {}
|
||||||
|
self.ram_total = 'Unknown'
|
||||||
|
self.ram_dimms = []
|
||||||
|
self.tests = OrderedDict()
|
||||||
|
|
||||||
|
# Update details
|
||||||
|
self.get_cpu_details()
|
||||||
|
self.get_ram_details()
|
||||||
|
|
||||||
|
def generate_report(self):
|
||||||
|
"""Generate CPU & RAM report, returns list."""
|
||||||
|
report = []
|
||||||
|
report.append(color_string('Device', 'BLUE'))
|
||||||
|
report.append(f' {self.description}')
|
||||||
|
|
||||||
|
# Include RAM details
|
||||||
|
report.append(color_string('RAM', 'BLUE'))
|
||||||
|
report.append(f' {self.ram_total} ({", ".join(self.ram_dimms)})')
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
for test in self.tests.values():
|
||||||
|
report.extend(test.report)
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
def get_cpu_details(self):
|
||||||
|
"""Get CPU details using OS specific methods."""
|
||||||
|
if PLATFORM == 'Darwin':
|
||||||
|
cmd = 'sysctl -n machdep.cpu.brand_string'.split()
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
self.description = re.sub(r'\s+', ' ', proc.stdout.strip())
|
||||||
|
elif PLATFORM == 'Linux':
|
||||||
|
cmd = ['lscpu', '--json']
|
||||||
|
json_data = get_json_from_command(cmd)
|
||||||
|
for line in json_data.get('lscpu', [{}]):
|
||||||
|
_field = line.get('field', '').replace(':', '')
|
||||||
|
_data = line.get('data', '')
|
||||||
|
if not (_field or _data):
|
||||||
|
# Skip
|
||||||
|
continue
|
||||||
|
self.details[_field] = _data
|
||||||
|
|
||||||
|
self.description = self.details.get('Model name', '')
|
||||||
|
|
||||||
|
# Replace empty description
|
||||||
|
if not self.description:
|
||||||
|
self.description = 'Unknown CPU'
|
||||||
|
|
||||||
|
def get_ram_details(self):
|
||||||
|
"""Get RAM details using OS specific methods."""
|
||||||
|
if PLATFORM == 'Darwin':
|
||||||
|
dimm_list = get_ram_list_macos()
|
||||||
|
elif PLATFORM == 'Linux':
|
||||||
|
dimm_list = get_ram_list_linux()
|
||||||
|
|
||||||
|
details = {'Total': 0}
|
||||||
|
for dimm_details in dimm_list:
|
||||||
|
size, manufacturer = dimm_details
|
||||||
|
if size <= 0:
|
||||||
|
# Skip empty DIMMs
|
||||||
|
continue
|
||||||
|
description = f'{bytes_to_string(size)} {manufacturer}'
|
||||||
|
details['Total'] += size
|
||||||
|
if description in details:
|
||||||
|
details[description] += 1
|
||||||
|
else:
|
||||||
|
details[description] = 1
|
||||||
|
|
||||||
|
# Save details
|
||||||
|
self.ram_total = bytes_to_string(details.pop('Total', 0))
|
||||||
|
self.ram_dimms = [
|
||||||
|
f'{count}x {desc}' for desc, count in sorted(details.items())
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Disk(BaseObj):
|
||||||
|
"""Object for tracking disk specific data."""
|
||||||
|
def __init__(self, path):
|
||||||
|
super().__init__()
|
||||||
|
self.attributes = {}
|
||||||
|
self.description = 'Unknown'
|
||||||
|
self.details = {}
|
||||||
|
self.notes = []
|
||||||
|
self.path = pathlib.Path(path).resolve()
|
||||||
|
self.smartctl = {}
|
||||||
|
self.tests = OrderedDict()
|
||||||
|
|
||||||
|
# Update details
|
||||||
|
self.get_details()
|
||||||
|
self.enable_smart()
|
||||||
|
self.update_smart_details()
|
||||||
|
if not self.is_4k_aligned():
|
||||||
|
self.add_note('One or more partitions are not 4K aligned', 'YELLOW')
|
||||||
|
|
||||||
|
def abort_self_test(self):
|
||||||
|
"""Abort currently running non-captive self-test."""
|
||||||
|
cmd = ['sudo', 'smartctl', '--abort', self.path]
|
||||||
|
run_program(cmd, check=False)
|
||||||
|
|
||||||
|
def add_note(self, note, color=None):
|
||||||
|
"""Add note that will be included in the disk report."""
|
||||||
|
if color:
|
||||||
|
note = color_string(note, color)
|
||||||
|
if note not in self.notes:
|
||||||
|
self.notes.append(note)
|
||||||
|
self.notes.sort()
|
||||||
|
|
||||||
|
def check_attributes(self, only_blocking=False):
|
||||||
|
"""Check if any known attributes are failing, returns bool."""
|
||||||
|
attributes_ok = True
|
||||||
|
known_attributes = get_known_disk_attributes(self.details['model'])
|
||||||
|
for attr, value in self.attributes.items():
|
||||||
|
# Skip unknown attributes
|
||||||
|
if attr not in known_attributes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get thresholds
|
||||||
|
blocking_attribute = known_attributes[attr].get('Blocking', False)
|
||||||
|
err_thresh = known_attributes[attr].get('Error', None)
|
||||||
|
max_thresh = known_attributes[attr].get('Maximum', None)
|
||||||
|
if not max_thresh:
|
||||||
|
max_thresh = float('inf')
|
||||||
|
|
||||||
|
# Skip non-blocking attributes if necessary
|
||||||
|
if only_blocking and not blocking_attribute:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip informational attributes
|
||||||
|
if not err_thresh:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check attribute
|
||||||
|
if err_thresh <= value['raw'] < max_thresh:
|
||||||
|
attributes_ok = False
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return attributes_ok
|
||||||
|
|
||||||
|
def disable_disk_tests(self):
|
||||||
|
"""Disable all tests."""
|
||||||
|
LOG.warning('Disabling all tests for: %s', self.path)
|
||||||
|
for test in self.tests.values():
|
||||||
|
if test.status in ('Pending', 'Working'):
|
||||||
|
test.set_status('Denied')
|
||||||
|
test.disabled = True
|
||||||
|
|
||||||
|
def enable_smart(self):
|
||||||
|
"""Try enabling SMART for this disk."""
|
||||||
|
cmd = [
|
||||||
|
'sudo',
|
||||||
|
'smartctl',
|
||||||
|
'--tolerance=permissive',
|
||||||
|
'--smart=on',
|
||||||
|
self.path,
|
||||||
|
]
|
||||||
|
run_program(cmd, check=False)
|
||||||
|
|
||||||
|
def generate_attribute_report(self):
|
||||||
|
"""Generate attribute report, returns list."""
|
||||||
|
known_attributes = get_known_disk_attributes(self.details['model'])
|
||||||
|
report = []
|
||||||
|
for attr, value in sorted(self.attributes.items()):
|
||||||
|
note = ''
|
||||||
|
value_color = 'GREEN'
|
||||||
|
|
||||||
|
# Skip attributes not in our list
|
||||||
|
if attr not in known_attributes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for attribute note
|
||||||
|
note = known_attributes[attr].get('Note', '')
|
||||||
|
|
||||||
|
# ID / Name
|
||||||
|
label = f'{attr:>3}'
|
||||||
|
if isinstance(attr, int):
|
||||||
|
# Assuming SMART, include hex ID and name
|
||||||
|
label += f' / {str(hex(attr))[2:].upper():0>2}: {value["name"]}'
|
||||||
|
label = f' {label.replace("_", " "):38}'
|
||||||
|
|
||||||
|
# Value color
|
||||||
|
for threshold, color in ATTRIBUTE_COLORS:
|
||||||
|
threshold_val = known_attributes[attr].get(threshold, None)
|
||||||
|
if threshold_val and value['raw'] >= threshold_val:
|
||||||
|
value_color = color
|
||||||
|
if threshold == 'Error':
|
||||||
|
note = '(failed)'
|
||||||
|
elif threshold == 'Maximum':
|
||||||
|
note = '(invalid?)'
|
||||||
|
|
||||||
|
# 199/C7 warning
|
||||||
|
if str(attr) == '199' and value['raw'] > 0:
|
||||||
|
note = '(bad cable?)'
|
||||||
|
|
||||||
|
# Build colored string and append to report
|
||||||
|
line = color_string(
|
||||||
|
[label, value['raw_str'], note],
|
||||||
|
[None, value_color, 'YELLOW'],
|
||||||
|
)
|
||||||
|
report.append(line)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return report
|
||||||
|
|
||||||
|
def generate_report(self, header=True):
|
||||||
|
"""Generate Disk report, returns list."""
|
||||||
|
report = []
|
||||||
|
if header:
|
||||||
|
report.append(color_string(f'Device ({self.path.name})', 'BLUE'))
|
||||||
|
report.append(f' {self.description}')
|
||||||
|
|
||||||
|
# Attributes
|
||||||
|
if self.attributes:
|
||||||
|
if header:
|
||||||
|
report.append(color_string('Attributes', 'BLUE'))
|
||||||
|
report.extend(self.generate_attribute_report())
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
if self.notes:
|
||||||
|
report.append(color_string('Notes', 'BLUE'))
|
||||||
|
for note in self.notes:
|
||||||
|
report.append(f' {note}')
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
for test in self.tests.values():
|
||||||
|
report.extend(test.report)
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
def get_details(self):
|
||||||
|
"""Get disk details using OS specific methods.
|
||||||
|
|
||||||
|
Required details default to generic descriptions
|
||||||
|
and are converted to the correct type.
|
||||||
|
"""
|
||||||
|
if PLATFORM == 'Darwin':
|
||||||
|
self.details = get_disk_details_macos(self.path)
|
||||||
|
elif PLATFORM == 'Linux':
|
||||||
|
self.details = get_disk_details_linux(self.path)
|
||||||
|
|
||||||
|
# Set necessary details
|
||||||
|
self.details['bus'] = str(self.details.get('bus', '???')).upper()
|
||||||
|
self.details['bus'] = self.details['bus'].replace('IMAGE', 'Image')
|
||||||
|
self.details['bus'] = self.details['bus'].replace('NVME', 'NVMe')
|
||||||
|
self.details['log-sec'] = self.details.get('log-sec', 512)
|
||||||
|
self.details['model'] = self.details.get('model', 'Unknown Model')
|
||||||
|
self.details['name'] = self.details.get('name', self.path)
|
||||||
|
self.details['phy-sec'] = self.details.get('phy-sec', 512)
|
||||||
|
self.details['serial'] = self.details.get('serial', 'Unknown Serial')
|
||||||
|
self.details['size'] = self.details.get('size', -1)
|
||||||
|
self.details['ssd'] = self.details.get('ssd', False)
|
||||||
|
|
||||||
|
# Ensure certain attributes types
|
||||||
|
for attr in ['bus', 'model', 'name', 'serial']:
|
||||||
|
if not isinstance(self.details[attr], str):
|
||||||
|
self.details[attr] = str(self.details[attr])
|
||||||
|
for attr in ['phy-sec', 'size']:
|
||||||
|
if not isinstance(self.details[attr], int):
|
||||||
|
try:
|
||||||
|
self.details[attr] = int(self.details[attr])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
LOG.error('Invalid disk %s: %s', attr, self.details[attr])
|
||||||
|
self.details[attr] = -1
|
||||||
|
|
||||||
|
# Set description
|
||||||
|
self.description = '{size_str} ({bus}) {model} {serial}'.format(
|
||||||
|
size_str=bytes_to_string(self.details['size'], use_binary=False),
|
||||||
|
**self.details,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_labels(self):
|
||||||
|
"""Build list of labels for this disk, returns list."""
|
||||||
|
labels = []
|
||||||
|
|
||||||
|
# Add all labels from lsblk
|
||||||
|
for disk in [self.details, *self.details.get('children', [])]:
|
||||||
|
labels.append(disk.get('label', ''))
|
||||||
|
labels.append(disk.get('partlabel', ''))
|
||||||
|
|
||||||
|
# Remove empty labels
|
||||||
|
labels = [str(label) for label in labels if label]
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return labels
|
||||||
|
|
||||||
|
def get_smart_self_test_details(self):
|
||||||
|
"""Shorthand to get deeply nested self-test details, returns dict."""
|
||||||
|
details = {}
|
||||||
|
try:
|
||||||
|
details = self.smartctl['ata_smart_data']['self_test']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
# Assuming disk lacks SMART support, ignore and return empty dict.
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return details
|
||||||
|
|
||||||
|
def is_4k_aligned(self):
|
||||||
|
"""Check that all disk partitions are aligned, returns bool."""
|
||||||
|
aligned = True
|
||||||
|
if PLATFORM == 'Darwin':
|
||||||
|
aligned = is_4k_aligned_macos(self.details)
|
||||||
|
elif PLATFORM == 'Linux':
|
||||||
|
aligned = is_4k_aligned_linux(self.path, self.details['phy-sec'])
|
||||||
|
#TODO: Add checks for other OS
|
||||||
|
|
||||||
|
return aligned
|
||||||
|
|
||||||
|
def safety_checks(self):
|
||||||
|
"""Run safety checks and raise an exception if necessary."""
|
||||||
|
blocking_event_encountered = False
|
||||||
|
self.update_smart_details()
|
||||||
|
|
||||||
|
# Attributes
|
||||||
|
if not self.check_attributes(only_blocking=True):
|
||||||
|
blocking_event_encountered = True
|
||||||
|
LOG.error('%s: Blocked for failing attribute(s)', self.path)
|
||||||
|
|
||||||
|
# NVMe status
|
||||||
|
nvme_status = self.smartctl.get('smart_status', {}).get('nvme', {})
|
||||||
|
if nvme_status.get('media_read_only', False):
|
||||||
|
blocking_event_encountered = True
|
||||||
|
msg = 'Media has been placed in read-only mode'
|
||||||
|
self.add_note(msg, 'RED')
|
||||||
|
LOG.error('%s %s', self.path, msg)
|
||||||
|
for key in NVME_WARNING_KEYS:
|
||||||
|
if nvme_status.get(key, False):
|
||||||
|
msg = key.replace('_', ' ')
|
||||||
|
self.add_note(msg, 'YELLOW')
|
||||||
|
LOG.warning('%s %s', self.path, msg)
|
||||||
|
|
||||||
|
# SMART overall assessment
|
||||||
|
smart_passed = True
|
||||||
|
try:
|
||||||
|
smart_passed = self.smartctl['smart_status']['passed']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
# Assuming disk doesn't support SMART overall assessment
|
||||||
|
pass
|
||||||
|
if not smart_passed:
|
||||||
|
blocking_event_encountered = True
|
||||||
|
msg = 'SMART overall self-assessment: Failed'
|
||||||
|
self.add_note(msg, 'RED')
|
||||||
|
LOG.error('%s %s', self.path, msg)
|
||||||
|
|
||||||
|
# Raise blocking exception if necessary
|
||||||
|
if blocking_event_encountered:
|
||||||
|
raise CriticalHardwareError(f'Critical error(s) for: {self.path}')
|
||||||
|
|
||||||
|
# SMART self-test status
|
||||||
|
test_details = self.get_smart_self_test_details()
|
||||||
|
if 'remaining_percent' in test_details.get('status', ''):
|
||||||
|
msg = f'SMART self-test in progress for: {self.path}'
|
||||||
|
LOG.error(msg)
|
||||||
|
raise SMARTSelfTestInProgressError(msg)
|
||||||
|
|
||||||
|
def run_self_test(self, log_path):
|
||||||
|
"""Run disk self-test and check if it passed, returns bool.
|
||||||
|
|
||||||
|
NOTE: This function is here to reserve a place for future
|
||||||
|
NVMe self-tests announced in NVMe spec v1.3.
|
||||||
|
"""
|
||||||
|
result = self.run_smart_self_test(log_path)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def run_smart_self_test(self, log_path):
|
||||||
|
"""Run SMART self-test and check if it passed, returns bool.
|
||||||
|
|
||||||
|
NOTE: An exception will be raised if the disk lacks SMART support.
|
||||||
|
"""
|
||||||
|
finished = False
|
||||||
|
result = None
|
||||||
|
started = False
|
||||||
|
status_str = 'Starting self-test...'
|
||||||
|
test_details = self.get_smart_self_test_details()
|
||||||
|
test_minutes = 15
|
||||||
|
|
||||||
|
# Check if disk supports self-tests
|
||||||
|
if not test_details:
|
||||||
|
raise SMARTNotSupportedError(
|
||||||
|
f'SMART self-test not supported for {self.path}')
|
||||||
|
|
||||||
|
# Get real test length
|
||||||
|
test_minutes = test_details.get('polling_minutes', {}).get('short', 5)
|
||||||
|
test_minutes = int(test_minutes) + 10
|
||||||
|
|
||||||
|
# Start test
|
||||||
|
cmd = [
|
||||||
|
'sudo',
|
||||||
|
'smartctl',
|
||||||
|
'--tolerance=normal',
|
||||||
|
'--test=short',
|
||||||
|
self.path,
|
||||||
|
]
|
||||||
|
run_program(cmd, check=False)
|
||||||
|
|
||||||
|
# Monitor progress (in five second intervals)
|
||||||
|
for _i in range(int(test_minutes*60/5)):
|
||||||
|
sleep(5)
|
||||||
|
|
||||||
|
# Update status
|
||||||
|
self.update_smart_details()
|
||||||
|
test_details = self.get_smart_self_test_details()
|
||||||
|
|
||||||
|
# Check test progress
|
||||||
|
if started:
|
||||||
|
status_str = test_details.get('status', {}).get('string', 'Unknown')
|
||||||
|
status_str = status_str.capitalize()
|
||||||
|
|
||||||
|
# Update log
|
||||||
|
with open(log_path, 'w') as _f:
|
||||||
|
_f.write(f'SMART self-test status for {self.path}:\n {status_str}')
|
||||||
|
|
||||||
|
# Check if finished
|
||||||
|
if 'remaining_percent' not in test_details['status']:
|
||||||
|
finished = True
|
||||||
|
break
|
||||||
|
|
||||||
|
elif 'remaining_percent' in test_details['status']:
|
||||||
|
started = True
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
if finished:
|
||||||
|
result = test_details.get('status', {}).get('passed', False)
|
||||||
|
elif started:
|
||||||
|
raise TimeoutError(f'SMART self-test timed out for {self.path}')
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return result
|
||||||
|
|
||||||
|
def update_smart_details(self):
|
||||||
|
"""Update SMART details via smartctl."""
|
||||||
|
self.attributes = {}
|
||||||
|
cmd = [
|
||||||
|
'sudo',
|
||||||
|
'smartctl',
|
||||||
|
'--tolerance=verypermissive',
|
||||||
|
'--all',
|
||||||
|
'--json',
|
||||||
|
self.path,
|
||||||
|
]
|
||||||
|
self.smartctl = get_json_from_command(cmd, check=False)
|
||||||
|
|
||||||
|
# Check for attributes
|
||||||
|
if KEY_NVME in self.smartctl:
|
||||||
|
for name, value in self.smartctl[KEY_NVME].items():
|
||||||
|
try:
|
||||||
|
self.attributes[name] = {
|
||||||
|
'name': name,
|
||||||
|
'raw': int(value),
|
||||||
|
'raw_str': str(value),
|
||||||
|
}
|
||||||
|
except ValueError:
|
||||||
|
# Ignoring invalid attribute
|
||||||
|
LOG.error('Invalid NVMe attribute: %s %s', name, value)
|
||||||
|
elif KEY_SMART in self.smartctl:
|
||||||
|
for attribute in self.smartctl[KEY_SMART].get('table', {}):
|
||||||
|
try:
|
||||||
|
_id = int(attribute['id'])
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
# Ignoring invalid attribute
|
||||||
|
LOG.error('Invalid SMART attribute: %s', attribute)
|
||||||
|
continue
|
||||||
|
name = str(attribute.get('name', 'Unknown')).replace('_', ' ').title()
|
||||||
|
raw = int(attribute.get('raw', {}).get('value', -1))
|
||||||
|
raw_str = attribute.get('raw', {}).get('string', 'Unknown')
|
||||||
|
|
||||||
|
# Fix power-on time
|
||||||
|
match = REGEX_POWER_ON_TIME.match(raw_str)
|
||||||
|
if _id == 9 and match:
|
||||||
|
raw = int(match.group(1))
|
||||||
|
|
||||||
|
# Add to dict
|
||||||
|
self.attributes[_id] = {
|
||||||
|
'name': name, 'raw': raw, 'raw_str': raw_str}
|
||||||
|
|
||||||
|
# Add note if necessary
|
||||||
|
if not self.attributes:
|
||||||
|
self.add_note('No NVMe or SMART data available', 'YELLOW')
|
||||||
|
|
||||||
|
|
||||||
|
class Test():
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
"""Object for tracking test specific data."""
|
||||||
|
def __init__(self, dev, label):
|
||||||
|
self.dev = dev
|
||||||
|
self.disabled = False
|
||||||
|
self.failed = False
|
||||||
|
self.label = label
|
||||||
|
self.passed = False
|
||||||
|
self.report = []
|
||||||
|
self.status = 'Pending'
|
||||||
|
|
||||||
|
def set_status(self, status):
|
||||||
|
"""Update status string."""
|
||||||
|
if self.disabled:
|
||||||
|
# Don't change status if disabled
|
||||||
|
return
|
||||||
|
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def get_disk_details_linux(path):
|
||||||
|
"""Get disk details using lsblk, returns dict."""
|
||||||
|
cmd = ['lsblk', '--bytes', '--json', '--output-all', '--paths', path]
|
||||||
|
json_data = get_json_from_command(cmd, check=False)
|
||||||
|
details = json_data.get('blockdevices', [{}])[0]
|
||||||
|
|
||||||
|
# Fix details
|
||||||
|
for dev in [details, *details.get('children', [])]:
|
||||||
|
dev['bus'] = dev.pop('tran', '???')
|
||||||
|
dev['parent'] = dev.pop('pkname', None)
|
||||||
|
dev['ssd'] = not dev.pop('rota', True)
|
||||||
|
if 'loop' in str(path) and dev['bus'] is None:
|
||||||
|
dev['bus'] = 'Image'
|
||||||
|
dev['model'] = ''
|
||||||
|
dev['serial'] = ''
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return details
|
||||||
|
|
||||||
|
|
||||||
|
def get_disk_details_macos(path):
|
||||||
|
"""Get disk details using diskutil, returns dict."""
|
||||||
|
details = {}
|
||||||
|
|
||||||
|
# Get "list" details
|
||||||
|
cmd = ['diskutil', 'list', '-plist', path]
|
||||||
|
proc = run_program(cmd, check=False, encoding=None, errors=None)
|
||||||
|
try:
|
||||||
|
plist_data = plistlib.loads(proc.stdout)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
# Invalid / corrupt plist data? return empty dict to avoid crash
|
||||||
|
LOG.error('Failed to get diskutil list for %s', path)
|
||||||
|
return details
|
||||||
|
|
||||||
|
# Parse "list" details
|
||||||
|
details = plist_data.get('AllDisksAndPartitions', [{}])[0]
|
||||||
|
details['children'] = details.pop('Partitions', [])
|
||||||
|
details['path'] = path
|
||||||
|
for child in details['children']:
|
||||||
|
child['path'] = path.with_name(child.get('DeviceIdentifier', 'null'))
|
||||||
|
|
||||||
|
# Get "info" details
|
||||||
|
for dev in [details, *details['children']]:
|
||||||
|
cmd = ['diskutil', 'info', '-plist', dev['path']]
|
||||||
|
proc = run_program(cmd, check=False, encoding=None, errors=None)
|
||||||
|
try:
|
||||||
|
plist_data = plistlib.loads(proc.stdout)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
LOG.error('Failed to get diskutil info for %s', path)
|
||||||
|
continue #Skip
|
||||||
|
|
||||||
|
# Parse "info" details
|
||||||
|
dev.update(plist_data)
|
||||||
|
dev['bus'] = dev.pop('BusProtocol', '???')
|
||||||
|
dev['fstype'] = dev.pop('FilesystemType', '')
|
||||||
|
dev['label'] = dev.pop('VolumeName', '')
|
||||||
|
dev['model'] = dev.pop('MediaName', 'Unknown')
|
||||||
|
dev['mountpoint'] = dev.pop('MountPoint', '')
|
||||||
|
dev['phy-sec'] = dev.pop('DeviceBlockSize', 512)
|
||||||
|
dev['serial'] = get_disk_serial_macos(dev['path'])
|
||||||
|
dev['size'] = dev.pop('Size', -1)
|
||||||
|
dev['ssd'] = dev.pop('SolidState', False)
|
||||||
|
dev['vendor'] = ''
|
||||||
|
if not dev.get('WholeDisk', True):
|
||||||
|
dev['parent'] = dev.pop('ParentWholeDisk', None)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return details
|
||||||
|
|
||||||
|
|
||||||
|
def get_disk_serial_macos(path):
|
||||||
|
"""Get disk serial using system_profiler, returns str."""
|
||||||
|
cmd = ['sudo', 'smartctl', '--info', '--json', path]
|
||||||
|
smart_info = get_json_from_command(cmd)
|
||||||
|
return smart_info.get('serial_number', 'Unknown Serial')
|
||||||
|
|
||||||
|
|
||||||
|
def get_disks(skip_kits=False):
|
||||||
|
"""Get disks using OS-specific methods, returns list."""
|
||||||
|
disks = []
|
||||||
|
if PLATFORM == 'Darwin':
|
||||||
|
disks = get_disks_macos()
|
||||||
|
elif PLATFORM == 'Linux':
|
||||||
|
disks = get_disks_linux()
|
||||||
|
|
||||||
|
# Skip WK disks
|
||||||
|
if skip_kits:
|
||||||
|
disks = [
|
||||||
|
disk_obj for disk_obj in disks
|
||||||
|
if not any(
|
||||||
|
[WK_LABEL_REGEX.search(label) for label in disk_obj.get_labels()]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return disks
|
||||||
|
|
||||||
|
|
||||||
|
def get_disks_linux():
|
||||||
|
"""Get disks via lsblk, returns list."""
|
||||||
|
cmd = ['lsblk', '--json', '--nodeps', '--paths']
|
||||||
|
disks = []
|
||||||
|
|
||||||
|
# Add valid disks
|
||||||
|
json_data = get_json_from_command(cmd)
|
||||||
|
for disk in json_data.get('blockdevices', []):
|
||||||
|
disk_obj = Disk(disk['name'])
|
||||||
|
|
||||||
|
# Skip loopback devices, optical devices, etc
|
||||||
|
if disk_obj.details['type'] != 'disk':
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add disk
|
||||||
|
disks.append(disk_obj)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return disks
|
||||||
|
|
||||||
|
|
||||||
|
def get_disks_macos():
|
||||||
|
"""Get disks via diskutil, returns list."""
|
||||||
|
cmd = ['diskutil', 'list', '-plist', 'physical']
|
||||||
|
disks = []
|
||||||
|
|
||||||
|
# Get info from diskutil
|
||||||
|
proc = run_program(cmd, encoding=None, errors=None)
|
||||||
|
try:
|
||||||
|
plist_data = plistlib.loads(proc.stdout)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
# Invalid / corrupt plist data? return empty list to avoid crash
|
||||||
|
LOG.error('Failed to get diskutil list')
|
||||||
|
return disks
|
||||||
|
|
||||||
|
# Add valid disks
|
||||||
|
for disk in plist_data['WholeDisks']:
|
||||||
|
disks.append(Disk(f'/dev/{disk}'))
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return disks
|
||||||
|
|
||||||
|
|
||||||
|
def get_known_disk_attributes(model):
|
||||||
|
"""Get known NVMe/SMART attributes (model specific), returns str."""
|
||||||
|
known_attributes = KNOWN_DISK_ATTRIBUTES.copy()
|
||||||
|
|
||||||
|
# Apply model-specific data
|
||||||
|
for regex, data in KNOWN_DISK_MODELS.items():
|
||||||
|
if re.search(regex, model):
|
||||||
|
for attr, thresholds in data.items():
|
||||||
|
if attr in known_attributes:
|
||||||
|
known_attributes[attr].update(thresholds)
|
||||||
|
else:
|
||||||
|
known_attributes[attr] = thresholds
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return known_attributes
|
||||||
|
|
||||||
|
|
||||||
|
def get_ram_list_linux():
|
||||||
|
"""Get RAM list using dmidecode."""
|
||||||
|
cmd = ['sudo', 'dmidecode', '--type', 'memory']
|
||||||
|
dimm_list = []
|
||||||
|
manufacturer = 'Unknown'
|
||||||
|
size = 0
|
||||||
|
|
||||||
|
# Get DMI data
|
||||||
|
proc = run_program(cmd)
|
||||||
|
dmi_data = proc.stdout.splitlines()
|
||||||
|
|
||||||
|
# Parse data
|
||||||
|
for line in dmi_data:
|
||||||
|
line = line.strip()
|
||||||
|
if line == 'Memory Device':
|
||||||
|
# Reset vars
|
||||||
|
manufacturer = 'Unknown'
|
||||||
|
size = 0
|
||||||
|
elif line.startswith('Size:'):
|
||||||
|
size = line.replace('Size: ', '')
|
||||||
|
try:
|
||||||
|
size = string_to_bytes(size, assume_binary=True)
|
||||||
|
except ValueError:
|
||||||
|
# Assuming empty module
|
||||||
|
size = 0
|
||||||
|
elif line.startswith('Manufacturer:'):
|
||||||
|
manufacturer = line.replace('Manufacturer: ', '')
|
||||||
|
dimm_list.append([size, manufacturer])
|
||||||
|
|
||||||
|
# Save details
|
||||||
|
return dimm_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_ram_list_macos():
|
||||||
|
"""Get RAM list using system_profiler."""
|
||||||
|
dimm_list = []
|
||||||
|
|
||||||
|
# Get and parse plist data
|
||||||
|
cmd = [
|
||||||
|
'system_profiler',
|
||||||
|
'-xml',
|
||||||
|
'SPMemoryDataType',
|
||||||
|
]
|
||||||
|
proc = run_program(cmd, check=False, encoding=None, errors=None)
|
||||||
|
try:
|
||||||
|
plist_data = plistlib.loads(proc.stdout)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
# Ignore and return an empty list
|
||||||
|
return dimm_list
|
||||||
|
|
||||||
|
# Check DIMM data
|
||||||
|
dimm_details = plist_data[0].get('_items', [{}])[0].get('_items', [])
|
||||||
|
for dimm in dimm_details:
|
||||||
|
manufacturer = dimm.get('dimm_manufacturer', None)
|
||||||
|
manufacturer = KNOWN_RAM_VENDOR_IDS.get(
|
||||||
|
manufacturer,
|
||||||
|
f'Unknown ({manufacturer})')
|
||||||
|
size = dimm.get('dimm_size', '0 GB')
|
||||||
|
try:
|
||||||
|
size = string_to_bytes(size, assume_binary=True)
|
||||||
|
except ValueError:
|
||||||
|
# Empty DIMM?
|
||||||
|
LOG.error('Invalid DIMM size: %s', size)
|
||||||
|
continue
|
||||||
|
dimm_list.append([size, manufacturer])
|
||||||
|
|
||||||
|
# Save details
|
||||||
|
return dimm_list
|
||||||
|
|
||||||
|
|
||||||
|
def is_4k_aligned_macos(disk_details):
|
||||||
|
"""Check partition alignment using diskutil info, returns bool."""
|
||||||
|
aligned = True
|
||||||
|
|
||||||
|
# Check partitions
|
||||||
|
for part in disk_details.get('children', []):
|
||||||
|
offset = part.get('PartitionMapPartitionOffset', 0)
|
||||||
|
if not offset:
|
||||||
|
# Assuming offset couldn't be found and it defaulted to 0
|
||||||
|
# NOTE: Just logging the error, not bailing
|
||||||
|
LOG.error('Failed to get partition offset for %s', part['path'])
|
||||||
|
aligned = aligned and offset >= 0 and offset % 4096 == 0
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return aligned
|
||||||
|
|
||||||
|
|
||||||
|
def is_4k_aligned_linux(dev_path, physical_sector_size):
|
||||||
|
"""Check partition alignment using lsblk, returns bool."""
|
||||||
|
aligned = True
|
||||||
|
cmd = [
|
||||||
|
'sudo',
|
||||||
|
'sfdisk',
|
||||||
|
'--json',
|
||||||
|
dev_path,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Get partition details
|
||||||
|
json_data = get_json_from_command(cmd)
|
||||||
|
|
||||||
|
# Check partitions
|
||||||
|
for part in json_data.get('partitiontable', {}).get('partitions', []):
|
||||||
|
offset = physical_sector_size * part.get('start', -1)
|
||||||
|
aligned = aligned and offset >= 0 and offset % 4096 == 0
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return aligned
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
412
scripts/wk/hw/sensors.py
Normal file
412
scripts/wk/hw/sensors.py
Normal file
|
|
@ -0,0 +1,412 @@
|
||||||
|
"""WizardKit: Hardware sensors"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
from subprocess import CalledProcessError
|
||||||
|
|
||||||
|
from wk.cfg.hw import CPU_CRITICAL_TEMP, SMC_IDS, TEMP_COLORS
|
||||||
|
from wk.exe import run_program, start_thread
|
||||||
|
from wk.std import PLATFORM, color_string, sleep
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
LM_SENSORS_CPU_REGEX = re.compile(r'(core|k\d+)temp', re.IGNORECASE)
|
||||||
|
SMC_REGEX = re.compile(
|
||||||
|
r'^\s*(?P<ID>\w{4})'
|
||||||
|
r'\s+\[(?P<Type>.*)\]'
|
||||||
|
r'\s+(?P<Value>.*?)'
|
||||||
|
r'\s*\(bytes (?P<Bytes>.*)\)$'
|
||||||
|
)
|
||||||
|
SENSOR_SOURCE_WIDTH = 25 if PLATFORM == 'Darwin' else 20
|
||||||
|
|
||||||
|
|
||||||
|
# Error Classes
|
||||||
|
class ThermalLimitReachedError(RuntimeError):
|
||||||
|
"""Raised when the thermal threshold is reached."""
|
||||||
|
|
||||||
|
|
||||||
|
# Classes
|
||||||
|
class Sensors():
|
||||||
|
"""Class for holding sensor specific data."""
|
||||||
|
def __init__(self):
|
||||||
|
self.background_thread = None
|
||||||
|
self.data = get_sensor_data()
|
||||||
|
self.out_path = None
|
||||||
|
|
||||||
|
def clear_temps(self):
|
||||||
|
"""Clear saved temps but keep structure"""
|
||||||
|
for adapters in self.data.values():
|
||||||
|
for sources in adapters.values():
|
||||||
|
for source_data in sources.values():
|
||||||
|
source_data['Temps'] = []
|
||||||
|
|
||||||
|
def cpu_max_temp(self):
|
||||||
|
"""Get max temp from any CPU source, returns float.
|
||||||
|
|
||||||
|
NOTE: If no temps are found this returns zero.
|
||||||
|
"""
|
||||||
|
max_temp = 0.0
|
||||||
|
|
||||||
|
# Check all CPU Temps
|
||||||
|
for section, adapters in self.data.items():
|
||||||
|
if not section.startswith('CPU'):
|
||||||
|
continue
|
||||||
|
for sources in adapters.values():
|
||||||
|
for source_data in sources.values():
|
||||||
|
max_temp = max(max_temp, source_data.get('Max', 0))
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return max_temp
|
||||||
|
|
||||||
|
def cpu_reached_critical_temp(self):
|
||||||
|
"""Check if CPU reached CPU_CRITICAL_TEMP, returns bool."""
|
||||||
|
for section, adapters in self.data.items():
|
||||||
|
if not section.startswith('CPU'):
|
||||||
|
# Limit to CPU temps
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ugly section
|
||||||
|
for sources in adapters.values():
|
||||||
|
for source_data in sources.values():
|
||||||
|
if source_data.get('Max', -1) >= CPU_CRITICAL_TEMP:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Didn't return above so temps are within the threshold
|
||||||
|
return False
|
||||||
|
|
||||||
|
def generate_report(self, *temp_labels, colored=True, only_cpu=False):
|
||||||
|
"""Generate report based on given temp_labels, returns list."""
|
||||||
|
report = []
|
||||||
|
|
||||||
|
for section, adapters in sorted(self.data.items()):
|
||||||
|
if only_cpu and not section.startswith('CPU'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ugly section
|
||||||
|
for adapter, sources in sorted(adapters.items()):
|
||||||
|
report.append(fix_sensor_name(adapter))
|
||||||
|
for source, source_data in sorted(sources.items()):
|
||||||
|
line = f'{fix_sensor_name(source):{SENSOR_SOURCE_WIDTH}} '
|
||||||
|
for label in temp_labels:
|
||||||
|
if label != 'Current':
|
||||||
|
line += f' {label.lower()}: '
|
||||||
|
line += get_temp_str(
|
||||||
|
source_data.get(label, '???'),
|
||||||
|
colored=colored,
|
||||||
|
)
|
||||||
|
report.append(line)
|
||||||
|
if not only_cpu:
|
||||||
|
report.append('')
|
||||||
|
|
||||||
|
# Handle empty reports
|
||||||
|
if not report:
|
||||||
|
report = [
|
||||||
|
color_string('WARNING: No sensors found', 'YELLOW'),
|
||||||
|
'',
|
||||||
|
'Please monitor temps manually',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return report
|
||||||
|
|
||||||
|
def monitor_to_file(
|
||||||
|
self, out_path,
|
||||||
|
exit_on_thermal_limit=True, temp_labels=None, thermal_action=None):
|
||||||
|
"""Write report to path every second until stopped.
|
||||||
|
|
||||||
|
thermal_action is a cmd to run if ThermalLimitReachedError is caught.
|
||||||
|
"""
|
||||||
|
stop_path = pathlib.Path(out_path).resolve().with_suffix('.stop')
|
||||||
|
if not temp_labels:
|
||||||
|
temp_labels = ('Current', 'Max')
|
||||||
|
|
||||||
|
# Start loop
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
self.update_sensor_data(exit_on_thermal_limit)
|
||||||
|
except ThermalLimitReachedError:
|
||||||
|
if thermal_action:
|
||||||
|
run_program(thermal_action, check=False)
|
||||||
|
report = self.generate_report(*temp_labels)
|
||||||
|
with open(out_path, 'w') as _f:
|
||||||
|
_f.write('\n'.join(report))
|
||||||
|
|
||||||
|
# Check if we should stop
|
||||||
|
if stop_path.exists():
|
||||||
|
break
|
||||||
|
|
||||||
|
# Sleep before next loop
|
||||||
|
sleep(0.5)
|
||||||
|
|
||||||
|
def save_average_temps(self, temp_label, seconds=10):
|
||||||
|
# pylint: disable=unused-variable
|
||||||
|
"""Save average temps under temp_label over provided seconds.."""
|
||||||
|
self.clear_temps()
|
||||||
|
|
||||||
|
# Get temps
|
||||||
|
for i in range(seconds):
|
||||||
|
self.update_sensor_data()
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
# Calculate averages
|
||||||
|
for adapters in self.data.values():
|
||||||
|
for sources in adapters.values():
|
||||||
|
for source_data in sources.values():
|
||||||
|
temps = source_data['Temps']
|
||||||
|
source_data[temp_label] = sum(temps) / len(temps)
|
||||||
|
|
||||||
|
def start_background_monitor(
|
||||||
|
self, out_path,
|
||||||
|
exit_on_thermal_limit=True, temp_labels=None, thermal_action=None):
|
||||||
|
"""Start background thread to save report to file.
|
||||||
|
|
||||||
|
thermal_action is a cmd to run if ThermalLimitReachedError is caught.
|
||||||
|
"""
|
||||||
|
if self.background_thread:
|
||||||
|
raise RuntimeError('Background thread already running')
|
||||||
|
|
||||||
|
self.out_path = pathlib.Path(out_path)
|
||||||
|
self.background_thread = start_thread(
|
||||||
|
self.monitor_to_file,
|
||||||
|
args=(out_path, exit_on_thermal_limit, temp_labels, thermal_action),
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop_background_monitor(self):
|
||||||
|
"""Stop background thread."""
|
||||||
|
self.out_path.with_suffix('.stop').touch()
|
||||||
|
self.background_thread.join()
|
||||||
|
|
||||||
|
# Reset vars to None
|
||||||
|
self.background_thread = None
|
||||||
|
self.out_path = None
|
||||||
|
|
||||||
|
def update_sensor_data(self, exit_on_thermal_limit=True):
|
||||||
|
"""Update sensor data via OS-specific means."""
|
||||||
|
if PLATFORM == 'Darwin':
|
||||||
|
self.update_sensor_data_macos(exit_on_thermal_limit)
|
||||||
|
elif PLATFORM == 'Linux':
|
||||||
|
self.update_sensor_data_linux(exit_on_thermal_limit)
|
||||||
|
|
||||||
|
def update_sensor_data_linux(self, exit_on_thermal_limit=True):
|
||||||
|
"""Update sensor data via lm_sensors."""
|
||||||
|
lm_sensor_data = get_sensor_data_lm()
|
||||||
|
for section, adapters in self.data.items():
|
||||||
|
for adapter, sources in adapters.items():
|
||||||
|
for source, source_data in sources.items():
|
||||||
|
try:
|
||||||
|
label = source_data['Label']
|
||||||
|
temp = lm_sensor_data[adapter][source][label]
|
||||||
|
source_data['Current'] = temp
|
||||||
|
source_data['Max'] = max(temp, source_data['Max'])
|
||||||
|
source_data['Temps'].append(temp)
|
||||||
|
except KeyError:
|
||||||
|
# Dumb workaround for Dell sensors with changing source names
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Raise exception if thermal limit reached
|
||||||
|
if exit_on_thermal_limit and section == 'CPUTemps':
|
||||||
|
if source_data['Current'] >= CPU_CRITICAL_TEMP:
|
||||||
|
raise ThermalLimitReachedError('CPU temps reached limit')
|
||||||
|
|
||||||
|
def update_sensor_data_macos(self, exit_on_thermal_limit=True):
|
||||||
|
"""Update sensor data via SMC."""
|
||||||
|
for section, adapters in self.data.items():
|
||||||
|
for sources in adapters.values():
|
||||||
|
for source_data in sources.values():
|
||||||
|
cmd = ['smc', '-k', source_data['Label'], '-r']
|
||||||
|
proc = run_program(cmd)
|
||||||
|
match = SMC_REGEX.match(proc.stdout.strip())
|
||||||
|
try:
|
||||||
|
temp = float(match.group('Value'))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
LOG.error('Failed to update temp %s', source_data['Label'])
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update source
|
||||||
|
source_data['Current'] = temp
|
||||||
|
source_data['Max'] = max(temp, source_data['Max'])
|
||||||
|
source_data['Temps'].append(temp)
|
||||||
|
|
||||||
|
# Raise exception if thermal limit reached
|
||||||
|
if exit_on_thermal_limit and section == 'CPUTemps':
|
||||||
|
if source_data['Current'] >= CPU_CRITICAL_TEMP:
|
||||||
|
raise ThermalLimitReachedError('CPU temps reached limit')
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def fix_sensor_name(name):
|
||||||
|
"""Cleanup sensor name, returns str."""
|
||||||
|
name = re.sub(r'^(\w+)-(\w+)-(\w+)', r'\1 (\2 \3)', name, re.IGNORECASE)
|
||||||
|
name = name.title()
|
||||||
|
name = name.replace('Acpi', 'ACPI')
|
||||||
|
name = name.replace('ACPItz', 'ACPI TZ')
|
||||||
|
name = name.replace('Coretemp', 'CoreTemp')
|
||||||
|
name = name.replace('Cpu', 'CPU')
|
||||||
|
name = name.replace('Id ', 'ID ')
|
||||||
|
name = name.replace('Isa ', 'ISA ')
|
||||||
|
name = name.replace('Pci ', 'PCI ')
|
||||||
|
name = name.replace('Smc', 'SMC')
|
||||||
|
name = re.sub(r'(\D+)(\d+)', r'\1 \2', name, re.IGNORECASE)
|
||||||
|
name = re.sub(r'^K (\d+)Temp', r'AMD K\1 Temps', name, re.IGNORECASE)
|
||||||
|
name = re.sub(r'T(ctl|die)', r'CPU (T\1)', name, re.IGNORECASE)
|
||||||
|
name = re.sub(r'\s+', ' ', name)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def get_sensor_data():
|
||||||
|
"""Get sensor data via OS-specific means, returns dict."""
|
||||||
|
sensor_data = {}
|
||||||
|
if PLATFORM == 'Darwin':
|
||||||
|
sensor_data = get_sensor_data_macos()
|
||||||
|
elif PLATFORM == 'Linux':
|
||||||
|
sensor_data = get_sensor_data_linux()
|
||||||
|
|
||||||
|
return sensor_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_sensor_data_linux():
|
||||||
|
"""Get sensor data via lm_sensors, returns dict."""
|
||||||
|
raw_lm_sensor_data = get_sensor_data_lm()
|
||||||
|
sensor_data = {'CPUTemps': {}, 'Others': {}}
|
||||||
|
|
||||||
|
# Parse lm_sensor data
|
||||||
|
for adapter, sources in raw_lm_sensor_data.items():
|
||||||
|
section = 'Others'
|
||||||
|
if LM_SENSORS_CPU_REGEX.search(adapter):
|
||||||
|
section = 'CPUTemps'
|
||||||
|
sensor_data[section][adapter] = {}
|
||||||
|
sources.pop('Adapter', None)
|
||||||
|
|
||||||
|
# Find current temp and add to dict
|
||||||
|
## current temp is labeled xxxx_input
|
||||||
|
for source, labels in sources.items():
|
||||||
|
for label, temp in labels.items():
|
||||||
|
if label.startswith('fan') or label.startswith('in'):
|
||||||
|
# Skip fan RPMs and voltages
|
||||||
|
continue
|
||||||
|
if 'input' in label:
|
||||||
|
sensor_data[section][adapter][source] = {
|
||||||
|
'Current': temp,
|
||||||
|
'Label': label,
|
||||||
|
'Max': temp,
|
||||||
|
'Temps': [temp],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove empty adapters
|
||||||
|
if not sensor_data[section][adapter]:
|
||||||
|
sensor_data[section].pop(adapter)
|
||||||
|
|
||||||
|
# Remove empty sections
|
||||||
|
for adapters in sensor_data.values():
|
||||||
|
adapters = {source: source_data for source, source_data in adapters.items()
|
||||||
|
if source_data}
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return sensor_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_sensor_data_lm():
|
||||||
|
"""Get raw sensor data via lm_sensors, returns dict."""
|
||||||
|
raw_lm_sensor_data = {}
|
||||||
|
cmd = ['sensors', '-j']
|
||||||
|
|
||||||
|
# Get raw data
|
||||||
|
try:
|
||||||
|
proc = run_program(cmd)
|
||||||
|
except CalledProcessError:
|
||||||
|
# Assuming no sensors available, return empty dict
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Workaround for bad sensors
|
||||||
|
raw_data = []
|
||||||
|
for line in proc.stdout.splitlines():
|
||||||
|
if line.strip() == ',':
|
||||||
|
# Assuming malformatted line caused by missing data
|
||||||
|
continue
|
||||||
|
raw_data.append(line)
|
||||||
|
|
||||||
|
# Parse JSON data
|
||||||
|
try:
|
||||||
|
raw_lm_sensor_data = json.loads('\n'.join(raw_data))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Still broken, just return the empty dict
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return raw_lm_sensor_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_sensor_data_macos():
|
||||||
|
"""Get sensor data via SMC, returns dict.
|
||||||
|
|
||||||
|
NOTE: The data is structured like the lm_sensor data.
|
||||||
|
"""
|
||||||
|
cmd = ['smc', '-l']
|
||||||
|
sensor_data = {'CPUTemps': {'SMC (CPU)': {}}, 'Others': {'SMC (Other)': {}}}
|
||||||
|
|
||||||
|
# Parse SMC data
|
||||||
|
proc = run_program(cmd)
|
||||||
|
for line in proc.stdout.splitlines():
|
||||||
|
tmp = SMC_REGEX.match(line.strip())
|
||||||
|
if tmp:
|
||||||
|
value = tmp.group('Value')
|
||||||
|
try:
|
||||||
|
LOG.debug('Invalid sensor: %s', tmp.group('ID'))
|
||||||
|
value = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
# Skip this sensor
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Only add known sensor IDs
|
||||||
|
sensor_id = tmp.group('ID')
|
||||||
|
if sensor_id not in SMC_IDS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add to dict
|
||||||
|
section = 'Others'
|
||||||
|
adapter = 'SMC (Other)'
|
||||||
|
if SMC_IDS[sensor_id].get('CPU Temp', False):
|
||||||
|
section = 'CPUTemps'
|
||||||
|
adapter = 'SMC (CPU)'
|
||||||
|
source = SMC_IDS[sensor_id]['Source']
|
||||||
|
sensor_data[section][adapter][source] = {
|
||||||
|
'Current': value,
|
||||||
|
'Label': sensor_id,
|
||||||
|
'Max': value,
|
||||||
|
'Temps': [value],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return sensor_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_temp_str(temp, colored=True):
|
||||||
|
"""Get colored string based on temp, returns str."""
|
||||||
|
temp_color = None
|
||||||
|
|
||||||
|
# Safety check
|
||||||
|
try:
|
||||||
|
temp = float(temp)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
# Invalid temp?
|
||||||
|
return color_string(temp, 'PURPLE')
|
||||||
|
|
||||||
|
# Determine color
|
||||||
|
if colored:
|
||||||
|
for threshold, color in sorted(TEMP_COLORS.items(), reverse=True):
|
||||||
|
if temp >= threshold:
|
||||||
|
temp_color = color
|
||||||
|
break
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return color_string(f'{"-" if temp < 0 else ""}{temp:2.0f}°C', temp_color)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
196
scripts/wk/io.py
196
scripts/wk/io.py
|
|
@ -0,0 +1,196 @@
|
||||||
|
"""WizardKit: I/O Functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def case_insensitive_path(path):
|
||||||
|
"""Find path case-insensitively, returns pathlib.Path obj."""
|
||||||
|
given_path = pathlib.Path(path).resolve()
|
||||||
|
real_path = None
|
||||||
|
|
||||||
|
# Quick check
|
||||||
|
if given_path.exists():
|
||||||
|
return given_path
|
||||||
|
|
||||||
|
# Search for real path
|
||||||
|
parts = list(given_path.parts)
|
||||||
|
real_path = parts.pop(0)
|
||||||
|
for part in parts:
|
||||||
|
try:
|
||||||
|
real_path = case_insensitive_search(real_path, part)
|
||||||
|
except NotADirectoryError:
|
||||||
|
# Reclassify error
|
||||||
|
raise FileNotFoundError(given_path)
|
||||||
|
real_path = pathlib.Path(real_path)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return real_path
|
||||||
|
|
||||||
|
|
||||||
|
def case_insensitive_search(path, item):
|
||||||
|
"""Search path for item case insensitively, returns pathlib.Path obj."""
|
||||||
|
path = pathlib.Path(path).resolve()
|
||||||
|
given_path = path.joinpath(item)
|
||||||
|
real_path = None
|
||||||
|
regex = fr'^{item}'
|
||||||
|
|
||||||
|
# Quick check
|
||||||
|
if given_path.exists():
|
||||||
|
return given_path
|
||||||
|
|
||||||
|
# Check all items in path
|
||||||
|
for entry in os.scandir(path):
|
||||||
|
if re.match(regex, entry.name, re.IGNORECASE):
|
||||||
|
real_path = path.joinpath(entry.name)
|
||||||
|
|
||||||
|
# Raise exception if necessary
|
||||||
|
if not real_path:
|
||||||
|
raise FileNotFoundError(given_path)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return real_path
|
||||||
|
|
||||||
|
|
||||||
|
def delete_empty_folders(path):
|
||||||
|
"""Recursively delete all empty folders in path."""
|
||||||
|
LOG.debug('path: %s', path)
|
||||||
|
|
||||||
|
# Delete empty subfolders first
|
||||||
|
for item in os.scandir(path):
|
||||||
|
if item.is_dir():
|
||||||
|
delete_empty_folders(item.path)
|
||||||
|
|
||||||
|
# Attempt to remove (top) path
|
||||||
|
try:
|
||||||
|
delete_folder(path, force=False)
|
||||||
|
except OSError:
|
||||||
|
# Assuming it's not empty
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def delete_folder(path, force=False, ignore_errors=False):
|
||||||
|
"""Delete folder if empty or if forced.
|
||||||
|
|
||||||
|
NOTE: Exceptions are not caught by this function,
|
||||||
|
ignore_errors is passed to shutil.rmtree to allow partial deletions.
|
||||||
|
"""
|
||||||
|
LOG.debug(
|
||||||
|
'path: %s, force: %s, ignore_errors: %s',
|
||||||
|
path, force, ignore_errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
if force:
|
||||||
|
shutil.rmtree(path, ignore_errors=ignore_errors)
|
||||||
|
else:
|
||||||
|
os.rmdir(path)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_item(path, force=False, ignore_errors=False):
|
||||||
|
"""Delete file or folder, optionally recursively.
|
||||||
|
|
||||||
|
NOTE: Exceptions are not caught by this function,
|
||||||
|
ignore_errors is passed to delete_folder to allow partial deletions.
|
||||||
|
"""
|
||||||
|
LOG.debug(
|
||||||
|
'path: %s, force: %s, ignore_errors: %s',
|
||||||
|
path, force, ignore_errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
path = pathlib.Path(path)
|
||||||
|
if path.is_dir():
|
||||||
|
delete_folder(path, force=force, ignore_errors=ignore_errors)
|
||||||
|
else:
|
||||||
|
os.remove(path)
|
||||||
|
|
||||||
|
|
||||||
|
def non_clobber_path(path):
|
||||||
|
"""Update path as needed to non-existing path, returns pathlib.Path."""
|
||||||
|
LOG.debug('path: %s', path)
|
||||||
|
path = pathlib.Path(path)
|
||||||
|
name = path.name
|
||||||
|
new_path = None
|
||||||
|
suffix = ''.join(path.suffixes)
|
||||||
|
name = name.replace(suffix, '')
|
||||||
|
|
||||||
|
# Bail early
|
||||||
|
if not path.exists():
|
||||||
|
return path
|
||||||
|
|
||||||
|
# Find non-existant path
|
||||||
|
for _i in range(1000):
|
||||||
|
test_path = path.with_name(f'{name}_{_i}').with_suffix(suffix)
|
||||||
|
if not test_path.exists():
|
||||||
|
new_path = test_path
|
||||||
|
break
|
||||||
|
|
||||||
|
# Raise error if viable path not found
|
||||||
|
if not new_path:
|
||||||
|
raise FileExistsError(new_path)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
LOG.debug('new path: %s', new_path)
|
||||||
|
return new_path
|
||||||
|
|
||||||
|
|
||||||
|
def recursive_copy(source, dest, overwrite=False):
|
||||||
|
"""Copy source to dest recursively.
|
||||||
|
|
||||||
|
NOTE: This uses rsync style source/dest syntax.
|
||||||
|
If the source has a trailing slash then it's contents are copied,
|
||||||
|
otherwise the source itself is copied.
|
||||||
|
|
||||||
|
Examples assuming "ExDir/ExFile.txt" exists:
|
||||||
|
recursive_copy("ExDir", "Dest/") results in "Dest/ExDir/ExFile.txt"
|
||||||
|
recursive_copy("ExDir/", "Dest/") results in "Dest/ExFile.txt"
|
||||||
|
|
||||||
|
NOTE 2: dest does not use find_path because it might not exist.
|
||||||
|
"""
|
||||||
|
copy_contents = str(source).endswith(('/', '\\'))
|
||||||
|
source = case_insensitive_path(source)
|
||||||
|
dest = pathlib.Path(dest).resolve().joinpath(source.name)
|
||||||
|
os.makedirs(dest.parent, exist_ok=True)
|
||||||
|
|
||||||
|
# Recursively copy source to dest
|
||||||
|
if source.is_dir():
|
||||||
|
if copy_contents:
|
||||||
|
# Trailing slash syntax
|
||||||
|
for item in os.scandir(source):
|
||||||
|
recursive_copy(item.path, dest.parent, overwrite=overwrite)
|
||||||
|
elif not dest.exists():
|
||||||
|
# No conflict, copying whole tree (no merging needed)
|
||||||
|
shutil.copytree(source, dest)
|
||||||
|
elif not dest.is_dir():
|
||||||
|
# Refusing to replace file with dir
|
||||||
|
raise FileExistsError(f'Refusing to replace file: {dest}')
|
||||||
|
else:
|
||||||
|
# Dest exists and is a dir, merge dirs
|
||||||
|
for item in os.scandir(source):
|
||||||
|
recursive_copy(item.path, dest, overwrite=overwrite)
|
||||||
|
elif source.is_file():
|
||||||
|
if not dest.exists():
|
||||||
|
# No conflict, copying file
|
||||||
|
shutil.copy2(source, dest)
|
||||||
|
elif not dest.is_file():
|
||||||
|
# Refusing to replace dir with file
|
||||||
|
raise FileExistsError(f'Refusing to replace dir: {dest}')
|
||||||
|
elif overwrite:
|
||||||
|
# Dest file exists, deleting and replacing file
|
||||||
|
os.remove(dest)
|
||||||
|
shutil.copy2(source, dest)
|
||||||
|
else:
|
||||||
|
# Refusing to delete file when overwrite=False
|
||||||
|
raise FileExistsError(f'Refusing to delete file: {dest}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
7
scripts/wk/kit/__init__.py
Normal file
7
scripts/wk/kit/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
"""WizardKit: kit module init"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import platform
|
||||||
|
|
||||||
|
if platform.system() == 'Linux':
|
||||||
|
from wk.kit import ufd
|
||||||
472
scripts/wk/kit/ufd.py
Normal file
472
scripts/wk/kit/ufd.py
Normal file
|
|
@ -0,0 +1,472 @@
|
||||||
|
"""WizardKit: UFD Functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
# TODO: Replace some lsblk usage with hw_obj?
|
||||||
|
# TODO: Reduce imports if possible
|
||||||
|
# TODO: Needs testing
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
from docopt import docopt
|
||||||
|
|
||||||
|
from wk import io, log, std
|
||||||
|
from wk.cfg.main import KIT_NAME_FULL, KIT_NAME_SHORT
|
||||||
|
from wk.cfg.ufd import BOOT_ENTRIES, BOOT_FILES, ITEMS, ITEMS_HIDDEN, SOURCES
|
||||||
|
from wk.exe import run_program
|
||||||
|
from wk.os import linux
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
DOCSTRING = '''WizardKit: Build UFD
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
build-ufd [options] --ufd-device PATH --linux PATH
|
||||||
|
[--linux-minimal PATH]
|
||||||
|
[--main-kit PATH]
|
||||||
|
[--winpe PATH]
|
||||||
|
[--eset PATH]
|
||||||
|
[--hdclone PATH]
|
||||||
|
[--extra-dir PATH]
|
||||||
|
build-ufd (-h | --help)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-c PATH, --hdclone PATH
|
||||||
|
-e PATH, --extra-dir PATH
|
||||||
|
-k PATH, --main-kit PATH
|
||||||
|
-l PATH, --linux PATH
|
||||||
|
-m PATH, --linux-minimal PATH
|
||||||
|
-s PATH, --eset PATH
|
||||||
|
-u PATH, --ufd-device PATH
|
||||||
|
-w PATH, --winpe PATH
|
||||||
|
|
||||||
|
-h --help Show this page
|
||||||
|
-M --use-mbr Use real MBR instead of GPT w/ Protective MBR
|
||||||
|
-F --force Bypass all confirmation messages. USE WITH EXTREME CAUTION!
|
||||||
|
-U --update Don't format device, just update
|
||||||
|
'''
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
ISO_LABEL = f'{KIT_NAME_SHORT}_LINUX'
|
||||||
|
UFD_LABEL = f'{KIT_NAME_SHORT}_UFD'
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def build_ufd():
|
||||||
|
"""Build UFD using selected sources."""
|
||||||
|
args = docopt(DOCSTRING)
|
||||||
|
log.update_log_path(dest_name='build-ufd', timestamp=True)
|
||||||
|
try_print = std.TryAndPrint()
|
||||||
|
try_print.indent = 2
|
||||||
|
|
||||||
|
# Check if running with root permissions
|
||||||
|
if not linux.running_as_root():
|
||||||
|
std.print_error('This script is meant to be run as root')
|
||||||
|
std.abort()
|
||||||
|
|
||||||
|
# Show header
|
||||||
|
std.print_success(KIT_NAME_FULL)
|
||||||
|
std.print_warning('UFD Build Tool')
|
||||||
|
std.print_warning(' ')
|
||||||
|
|
||||||
|
# Verify selections
|
||||||
|
ufd_dev = verify_ufd(args['--ufd-device'])
|
||||||
|
sources = verify_sources(args, SOURCES)
|
||||||
|
show_selections(args, sources, ufd_dev, SOURCES)
|
||||||
|
if not args['--force']:
|
||||||
|
confirm_selections(update=args['--update'])
|
||||||
|
|
||||||
|
# Prep UFD
|
||||||
|
if not args['--update']:
|
||||||
|
std.print_info('Prep UFD')
|
||||||
|
prep_device(ufd_dev, UFD_LABEL, use_mbr=args['--use-mbr'])
|
||||||
|
|
||||||
|
# Mount UFD
|
||||||
|
try_print.run(
|
||||||
|
message='Mounting UFD...',
|
||||||
|
function=linux.mount,
|
||||||
|
mount_source=find_first_partition(ufd_dev),
|
||||||
|
mount_point='/mnt/UFD',
|
||||||
|
read_write=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove Arch folder
|
||||||
|
if args['--update']:
|
||||||
|
try_print.run(
|
||||||
|
message='Removing Linux...',
|
||||||
|
function=remove_arch,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy sources
|
||||||
|
std.print_standard(' ')
|
||||||
|
std.print_info('Copy Sources')
|
||||||
|
for s_label, s_path in sources.items():
|
||||||
|
try_print.run(
|
||||||
|
message='Copying {}...'.format(s_label),
|
||||||
|
function=copy_source,
|
||||||
|
source=s_path,
|
||||||
|
items=ITEMS[s_label],
|
||||||
|
overwrite=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update boot entries
|
||||||
|
std.print_standard(' ')
|
||||||
|
std.print_info('Boot Setup')
|
||||||
|
try_print.run(
|
||||||
|
message='Updating boot entries...',
|
||||||
|
function=update_boot_entries,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Install syslinux (to partition)
|
||||||
|
try_print.run(
|
||||||
|
message='Syslinux (partition)...',
|
||||||
|
function=install_syslinux_to_partition,
|
||||||
|
partition=find_first_partition(ufd_dev),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unmount UFD
|
||||||
|
try_print.run(
|
||||||
|
message='Unmounting UFD...',
|
||||||
|
function=linux.unmount,
|
||||||
|
mount_point='/mnt/UFD',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Install syslinux (to device)
|
||||||
|
try_print.run(
|
||||||
|
message='Syslinux (device)...',
|
||||||
|
function=install_syslinux_to_dev,
|
||||||
|
ufd_dev=ufd_dev,
|
||||||
|
use_mbr=args['--use-mbr'],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Hide items
|
||||||
|
std.print_standard(' ')
|
||||||
|
std.print_info('Final Touches')
|
||||||
|
try_print.run(
|
||||||
|
message='Hiding items...',
|
||||||
|
function=hide_items,
|
||||||
|
ufd_dev=ufd_dev,
|
||||||
|
items=ITEMS_HIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
std.print_standard('\nDone.')
|
||||||
|
if not args['--force']:
|
||||||
|
std.pause('Press Enter to exit...')
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_selections(update=False):
|
||||||
|
"""Ask tech to confirm selections, twice if necessary."""
|
||||||
|
if not std.ask('Is the above information correct?'):
|
||||||
|
std.abort()
|
||||||
|
|
||||||
|
# Safety check
|
||||||
|
if not update:
|
||||||
|
std.print_standard(' ')
|
||||||
|
std.print_warning('SAFETY CHECK')
|
||||||
|
std.print_standard(
|
||||||
|
'All data will be DELETED from the disk and partition(s) listed above.')
|
||||||
|
std.print_colored(
|
||||||
|
['This is irreversible and will lead to', 'DATA LOSS'],
|
||||||
|
[None, 'RED'],
|
||||||
|
)
|
||||||
|
if not std.ask('Asking again to confirm, is this correct?'):
|
||||||
|
std.abort()
|
||||||
|
|
||||||
|
std.print_standard(' ')
|
||||||
|
|
||||||
|
|
||||||
|
def copy_source(source, items, overwrite=False):
|
||||||
|
"""Copy source items to /mnt/UFD."""
|
||||||
|
is_image = source.is_file()
|
||||||
|
|
||||||
|
# Mount source if necessary
|
||||||
|
if is_image:
|
||||||
|
linux.mount(source, '/mnt/Source')
|
||||||
|
|
||||||
|
# Copy items
|
||||||
|
for i_source, i_dest in items:
|
||||||
|
i_source = f'{"/mnt/Source" if is_image else source}{i_source}'
|
||||||
|
i_dest = f'/mnt/UFD{i_dest}'
|
||||||
|
try:
|
||||||
|
io.recursive_copy(i_source, i_dest, overwrite=overwrite)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# Going to assume (hope) that this is fine
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Unmount source if necessary
|
||||||
|
if is_image:
|
||||||
|
linux.unmount('/mnt/Source')
|
||||||
|
|
||||||
|
|
||||||
|
def find_first_partition(dev_path):
|
||||||
|
"""Find path to first partition of dev, returns str.
|
||||||
|
|
||||||
|
NOTE: This assumes the dev was just partitioned with
|
||||||
|
a single partition.
|
||||||
|
"""
|
||||||
|
cmd = [
|
||||||
|
'lsblk',
|
||||||
|
'--list',
|
||||||
|
'--noheadings',
|
||||||
|
'--output', 'name',
|
||||||
|
'--paths',
|
||||||
|
dev_path,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Run cmd
|
||||||
|
proc = run_program(cmd)
|
||||||
|
part_path = proc.stdout.splitlines()[-1].strip()
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return part_path
|
||||||
|
|
||||||
|
|
||||||
|
def hide_items(ufd_dev, items):
|
||||||
|
"""Set FAT32 hidden flag for items."""
|
||||||
|
first_partition = find_first_partition(ufd_dev)
|
||||||
|
with open('/root/.mtoolsrc', 'w') as _f:
|
||||||
|
_f.write(f'drive U: file="{first_partition}"\n')
|
||||||
|
_f.write('mtools_skip_check=1\n')
|
||||||
|
|
||||||
|
# Hide items
|
||||||
|
for item in items:
|
||||||
|
cmd = [f'yes | mattrib +h "U:/{item}"']
|
||||||
|
run_program(cmd, check=False, shell=True)
|
||||||
|
|
||||||
|
|
||||||
|
def install_syslinux_to_dev(ufd_dev, use_mbr):
|
||||||
|
"""Install Syslinux to UFD (dev)."""
|
||||||
|
cmd = [
|
||||||
|
'dd',
|
||||||
|
'bs=440',
|
||||||
|
'count=1',
|
||||||
|
f'if=/usr/lib/syslinux/bios/{"mbr" if use_mbr else "gptmbr"}.bin',
|
||||||
|
f'of={ufd_dev}',
|
||||||
|
]
|
||||||
|
run_program(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def install_syslinux_to_partition(partition):
|
||||||
|
"""Install Syslinux to UFD (partition)."""
|
||||||
|
cmd = [
|
||||||
|
'syslinux',
|
||||||
|
'--install',
|
||||||
|
'--directory',
|
||||||
|
'/arch/boot/syslinux/',
|
||||||
|
partition,
|
||||||
|
]
|
||||||
|
run_program(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_path(path_obj, path_type):
|
||||||
|
"""Verify path_obj is valid by type, returns bool."""
|
||||||
|
valid_path = False
|
||||||
|
if path_type == 'DIR':
|
||||||
|
valid_path = path_obj.is_dir()
|
||||||
|
elif path_type == 'KIT':
|
||||||
|
valid_path = path_obj.is_dir() and path_obj.joinpath('.bin').exists()
|
||||||
|
elif path_type == 'IMG':
|
||||||
|
valid_path = path_obj.is_file() and path_obj.suffix.lower() == '.img'
|
||||||
|
elif path_type == 'ISO':
|
||||||
|
valid_path = path_obj.is_file() and path_obj.suffix.lower() == '.iso'
|
||||||
|
elif path_type == 'UFD':
|
||||||
|
valid_path = path_obj.is_block_device()
|
||||||
|
|
||||||
|
return valid_path
|
||||||
|
|
||||||
|
|
||||||
|
def prep_device(dev_path, label, use_mbr=False):
|
||||||
|
"""Format device in preparation for applying the WizardKit components
|
||||||
|
|
||||||
|
This is done is four steps:
|
||||||
|
1. Zero-out first 64MB (this deletes the partition table and/or bootloader)
|
||||||
|
2. Create a new partition table (GPT by default, optionally MBR)
|
||||||
|
3. Set boot flag
|
||||||
|
4. Format partition (FAT32, 4K aligned)
|
||||||
|
"""
|
||||||
|
try_print = std.TryAndPrint()
|
||||||
|
try_print.indent = 2
|
||||||
|
|
||||||
|
# Zero-out first 64MB
|
||||||
|
cmd = [
|
||||||
|
'dd',
|
||||||
|
'bs=4M',
|
||||||
|
'count=16',
|
||||||
|
'if=/dev/zero',
|
||||||
|
f'of={dev_path}',
|
||||||
|
]
|
||||||
|
try_print.run(
|
||||||
|
message='Zeroing first 64MiB...',
|
||||||
|
function=run_program,
|
||||||
|
cmd=cmd,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create partition table
|
||||||
|
cmd = [
|
||||||
|
'parted', dev_path,
|
||||||
|
'--script',
|
||||||
|
'--',
|
||||||
|
'mklabel', 'msdos' if use_mbr else 'gpt',
|
||||||
|
'-1s' if use_mbr else '-4MiB',
|
||||||
|
]
|
||||||
|
try_print.run(
|
||||||
|
message='Creating partition table...',
|
||||||
|
function=run_program,
|
||||||
|
cmd=cmd,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set boot flag
|
||||||
|
cmd = [
|
||||||
|
'parted', dev_path,
|
||||||
|
'set', '1',
|
||||||
|
'boot' if use_mbr else 'legacy_boot',
|
||||||
|
'on',
|
||||||
|
]
|
||||||
|
try_print.run(
|
||||||
|
message='Setting boot flag...',
|
||||||
|
function=run_program,
|
||||||
|
cmd=cmd,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format partition
|
||||||
|
cmd = [
|
||||||
|
'mkfs.vfat',
|
||||||
|
'-F', '32',
|
||||||
|
'-n', label,
|
||||||
|
find_first_partition(dev_path),
|
||||||
|
]
|
||||||
|
try_print.run(
|
||||||
|
message='Formatting partition...',
|
||||||
|
function=run_program,
|
||||||
|
cmd=cmd,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_arch():
|
||||||
|
"""Remove arch dir from UFD.
|
||||||
|
|
||||||
|
This ensures a clean installation to the UFD and resets the boot files
|
||||||
|
"""
|
||||||
|
shutil.rmtree(io.case_insensitive_path('/mnt/UFD/arch'))
|
||||||
|
|
||||||
|
|
||||||
|
def show_selections(args, sources, ufd_dev, ufd_sources):
|
||||||
|
"""Show selections including non-specified options."""
|
||||||
|
|
||||||
|
# Sources
|
||||||
|
std.print_info('Sources')
|
||||||
|
for label in ufd_sources.keys():
|
||||||
|
if label in sources:
|
||||||
|
std.print_standard(f' {label+":":<18} {sources["label"]}')
|
||||||
|
else:
|
||||||
|
std.print_colored(
|
||||||
|
[f' {label+":":<18}', 'Not Specified'],
|
||||||
|
[None, 'YELLOW'],
|
||||||
|
)
|
||||||
|
std.print_standard(' ')
|
||||||
|
|
||||||
|
# Destination
|
||||||
|
std.print_info('Destination')
|
||||||
|
cmd = [
|
||||||
|
'lsblk', '--nodeps', '--noheadings', '--paths',
|
||||||
|
'--output', 'NAME,FSTYPE,TRAN,SIZE,VENDOR,MODEL,SERIAL',
|
||||||
|
ufd_dev,
|
||||||
|
]
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
std.print_standard(proc.stdout.strip())
|
||||||
|
cmd = [
|
||||||
|
'lsblk', '--noheadings', '--paths',
|
||||||
|
'--output', 'NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT',
|
||||||
|
ufd_dev,
|
||||||
|
]
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
for line in proc.stdout.splitlines()[1:]:
|
||||||
|
std.print_standard(line)
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
if args['--update']:
|
||||||
|
std.print_warning('Updating kit in-place')
|
||||||
|
elif args['--use-mbr']:
|
||||||
|
std.print_warning('Formatting using legacy MBR')
|
||||||
|
std.print_standard(' ')
|
||||||
|
|
||||||
|
|
||||||
|
def update_boot_entries():
|
||||||
|
"""Update boot files for UFD usage"""
|
||||||
|
configs = []
|
||||||
|
|
||||||
|
# Find config files
|
||||||
|
for c_path, c_ext in BOOT_FILES.items():
|
||||||
|
c_path = io.case_insensitive_path('/mnt/UFD{c_path}')
|
||||||
|
for item in os.scandir(c_path):
|
||||||
|
if item.name.lower().endswith(c_ext.lower()):
|
||||||
|
configs.append(item.path)
|
||||||
|
|
||||||
|
# Update Linux labels
|
||||||
|
cmd = [
|
||||||
|
'sed',
|
||||||
|
'--in-place',
|
||||||
|
'--regexp-extended',
|
||||||
|
f's/(eSysRescueLiveCD|{ISO_LABEL})/{UFD_LABEL}/',
|
||||||
|
*configs,
|
||||||
|
]
|
||||||
|
run_program(cmd)
|
||||||
|
|
||||||
|
# Uncomment extra entries if present
|
||||||
|
for b_path, b_comment in BOOT_ENTRIES.items():
|
||||||
|
try:
|
||||||
|
io.case_insensitive_path(f'/mnt/UFD{b_path}')
|
||||||
|
except (FileNotFoundError, NotADirectoryError):
|
||||||
|
# Entry not found, continue to next entry
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Entry found, update config files
|
||||||
|
cmd = [
|
||||||
|
'sed',
|
||||||
|
'--in-place',
|
||||||
|
f's/#{b_comment}#//',
|
||||||
|
*configs,
|
||||||
|
]
|
||||||
|
run_program(cmd, check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_sources(args, ufd_sources):
|
||||||
|
"""Check all sources and abort if necessary, returns dict."""
|
||||||
|
sources = OrderedDict()
|
||||||
|
|
||||||
|
for label, data in ufd_sources.items():
|
||||||
|
s_path = args[data['Arg']]
|
||||||
|
if s_path:
|
||||||
|
try:
|
||||||
|
s_path_obj = io.case_insensitive_path(s_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
std.print_error(f'ERROR: {label} not found: {s_path}')
|
||||||
|
std.abort()
|
||||||
|
if not is_valid_path(s_path_obj, data['Type']):
|
||||||
|
std.print_error(f'ERROR: Invalid {label} source: {s_path}')
|
||||||
|
std.abort()
|
||||||
|
sources[label] = s_path_obj
|
||||||
|
|
||||||
|
return sources
|
||||||
|
|
||||||
|
|
||||||
|
def verify_ufd(dev_path):
|
||||||
|
"""Check that dev_path is a valid UFD, returns pathlib.Path obj."""
|
||||||
|
ufd_dev = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
ufd_dev = io.case_insensitive_path(dev_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
std.print_error(f'ERROR: UFD device not found: {dev_path}')
|
||||||
|
std.abort()
|
||||||
|
|
||||||
|
if not is_valid_path(ufd_dev, 'UFD'):
|
||||||
|
std.print_error(f'ERROR: Invalid UFD device: {ufd_dev}')
|
||||||
|
std.abort()
|
||||||
|
|
||||||
|
return ufd_dev
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
154
scripts/wk/log.py
Normal file
154
scripts/wk/log.py
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
"""WizardKit: Log Functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
|
||||||
|
from wk import cfg
|
||||||
|
from wk.io import non_clobber_path
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
if os.name == 'nt':
|
||||||
|
# Example: "C:\WK\1955-11-05\WizardKit"
|
||||||
|
DEFAULT_LOG_DIR = (
|
||||||
|
f'{os.environ.get("SYSTEMDRIVE", "C:")}/'
|
||||||
|
f'{cfg.main.KIT_NAME_SHORT}/'
|
||||||
|
f'{time.strftime("%Y-%m-%d")}'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Example: "/home/tech/Logs"
|
||||||
|
DEFAULT_LOG_DIR = f'{os.path.expanduser("~")}/Logs'
|
||||||
|
DEFAULT_LOG_NAME = cfg.main.KIT_NAME_FULL
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def enable_debug_mode():
|
||||||
|
"""Configures logging for better debugging."""
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
for handler in root_logger.handlers:
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
datefmt=cfg.log.DEBUG['datefmt'],
|
||||||
|
fmt=cfg.log.DEBUG['format'],
|
||||||
|
)
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
root_logger.setLevel('DEBUG')
|
||||||
|
|
||||||
|
|
||||||
|
def format_log_path(
|
||||||
|
log_dir=None, log_name=None, timestamp=False,
|
||||||
|
kit=False, tool=False):
|
||||||
|
"""Format path based on args passed, returns pathlib.Path obj."""
|
||||||
|
log_path = pathlib.Path(
|
||||||
|
f'{log_dir if log_dir else DEFAULT_LOG_DIR}/'
|
||||||
|
f'{cfg.main.KIT_NAME_FULL+"/" if kit else ""}'
|
||||||
|
f'{"Tools/" if tool else ""}'
|
||||||
|
f'{log_name if log_name else DEFAULT_LOG_NAME}'
|
||||||
|
f'{"_" if timestamp else ""}'
|
||||||
|
f'{time.strftime("%Y-%m-%d_%H%M%S%z") if timestamp else ""}'
|
||||||
|
'.log'
|
||||||
|
)
|
||||||
|
log_path = log_path.resolve()
|
||||||
|
|
||||||
|
# Avoid clobbering
|
||||||
|
log_path = non_clobber_path(log_path)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return log_path
|
||||||
|
|
||||||
|
|
||||||
|
def get_root_logger_path():
|
||||||
|
"""Get path to log file from root logger, returns pathlib.Path obj."""
|
||||||
|
log_path = None
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
|
||||||
|
# Check all handlers and use the first fileHandler found
|
||||||
|
for handler in root_logger.handlers:
|
||||||
|
if isinstance(handler, logging.FileHandler):
|
||||||
|
log_path = pathlib.Path(handler.baseFilename).resolve()
|
||||||
|
break
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return log_path
|
||||||
|
|
||||||
|
|
||||||
|
def remove_empty_log():
|
||||||
|
"""Remove log if empty."""
|
||||||
|
is_empty = False
|
||||||
|
|
||||||
|
# Check if log is empty
|
||||||
|
log_path = get_root_logger_path()
|
||||||
|
try:
|
||||||
|
is_empty = log_path and log_path.exists() and log_path.stat().st_size == 0
|
||||||
|
except (FileNotFoundError, AttributeError):
|
||||||
|
# File doesn't exist or couldn't verify it's empty
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Delete log
|
||||||
|
if is_empty:
|
||||||
|
log_path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def start(config=None):
|
||||||
|
"""Configure and start logging using safe defaults."""
|
||||||
|
log_path = format_log_path(timestamp=os.name != 'nt')
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
|
||||||
|
# Safety checks
|
||||||
|
if not config:
|
||||||
|
config = cfg.log.DEFAULT
|
||||||
|
if root_logger.hasHandlers():
|
||||||
|
raise UserWarning('Logging already started, results may be unpredictable.')
|
||||||
|
|
||||||
|
# Create log_dir
|
||||||
|
os.makedirs(log_path.parent, exist_ok=True)
|
||||||
|
|
||||||
|
# Config logger
|
||||||
|
logging.basicConfig(filename=log_path, **config)
|
||||||
|
|
||||||
|
# Register shutdown to run atexit
|
||||||
|
atexit.register(remove_empty_log)
|
||||||
|
atexit.register(logging.shutdown)
|
||||||
|
|
||||||
|
|
||||||
|
def update_log_path(
|
||||||
|
dest_dir=None, dest_name=None, keep_history=True, timestamp=True):
|
||||||
|
"""Moves current log file to new path and updates the root logger."""
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
cur_handler = None
|
||||||
|
cur_path = get_root_logger_path()
|
||||||
|
new_path = format_log_path(dest_dir, dest_name, timestamp=timestamp)
|
||||||
|
os.makedirs(new_path.parent, exist_ok=True)
|
||||||
|
|
||||||
|
# Get current logging file handler
|
||||||
|
for handler in root_logger.handlers:
|
||||||
|
if isinstance(handler, logging.FileHandler):
|
||||||
|
cur_handler = handler
|
||||||
|
break
|
||||||
|
if not cur_handler:
|
||||||
|
raise RuntimeError('Logging FileHandler not found')
|
||||||
|
|
||||||
|
# Copy original log to new location
|
||||||
|
if keep_history:
|
||||||
|
if new_path.exists():
|
||||||
|
raise FileExistsError(f'Refusing to clobber: {new_path}')
|
||||||
|
shutil.move(cur_path, new_path)
|
||||||
|
|
||||||
|
# Remove old log if empty
|
||||||
|
remove_empty_log()
|
||||||
|
|
||||||
|
# Create new cur_handler (preserving formatter settings)
|
||||||
|
new_handler = logging.FileHandler(new_path, mode='a')
|
||||||
|
new_handler.setFormatter(cur_handler.formatter)
|
||||||
|
|
||||||
|
# Replace current handler
|
||||||
|
root_logger.removeHandler(cur_handler)
|
||||||
|
root_logger.addHandler(new_handler)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
"""WizardKit: Net Functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
from wk.exe import get_json_from_command, run_program
|
||||||
|
from wk.std import PLATFORM, GenericError, show_data
|
||||||
|
|
||||||
|
from wk.cfg.net import BACKUP_SERVERS
|
||||||
|
|
||||||
|
|
||||||
|
# REGEX
|
||||||
|
REGEX_VALID_IP = re.compile(
|
||||||
|
r'(10.\d+.\d+.\d+'
|
||||||
|
r'|172.(1[6-9]|2\d|3[0-1])'
|
||||||
|
r'|192.168.\d+.\d+)',
|
||||||
|
re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def connected_to_private_network(raise_on_error=False):
|
||||||
|
"""Check if connected to a private network, returns bool.
|
||||||
|
|
||||||
|
This checks for a valid private IP assigned to this system.
|
||||||
|
|
||||||
|
NOTE: If one isn't found and raise_on_error=True then an exception is raised.
|
||||||
|
NOTE 2: If one is found and raise_on_error=True then None is returned.
|
||||||
|
"""
|
||||||
|
connected = False
|
||||||
|
|
||||||
|
# Check IPs
|
||||||
|
devs = psutil.net_if_addrs()
|
||||||
|
for dev in devs.values():
|
||||||
|
for family in dev:
|
||||||
|
if REGEX_VALID_IP.search(family.address):
|
||||||
|
# Valid IP found
|
||||||
|
connected = True
|
||||||
|
break
|
||||||
|
if connected:
|
||||||
|
break
|
||||||
|
|
||||||
|
# No valid IP found
|
||||||
|
if not connected and raise_on_error:
|
||||||
|
raise GenericError('Not connected to a network')
|
||||||
|
|
||||||
|
# Done
|
||||||
|
if raise_on_error:
|
||||||
|
connected = None
|
||||||
|
return connected
|
||||||
|
|
||||||
|
|
||||||
|
def mount_backup_shares(read_write=False):
|
||||||
|
"""Mount backup shares using OS specific methods."""
|
||||||
|
report = []
|
||||||
|
for name, details in BACKUP_SERVERS.items():
|
||||||
|
mount_point = None
|
||||||
|
mount_str = f'{name} (//{details["Address"]}/{details["Share"]})'
|
||||||
|
|
||||||
|
# Prep mount point
|
||||||
|
if PLATFORM in ('Darwin', 'Linux'):
|
||||||
|
mount_point = pathlib.Path(f'/Backups/{name}')
|
||||||
|
try:
|
||||||
|
if not mount_point.exists():
|
||||||
|
# Script should be run as user so sudo is required
|
||||||
|
run_program(['sudo', 'mkdir', '-p', mount_point])
|
||||||
|
except OSError:
|
||||||
|
# Assuming permission denied under macOS
|
||||||
|
pass
|
||||||
|
if mount_point:
|
||||||
|
mount_str += f' to {mount_point}'
|
||||||
|
|
||||||
|
# Check if already mounted
|
||||||
|
if share_is_mounted(details):
|
||||||
|
report.append(f'(Already) Mounted {mount_str}')
|
||||||
|
# Skip to next share
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Mount share
|
||||||
|
proc = mount_network_share(details, mount_point, read_write=read_write)
|
||||||
|
if proc.returncode:
|
||||||
|
report.append(f'Failed to Mount {mount_str}')
|
||||||
|
else:
|
||||||
|
report.append(f'Mounted {mount_str}')
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def mount_network_share(details, mount_point=None, read_write=False):
|
||||||
|
"""Mount network share using OS specific methods."""
|
||||||
|
cmd = None
|
||||||
|
address = details['Address']
|
||||||
|
share = details['Share']
|
||||||
|
username = details['RO-User']
|
||||||
|
password = details['RO-Pass']
|
||||||
|
if read_write:
|
||||||
|
username = details['RW-User']
|
||||||
|
password = details['RW-Pass']
|
||||||
|
|
||||||
|
# Network check
|
||||||
|
if not connected_to_private_network():
|
||||||
|
raise RuntimeError('Not connected to a network')
|
||||||
|
|
||||||
|
# Build OS-specific command
|
||||||
|
if PLATFORM == 'Darwin':
|
||||||
|
cmd = [
|
||||||
|
'sudo',
|
||||||
|
'mount',
|
||||||
|
'-t', 'smbfs',
|
||||||
|
'-o', f'{"rw" if read_write else "ro"}',
|
||||||
|
f'//{username}:{password}@{address}/{share}',
|
||||||
|
mount_point,
|
||||||
|
]
|
||||||
|
elif PLATFORM == 'Linux':
|
||||||
|
cmd = [
|
||||||
|
'sudo',
|
||||||
|
'mount',
|
||||||
|
'-t', 'cifs',
|
||||||
|
'-o', (
|
||||||
|
f'{"rw" if read_write else "ro"}'
|
||||||
|
f',uid={os.getuid()}'
|
||||||
|
f',gid={os.getgid()}'
|
||||||
|
f',username={username}'
|
||||||
|
f',{"password=" if password else "guest"}{password}'
|
||||||
|
),
|
||||||
|
f'//{address}/{share}',
|
||||||
|
mount_point
|
||||||
|
]
|
||||||
|
elif PLATFORM == 'Windows':
|
||||||
|
cmd = ['net', 'use']
|
||||||
|
if mount_point:
|
||||||
|
cmd.append(f'{mount_point}:')
|
||||||
|
cmd.append(f'/user:{username}')
|
||||||
|
cmd.append(fr'\\{address}\{share}')
|
||||||
|
cmd.append(password)
|
||||||
|
|
||||||
|
# Mount share
|
||||||
|
return run_program(cmd, check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def ping(addr='google.com'):
|
||||||
|
"""Attempt to ping addr."""
|
||||||
|
cmd = (
|
||||||
|
'ping',
|
||||||
|
'-n' if psutil.WINDOWS else '-c',
|
||||||
|
'2',
|
||||||
|
addr,
|
||||||
|
)
|
||||||
|
run_program(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def share_is_mounted(details):
|
||||||
|
"""Check if dev/share/etc is mounted, returns bool."""
|
||||||
|
mounted = False
|
||||||
|
|
||||||
|
if PLATFORM == 'Darwin':
|
||||||
|
# Weak and naive text search
|
||||||
|
proc = run_program(['mount'], check=False)
|
||||||
|
for line in proc.stdout.splitlines():
|
||||||
|
if f'{details["Address"]}/{details["Share"]}' in line:
|
||||||
|
mounted = True
|
||||||
|
break
|
||||||
|
elif PLATFORM == 'Linux':
|
||||||
|
cmd = [
|
||||||
|
'findmnt',
|
||||||
|
'--list',
|
||||||
|
'--json',
|
||||||
|
'--invert',
|
||||||
|
'--types', (
|
||||||
|
'autofs,binfmt_misc,bpf,cgroup,cgroup2,configfs,debugfs,devpts,'
|
||||||
|
'devtmpfs,hugetlbfs,mqueue,proc,pstore,securityfs,sysfs,tmpfs'
|
||||||
|
),
|
||||||
|
'--output', 'SOURCE',
|
||||||
|
]
|
||||||
|
mount_data = get_json_from_command(cmd)
|
||||||
|
for row in mount_data.get('filesystems', []):
|
||||||
|
if row['source'] == f'//{details["Address"]}/{details["Share"]}':
|
||||||
|
mounted = True
|
||||||
|
break
|
||||||
|
#TODO: Check mount status under Windows
|
||||||
|
#elif PLATFORM == 'Windows':
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return mounted
|
||||||
|
|
||||||
|
|
||||||
|
def show_valid_addresses():
|
||||||
|
"""Show all valid private IP addresses assigned to the system."""
|
||||||
|
devs = psutil.net_if_addrs()
|
||||||
|
for dev, families in sorted(devs.items()):
|
||||||
|
for family in families:
|
||||||
|
if REGEX_VALID_IP.search(family.address):
|
||||||
|
# Valid IP found
|
||||||
|
show_data(message=dev, data=family.address)
|
||||||
|
|
||||||
|
|
||||||
|
def speedtest():
|
||||||
|
"""Run a network speedtest using speedtest-cli."""
|
||||||
|
cmd = ['speedtest-cli', '--simple']
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
output = [line.strip() for line in proc.stdout.splitlines() if line.strip()]
|
||||||
|
output = [line.split() for line in output]
|
||||||
|
output = [(a, float(b), c) for a, b, c in output]
|
||||||
|
return [f'{a:<10}{b:6.2f} {c}' for a, b, c in output]
|
||||||
|
|
||||||
|
|
||||||
|
def unmount_backup_shares():
|
||||||
|
"""Unmount backup shares."""
|
||||||
|
report = []
|
||||||
|
for name, details in BACKUP_SERVERS.items():
|
||||||
|
kwargs = {}
|
||||||
|
source_str = f'{name} (//{details["Address"]}/{details["Share"]})'
|
||||||
|
|
||||||
|
# Check if mounted
|
||||||
|
if not share_is_mounted(details):
|
||||||
|
report.append(f'Not mounted {source_str}')
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Build OS specific kwargs
|
||||||
|
if PLATFORM in ('Darwin', 'Linux'):
|
||||||
|
kwargs['mount_point'] = f'/Backups/{name}'
|
||||||
|
elif PLATFORM == 'Windows':
|
||||||
|
kwargs['details'] = details
|
||||||
|
|
||||||
|
# Unmount and add to report
|
||||||
|
proc = unmount_network_share(**kwargs)
|
||||||
|
if proc.returncode:
|
||||||
|
report.append(f'Failed to unmount {source_str}')
|
||||||
|
else:
|
||||||
|
report.append(f'Unmounted {source_str}')
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def unmount_network_share(details=None, mount_point=None):
|
||||||
|
"""Unmount network share"""
|
||||||
|
cmd = []
|
||||||
|
|
||||||
|
# Build OS specific command
|
||||||
|
if PLATFORM in ('Darwin', 'Linux'):
|
||||||
|
cmd = ['sudo', 'umount', mount_point]
|
||||||
|
elif PLATFORM == 'Windows':
|
||||||
|
cmd = ['net', 'use']
|
||||||
|
if mount_point:
|
||||||
|
cmd.append(f'{mount_point}:')
|
||||||
|
elif details:
|
||||||
|
cmd.append(fr'\\{details["Address"]}\{details["Share"]}')
|
||||||
|
cmd.append('/delete')
|
||||||
|
|
||||||
|
# Unmount share
|
||||||
|
return run_program(cmd, check=False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
10
scripts/wk/os/__init__.py
Normal file
10
scripts/wk/os/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""WizardKit: os module init"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import platform
|
||||||
|
|
||||||
|
#if platform.system() == 'Darwin':
|
||||||
|
if platform.system() == 'Linux':
|
||||||
|
from wk.os import linux
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
from wk.os import win
|
||||||
240
scripts/wk/os/linux.py
Normal file
240
scripts/wk/os/linux.py
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
"""WizardKit: Linux Functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from wk import std
|
||||||
|
from wk.exe import popen_program, run_program
|
||||||
|
from wk.hw.obj import Disk
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
UUID_CORESTORAGE = '53746f72-6167-11aa-aa11-00306543ecac'
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def get_user_home(user):
|
||||||
|
"""Get path to user's home dir, returns pathlib.Path obj."""
|
||||||
|
home = None
|
||||||
|
|
||||||
|
# Get path from user details
|
||||||
|
cmd = ['getent', 'passwd', user]
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
try:
|
||||||
|
home = proc.stdout.split(':')[5]
|
||||||
|
except IndexError:
|
||||||
|
# Try using environment variable
|
||||||
|
home = os.environ.get('HOME')
|
||||||
|
|
||||||
|
# Raise exception if necessary
|
||||||
|
if not home:
|
||||||
|
raise RuntimeError(f'Failed to find home for: {user}')
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return pathlib.Path(home)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_name():
|
||||||
|
"""Get real user name, returns str."""
|
||||||
|
user = None
|
||||||
|
|
||||||
|
# Query environment
|
||||||
|
user = os.environ.get('SUDO_USER')
|
||||||
|
if not user:
|
||||||
|
user = os.environ.get('USER')
|
||||||
|
|
||||||
|
# Raise exception if necessary
|
||||||
|
if not user:
|
||||||
|
raise RuntimeError('Failed to determine user')
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def make_temp_file():
|
||||||
|
"""Make temporary file, returns pathlib.Path() obj."""
|
||||||
|
proc = run_program(['mktemp'], check=False)
|
||||||
|
return pathlib.Path(proc.stdout.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def mount(source, mount_point=None, read_write=False):
|
||||||
|
"""Mount source (on mount_point if provided).
|
||||||
|
|
||||||
|
NOTE: If not running_as_root() then udevil will be used.
|
||||||
|
"""
|
||||||
|
cmd = [
|
||||||
|
'mount',
|
||||||
|
'-o', 'rw' if read_write else 'ro',
|
||||||
|
source,
|
||||||
|
]
|
||||||
|
if not running_as_root():
|
||||||
|
cmd.insert(0, 'udevil')
|
||||||
|
if mount_point:
|
||||||
|
cmd.append(mount_point)
|
||||||
|
|
||||||
|
# Run mount command
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
if not proc.returncode == 0:
|
||||||
|
raise RuntimeError(f'Failed to mount: {source} on {mount_point}')
|
||||||
|
|
||||||
|
|
||||||
|
def mount_volumes(device_path=None, read_write=False, scan_corestorage=False):
|
||||||
|
"""Mount all detected volumes, returns list.
|
||||||
|
|
||||||
|
NOTE: If device_path is specified then only volumes
|
||||||
|
under that path will be mounted.
|
||||||
|
"""
|
||||||
|
report = []
|
||||||
|
volumes = []
|
||||||
|
containers = []
|
||||||
|
|
||||||
|
# Get list of volumes
|
||||||
|
cmd = [
|
||||||
|
'lsblk',
|
||||||
|
'--list',
|
||||||
|
'--noheadings',
|
||||||
|
'--output=name',
|
||||||
|
'--paths',
|
||||||
|
]
|
||||||
|
if device_path:
|
||||||
|
cmd.append(device_path)
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
for line in sorted(proc.stdout.splitlines()):
|
||||||
|
volumes.append(Disk(line.strip()))
|
||||||
|
|
||||||
|
# Get list of CoreStorage containers
|
||||||
|
containers = [
|
||||||
|
vol for vol in volumes if vol.details.get('parttype', '') == UUID_CORESTORAGE
|
||||||
|
]
|
||||||
|
|
||||||
|
# Scan CoreStorage containers
|
||||||
|
if scan_corestorage:
|
||||||
|
if containers:
|
||||||
|
std.print_warning(
|
||||||
|
f'Detected CoreStorage container{"s" if len(containers) > 1 else ""}',
|
||||||
|
)
|
||||||
|
std.print_standard('Scanning for inner volume(s)...')
|
||||||
|
for container in containers:
|
||||||
|
volumes.extend(scan_corestorage_container(container))
|
||||||
|
|
||||||
|
# Mount volumes
|
||||||
|
for vol in volumes:
|
||||||
|
already_mounted = vol.details.get('mountpoint', '')
|
||||||
|
result = f'{vol.details["name"].replace("/dev/mapper/", ""):<20}'
|
||||||
|
|
||||||
|
# Parent devices
|
||||||
|
if vol.details.get('children', False):
|
||||||
|
if vol.details.get('fstype', ''):
|
||||||
|
result += vol.details['fstype']
|
||||||
|
if vol.details.get('label', ''):
|
||||||
|
result += f' "{vol.details["label"]}"'
|
||||||
|
report.append(std.color_string(result, 'BLUE'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Attempt to mount volume
|
||||||
|
if not already_mounted:
|
||||||
|
mount(vol.path, read_write=read_write)
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
if proc.returncode:
|
||||||
|
result += 'Failed to mount'
|
||||||
|
report.append(std.color_string(result, 'RED'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add size to result
|
||||||
|
vol.get_details()
|
||||||
|
vol.details['fsused'] = vol.details.get('fsused', -1)
|
||||||
|
vol.details['fsavail'] = vol.details.get('fsavail', -1)
|
||||||
|
result += f'{"Mounted on "+vol.details.get("mountpoint", "?"):<40}'
|
||||||
|
result = (
|
||||||
|
f'{result} ({vol.details.get("fstype", "Unknown FS")+",":<5} '
|
||||||
|
f'{std.bytes_to_string(vol.details["fsused"], decimals=1):>9} used, '
|
||||||
|
f'{std.bytes_to_string(vol.details["fsavail"], decimals=1):>9} free)'
|
||||||
|
)
|
||||||
|
report.append(
|
||||||
|
std.color_string(
|
||||||
|
result,
|
||||||
|
'YELLOW' if already_mounted else None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def running_as_root():
|
||||||
|
"""Check if running with effective UID of 0, returns bool."""
|
||||||
|
return os.geteuid() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def scan_corestorage_container(container, timeout=300):
|
||||||
|
"""Scan CoreStorage container for inner volumes, returns list."""
|
||||||
|
# TODO: Test Scanning CoreStorage containers
|
||||||
|
detected_volumes = {}
|
||||||
|
inner_volumes = []
|
||||||
|
log_path = make_temp_file()
|
||||||
|
|
||||||
|
# Run scan via TestDisk
|
||||||
|
cmd = [
|
||||||
|
'sudo', 'testdisk',
|
||||||
|
'/logname', log_path,
|
||||||
|
'/debug',
|
||||||
|
'/log',
|
||||||
|
'/cmd', container.path, 'partition_none,analyze',
|
||||||
|
]
|
||||||
|
proc = popen_program(cmd)
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=timeout)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
# Failed to find any volumes, stop scan
|
||||||
|
run_program(['sudo', 'kill', proc.pid], check=False)
|
||||||
|
|
||||||
|
# Check results
|
||||||
|
if proc.returncode == 0 and log_path.exists():
|
||||||
|
results = log_path.read_text(encoding='utf-8', errors='ignore')
|
||||||
|
for line in results.splitlines():
|
||||||
|
line = line.lower().strip()
|
||||||
|
match = re.match(r'^.*echo "([^"]+)" . dmsetup create test(\d)$', line)
|
||||||
|
if match:
|
||||||
|
cs_name = f'CoreStorage_{container.path.name}_{match.group(2)}'
|
||||||
|
detected_volumes[cs_name] = match.group(1)
|
||||||
|
|
||||||
|
# Create mapper device(s) if necessary
|
||||||
|
for name, cmd in detected_volumes.items():
|
||||||
|
cmd_file = make_temp_file()
|
||||||
|
cmd_file.write_text(cmd)
|
||||||
|
proc = run_program(
|
||||||
|
cmd=['sudo', 'dmsetup', 'create', name, cmd_file],
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if proc.returncode == 0:
|
||||||
|
inner_volumes.append(Disk(f'/dev/mapper/{name}'))
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return inner_volumes
|
||||||
|
|
||||||
|
|
||||||
|
def unmount(source_or_mountpoint):
|
||||||
|
"""Unmount source_or_mountpoint.
|
||||||
|
|
||||||
|
NOTE: If not running_as_root() then udevil will be used.
|
||||||
|
"""
|
||||||
|
cmd = [
|
||||||
|
'umount',
|
||||||
|
source_or_mountpoint,
|
||||||
|
]
|
||||||
|
if not running_as_root():
|
||||||
|
cmd.insert(0, 'udevil')
|
||||||
|
|
||||||
|
# Run unmount command
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
if not proc.returncode == 0:
|
||||||
|
raise RuntimeError(f'Failed to unmount: {source_or_mountpoint}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
181
scripts/wk/os/win.py
Normal file
181
scripts/wk/os/win.py
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
"""WizardKit: Windows Functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import platform
|
||||||
|
|
||||||
|
from wk.borrowed import acpi
|
||||||
|
from wk.exe import run_program
|
||||||
|
from wk.io import non_clobber_path
|
||||||
|
from wk.log import format_log_path
|
||||||
|
from wk.std import GenericError, GenericWarning, sleep
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
OS_VERSION = float(platform.win32_ver()[0]) # TODO: Check if Win8.1 returns '8'
|
||||||
|
REG_MSISERVER = r'HKLM\SYSTEM\CurrentControlSet\Control\SafeBoot\Network\MSIServer'
|
||||||
|
SLMGR = pathlib.Path(f'{os.environ.get("SYSTEMROOT")}/System32/slmgr.vbs')
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def activate_with_bios():
|
||||||
|
"""Attempt to activate Windows with a key stored in the BIOS."""
|
||||||
|
# Code borrowed from https://github.com/aeruder/get_win8key
|
||||||
|
#####################################################
|
||||||
|
#script to query windows 8.x OEM key from PC firmware
|
||||||
|
#ACPI -> table MSDM -> raw content -> byte offset 56 to end
|
||||||
|
#ck, 03-Jan-2014 (christian@korneck.de)
|
||||||
|
#####################################################
|
||||||
|
bios_key = None
|
||||||
|
table = b"MSDM"
|
||||||
|
if acpi.FindAcpiTable(table) is True:
|
||||||
|
rawtable = acpi.GetAcpiTable(table)
|
||||||
|
#http://msdn.microsoft.com/library/windows/hardware/hh673514
|
||||||
|
#byte offset 36 from beginning
|
||||||
|
# = Microsoft 'software licensing data structure'
|
||||||
|
# / 36 + 20 bytes offset from beginning = Win Key
|
||||||
|
bios_key = rawtable[56:len(rawtable)].decode("utf-8")
|
||||||
|
if not bios_key:
|
||||||
|
raise GenericError('BIOS key not found.')
|
||||||
|
|
||||||
|
# Check if activation is needed
|
||||||
|
if is_activated():
|
||||||
|
raise GenericWarning('System already activated')
|
||||||
|
|
||||||
|
# Install Key
|
||||||
|
cmd = ['cscript', '//nologo', SLMGR, '/ipk', bios_key]
|
||||||
|
run_program(cmd, check=False)
|
||||||
|
sleep(5)
|
||||||
|
|
||||||
|
# Attempt activation
|
||||||
|
cmd = ['cscript', '//nologo', SLMGR, '/ato']
|
||||||
|
run_program(cmd, check=False)
|
||||||
|
sleep(5)
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
if not is_activated():
|
||||||
|
raise GenericError('Activation Failed')
|
||||||
|
|
||||||
|
|
||||||
|
def disable_safemode():
|
||||||
|
"""Edit BCD to remove safeboot value."""
|
||||||
|
cmd = ['bcdedit', '/deletevalue', '{default}', 'safeboot']
|
||||||
|
run_program(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def disable_safemode_msi():
|
||||||
|
"""Disable MSI access under safemode."""
|
||||||
|
cmd = ['reg', 'delete', REG_MSISERVER, '/f']
|
||||||
|
run_program(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_safemode():
|
||||||
|
"""Edit BCD to set safeboot as default."""
|
||||||
|
cmd = ['bcdedit', '/set', '{default}', 'safeboot', 'network']
|
||||||
|
run_program(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_safemode_msi():
|
||||||
|
"""Enable MSI access under safemode."""
|
||||||
|
cmd = ['reg', 'add', REG_MSISERVER, '/f']
|
||||||
|
run_program(cmd)
|
||||||
|
cmd = [
|
||||||
|
'reg', 'add', REG_MSISERVER, '/ve',
|
||||||
|
'/t', 'REG_SZ',
|
||||||
|
'/d', 'Service', '/f',
|
||||||
|
]
|
||||||
|
run_program(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def get_activation_string():
|
||||||
|
"""Get activation status, returns str."""
|
||||||
|
cmd = ['cscript', '//nologo', SLMGR, '/xpr']
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
act_str = proc.stdout
|
||||||
|
act_str = act_str.splitlines()[1]
|
||||||
|
act_str = act_str.strip()
|
||||||
|
return act_str
|
||||||
|
|
||||||
|
|
||||||
|
def is_activated():
|
||||||
|
"""Check if Windows is activated via slmgr.vbs and return bool."""
|
||||||
|
act_str = get_activation_string()
|
||||||
|
|
||||||
|
# Check result.
|
||||||
|
return act_str and 'permanent' in act_str
|
||||||
|
|
||||||
|
|
||||||
|
def run_chkdsk_offline():
|
||||||
|
"""Set filesystem 'dirty bit' to force a CHKDSK during startup."""
|
||||||
|
cmd = f'fsutil dirty set {os.environ.get("SYSTEMDRIVE")}'
|
||||||
|
proc = run_program(cmd.split(), check=False)
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
if proc.returncode > 0:
|
||||||
|
raise GenericError('Failed to set dirty bit.')
|
||||||
|
|
||||||
|
|
||||||
|
def run_chkdsk_online():
|
||||||
|
"""Run CHKDSK in a split window.
|
||||||
|
|
||||||
|
NOTE: If run on Windows 8+ online repairs are attempted.
|
||||||
|
"""
|
||||||
|
cmd = ['CHKDSK', os.environ.get('SYSTEMDRIVE', 'C:')]
|
||||||
|
if OS_VERSION >= 8:
|
||||||
|
cmd.extend(['/scan', '/perf'])
|
||||||
|
log_path = format_log_path(log_name='CHKDSK', tool=True)
|
||||||
|
err_path = log_path.with_suffix('.err')
|
||||||
|
|
||||||
|
# Run scan
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
if proc.returncode == 1:
|
||||||
|
raise GenericWarning('Repaired (or manually aborted)')
|
||||||
|
if proc.returncode > 1:
|
||||||
|
raise GenericError('Issue(s) detected')
|
||||||
|
|
||||||
|
# Save output
|
||||||
|
os.makedirs(log_path.parent, exist_ok=True)
|
||||||
|
with open(log_path, 'w') as _f:
|
||||||
|
_f.write(proc.stdout)
|
||||||
|
with open(err_path, 'w') as _f:
|
||||||
|
_f.write(proc.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def run_sfc_scan():
|
||||||
|
"""Run SFC and save results."""
|
||||||
|
cmd = ['sfc', '/scannow']
|
||||||
|
log_path = format_log_path(log_name='SFC', tool=True)
|
||||||
|
err_path = log_path.with_suffix('.err')
|
||||||
|
|
||||||
|
# Run SFC
|
||||||
|
proc = run_program(cmd, check=False, encoding='utf-16')
|
||||||
|
|
||||||
|
# Fix paths
|
||||||
|
log_path = non_clobber_path(log_path)
|
||||||
|
err_path = non_clobber_path(err_path)
|
||||||
|
|
||||||
|
# Save output
|
||||||
|
os.makedirs(log_path.parent, exist_ok=True)
|
||||||
|
with open(log_path, 'w') as _f:
|
||||||
|
_f.write(proc.stdout)
|
||||||
|
with open(err_path, 'w') as _f:
|
||||||
|
_f.write(proc.stderr)
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
if 'did not find any integrity violations' in proc.stdout:
|
||||||
|
pass
|
||||||
|
elif 'successfully repaired' in proc.stdout:
|
||||||
|
raise GenericWarning('Repaired')
|
||||||
|
elif 'found corrupt files' in proc.stdout:
|
||||||
|
raise GenericError('Corruption detected')
|
||||||
|
else:
|
||||||
|
raise OSError
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
1061
scripts/wk/std.py
1061
scripts/wk/std.py
File diff suppressed because it is too large
Load diff
1
scripts/wk/sw/__init__.py
Normal file
1
scripts/wk/sw/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""WizardKit: sw module init"""
|
||||||
287
scripts/wk/tmux.py
Normal file
287
scripts/wk/tmux.py
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
"""WizardKit: tmux Functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
from wk.exe import run_program
|
||||||
|
from wk.std import PLATFORM
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC_VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def capture_pane(pane_id=None):
|
||||||
|
"""Capture text from current or target pane, returns str."""
|
||||||
|
cmd = ['tmux', 'capture-pane', '-p']
|
||||||
|
if pane_id:
|
||||||
|
cmd.extend(['-t', pane_id])
|
||||||
|
|
||||||
|
# Capture and return
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
return proc.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def clear_pane(pane_id=None):
|
||||||
|
"""Clear pane buffer for current or target pane."""
|
||||||
|
cmd = ['tmux', 'send-keys', '-R']
|
||||||
|
if pane_id:
|
||||||
|
cmd.extend(['-t', pane_id])
|
||||||
|
|
||||||
|
# Clear pane
|
||||||
|
run_program(cmd, check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_layout(panes, layout, forced=False):
|
||||||
|
"""Fix pane sizes based on layout."""
|
||||||
|
if not (forced or layout_needs_fixed(panes, layout)):
|
||||||
|
# Layout should be fine
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update panes
|
||||||
|
for name, data in layout.items():
|
||||||
|
# Skip missing panes
|
||||||
|
if name not in panes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Resize pane(s)
|
||||||
|
pane_list = panes[name]
|
||||||
|
if isinstance(pane_list, str):
|
||||||
|
pane_list = [pane_list]
|
||||||
|
for pane_id in pane_list:
|
||||||
|
if name == 'Current':
|
||||||
|
pane_id = None
|
||||||
|
try:
|
||||||
|
resize_pane(pane_id, **data)
|
||||||
|
except RuntimeError:
|
||||||
|
# Assuming pane was closed just before resizing
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_pane_size(pane_id=None):
|
||||||
|
"""Get current or target pane size, returns tuple."""
|
||||||
|
cmd = ['tmux', 'display', '-p']
|
||||||
|
if pane_id:
|
||||||
|
cmd.extend(['-t', pane_id])
|
||||||
|
cmd.append('#{pane_width} #{pane_height}')
|
||||||
|
|
||||||
|
# Get resolution
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
width, height = proc.stdout.strip().split()
|
||||||
|
width = int(width)
|
||||||
|
height = int(height)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return (width, height)
|
||||||
|
|
||||||
|
|
||||||
|
def kill_all_panes(pane_id=None):
|
||||||
|
"""Kill all panes except for the current or target pane."""
|
||||||
|
cmd = ['tmux', 'kill-pane', '-a']
|
||||||
|
if pane_id:
|
||||||
|
cmd.extend(['-t', pane_id])
|
||||||
|
|
||||||
|
# Kill
|
||||||
|
run_program(cmd, check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def kill_pane(*pane_ids):
|
||||||
|
"""Kill pane(s) by id."""
|
||||||
|
cmd = ['tmux', 'kill-pane', '-t']
|
||||||
|
|
||||||
|
# Iterate over all passed pane IDs
|
||||||
|
for pane_id in pane_ids:
|
||||||
|
run_program(cmd+[pane_id], check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def layout_needs_fixed(panes, layout):
|
||||||
|
"""Check if layout needs fixed, returns bool."""
|
||||||
|
needs_fixed = False
|
||||||
|
|
||||||
|
# Check panes
|
||||||
|
for name, data in layout.items():
|
||||||
|
# Skip unpredictably sized panes
|
||||||
|
if not data.get('Check', False):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip missing panes
|
||||||
|
if name not in panes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check pane size(s)
|
||||||
|
pane_list = panes[name]
|
||||||
|
if isinstance(pane_list, str):
|
||||||
|
pane_list = [pane_list]
|
||||||
|
for pane_id in pane_list:
|
||||||
|
try:
|
||||||
|
width, height = get_pane_size(pane_id)
|
||||||
|
except ValueError:
|
||||||
|
# Pane may have disappeared during this loop
|
||||||
|
continue
|
||||||
|
if data.get('width', False) and data['width'] != width:
|
||||||
|
needs_fixed = True
|
||||||
|
if data.get('height', False) and data['height'] != height:
|
||||||
|
needs_fixed = True
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return needs_fixed
|
||||||
|
|
||||||
|
|
||||||
|
def poll_pane(pane_id):
|
||||||
|
"""Check if pane exists, returns bool."""
|
||||||
|
cmd = ['tmux', 'list-panes', '-F', '#D']
|
||||||
|
|
||||||
|
# Get list of panes
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
existant_panes = proc.stdout.splitlines()
|
||||||
|
|
||||||
|
# Check if pane exists
|
||||||
|
return pane_id in existant_panes
|
||||||
|
|
||||||
|
|
||||||
|
def prep_action(
|
||||||
|
cmd=None, working_dir=None, text=None, watch_file=None, watch_cmd='cat'):
|
||||||
|
"""Prep action to perform during a tmux call, returns list.
|
||||||
|
|
||||||
|
This will prep for running a basic command, displaying text on screen,
|
||||||
|
or monitoring a file. The last option uses cat by default but can be
|
||||||
|
overridden by using the watch_cmd.
|
||||||
|
"""
|
||||||
|
action_cmd = []
|
||||||
|
if working_dir:
|
||||||
|
action_cmd.extend(['-c', working_dir])
|
||||||
|
|
||||||
|
if cmd:
|
||||||
|
# Basic command
|
||||||
|
action_cmd.append(cmd)
|
||||||
|
elif text:
|
||||||
|
# Display text
|
||||||
|
echo_cmd = ['echo']
|
||||||
|
if PLATFORM == 'Linux':
|
||||||
|
echo_cmd.append('-e')
|
||||||
|
action_cmd.extend([
|
||||||
|
'watch',
|
||||||
|
'--color',
|
||||||
|
'--exec',
|
||||||
|
'--no-title',
|
||||||
|
'--interval', '1',
|
||||||
|
])
|
||||||
|
action_cmd.extend(echo_cmd)
|
||||||
|
action_cmd.append(text)
|
||||||
|
elif watch_file:
|
||||||
|
# Monitor file
|
||||||
|
prep_file(watch_file)
|
||||||
|
if watch_cmd == 'cat':
|
||||||
|
action_cmd.extend([
|
||||||
|
'watch',
|
||||||
|
'--color',
|
||||||
|
'--no-title',
|
||||||
|
'--interval', '1',
|
||||||
|
'cat',
|
||||||
|
])
|
||||||
|
elif watch_cmd == 'tail':
|
||||||
|
action_cmd.extend(['tail', '-f'])
|
||||||
|
action_cmd.append(watch_file)
|
||||||
|
else:
|
||||||
|
LOG.error('No action specified')
|
||||||
|
raise RuntimeError('No action specified')
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return action_cmd
|
||||||
|
|
||||||
|
|
||||||
|
def prep_file(path):
|
||||||
|
"""Check if file exists and create empty file if not."""
|
||||||
|
path = pathlib.Path(path).resolve()
|
||||||
|
try:
|
||||||
|
path.touch(exist_ok=False)
|
||||||
|
except FileExistsError:
|
||||||
|
# Leave existing files alone
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def resize_pane(pane_id=None, width=None, height=None, **kwargs):
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
"""Resize current or target pane.
|
||||||
|
|
||||||
|
NOTE: kwargs is only here to make calling this function easier
|
||||||
|
by dropping any extra kwargs passed.
|
||||||
|
"""
|
||||||
|
cmd = ['tmux', 'resize-pane']
|
||||||
|
|
||||||
|
# Safety checks
|
||||||
|
if not (width or height):
|
||||||
|
LOG.error('Neither width nor height specified')
|
||||||
|
raise RuntimeError('Neither width nor height specified')
|
||||||
|
|
||||||
|
# Finish building cmd
|
||||||
|
if pane_id:
|
||||||
|
cmd.extend(['-t', pane_id])
|
||||||
|
if width:
|
||||||
|
cmd.extend(['-x', str(width)])
|
||||||
|
if height:
|
||||||
|
cmd.extend(['-y', str(height)])
|
||||||
|
|
||||||
|
# Resize
|
||||||
|
run_program(cmd, check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def split_window(
|
||||||
|
lines=None, percent=None,
|
||||||
|
behind=False, vertical=False,
|
||||||
|
target_id=None, **action):
|
||||||
|
"""Split tmux window, run action, and return pane_id as str."""
|
||||||
|
cmd = ['tmux', 'split-window', '-d', '-PF', '#D']
|
||||||
|
|
||||||
|
# Safety checks
|
||||||
|
if not (lines or percent):
|
||||||
|
LOG.error('Neither lines nor percent specified')
|
||||||
|
raise RuntimeError('Neither lines nor percent specified')
|
||||||
|
|
||||||
|
# New pane placement
|
||||||
|
if behind:
|
||||||
|
cmd.append('-b')
|
||||||
|
if vertical:
|
||||||
|
cmd.append('-v')
|
||||||
|
else:
|
||||||
|
cmd.append('-h')
|
||||||
|
if target_id:
|
||||||
|
cmd.extend(['-t', target_id])
|
||||||
|
|
||||||
|
# New pane size
|
||||||
|
if lines:
|
||||||
|
cmd.extend(['-l', str(lines)])
|
||||||
|
elif percent:
|
||||||
|
cmd.extend(['-p', str(percent)])
|
||||||
|
|
||||||
|
# New pane action
|
||||||
|
cmd.extend(prep_action(**action))
|
||||||
|
|
||||||
|
# Run and return pane_id
|
||||||
|
proc = run_program(cmd, check=False)
|
||||||
|
return proc.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def respawn_pane(pane_id, **action):
|
||||||
|
"""Respawn pane with action."""
|
||||||
|
cmd = ['tmux', 'respawn-pane', '-k', '-t', pane_id]
|
||||||
|
cmd.extend(prep_action(**action))
|
||||||
|
|
||||||
|
# Respawn
|
||||||
|
run_program(cmd, check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def zoom_pane(pane_id=None):
|
||||||
|
"""Toggle zoom status for current or target pane."""
|
||||||
|
cmd = ['tmux', 'resize-pane', '-Z']
|
||||||
|
if pane_id:
|
||||||
|
cmd.extend(['-t', pane_id])
|
||||||
|
|
||||||
|
# Toggle
|
||||||
|
run_program(cmd, check=False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
|
|
@ -11,10 +11,10 @@ set -o pipefail
|
||||||
DATE="$(date +%F)"
|
DATE="$(date +%F)"
|
||||||
DATETIME="$(date +%F_%H%M)"
|
DATETIME="$(date +%F_%H%M)"
|
||||||
ROOT_DIR="$(realpath $(dirname "$0"))"
|
ROOT_DIR="$(realpath $(dirname "$0"))"
|
||||||
BUILD_DIR="$ROOT_DIR/BUILD_LINUX"
|
BUILD_DIR="$ROOT_DIR/setup/BUILD_LINUX"
|
||||||
LIVE_DIR="$BUILD_DIR/live"
|
LIVE_DIR="$BUILD_DIR/live"
|
||||||
LOG_DIR="$BUILD_DIR/logs"
|
LOG_DIR="$BUILD_DIR/logs"
|
||||||
OUT_DIR="$ROOT_DIR/OUT_LINUX"
|
OUT_DIR="$ROOT_DIR/setup/OUT_LINUX"
|
||||||
REPO_DIR="$BUILD_DIR/repo"
|
REPO_DIR="$BUILD_DIR/repo"
|
||||||
SKEL_DIR="$LIVE_DIR/airootfs/etc/skel"
|
SKEL_DIR="$LIVE_DIR/airootfs/etc/skel"
|
||||||
TEMP_DIR="$BUILD_DIR/temp"
|
TEMP_DIR="$BUILD_DIR/temp"
|
||||||
|
|
@ -57,7 +57,7 @@ function cleanup() {
|
||||||
|
|
||||||
function fix_kit_permissions() {
|
function fix_kit_permissions() {
|
||||||
# GitHub zip archives don't preserve the correct permissions
|
# GitHub zip archives don't preserve the correct permissions
|
||||||
for d in .bin .cbin .kit_items .linux_items .pe_items Images; do
|
for d in docs images scripts setup; do
|
||||||
find "$ROOT_DIR/$d" -type d -exec chmod 755 "{}" \;
|
find "$ROOT_DIR/$d" -type d -exec chmod 755 "{}" \;
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +80,7 @@ function load_settings() {
|
||||||
if [[ "${1:-}" == "--edit" ]]; then
|
if [[ "${1:-}" == "--edit" ]]; then
|
||||||
# Copy settings
|
# Copy settings
|
||||||
if [[ ! -e "$BUILD_DIR/main.py" ]] || ask "Overwrite main.py?"; then
|
if [[ ! -e "$BUILD_DIR/main.py" ]] || ask "Overwrite main.py?"; then
|
||||||
cp -bv "$ROOT_DIR/.bin/Scripts/settings/main.py" "$BUILD_DIR/main.py"
|
cp -bv "$ROOT_DIR/scripts/wk/cfg/main.py" "$BUILD_DIR/main.py"
|
||||||
dos2unix "$BUILD_DIR/main.py"
|
dos2unix "$BUILD_DIR/main.py"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -89,7 +89,7 @@ function load_settings() {
|
||||||
"$EDITOR" "$BUILD_DIR/main.py"
|
"$EDITOR" "$BUILD_DIR/main.py"
|
||||||
else
|
else
|
||||||
# Load settings from $LIVE_DIR
|
# Load settings from $LIVE_DIR
|
||||||
_main_path="$LIVE_DIR/airootfs/usr/local/bin/settings/main.py"
|
_main_path="$LIVE_DIR/airootfs/usr/local/bin/wk/cfg/main.py"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Load settings
|
# Load settings
|
||||||
|
|
@ -118,13 +118,13 @@ function copy_live_env() {
|
||||||
rm "$LIVE_DIR/syslinux"/*.cfg "$LIVE_DIR/syslinux"/*.png
|
rm "$LIVE_DIR/syslinux"/*.cfg "$LIVE_DIR/syslinux"/*.png
|
||||||
|
|
||||||
# Add items
|
# Add items
|
||||||
rsync -aI "$ROOT_DIR/.linux_items/include/" "$LIVE_DIR/"
|
rsync -aI "$ROOT_DIR/setup/linux/include/" "$LIVE_DIR/"
|
||||||
if [[ "${1:-}" != "--minimal" ]]; then
|
if [[ "${1:-}" != "--minimal" ]]; then
|
||||||
rsync -aI "$ROOT_DIR/.linux_items/include_x/" "$LIVE_DIR/"
|
rsync -aI "$ROOT_DIR/setup/linux/include_x/" "$LIVE_DIR/"
|
||||||
fi
|
fi
|
||||||
mkdir -p "$LIVE_DIR/airootfs/usr/local/bin"
|
mkdir -p "$LIVE_DIR/airootfs/usr/local/bin"
|
||||||
rsync -aI "$ROOT_DIR/.bin/Scripts/" "$LIVE_DIR/airootfs/usr/local/bin/"
|
rsync -aI "$ROOT_DIR/scripts/" "$LIVE_DIR/airootfs/usr/local/bin/"
|
||||||
cp -a "$BUILD_DIR/main.py" "$LIVE_DIR/airootfs/usr/local/bin/settings/"
|
cp -a "$BUILD_DIR/main.py" "$LIVE_DIR/airootfs/usr/local/bin/wk/cfg/"
|
||||||
}
|
}
|
||||||
|
|
||||||
function run_elevated() {
|
function run_elevated() {
|
||||||
|
|
@ -155,8 +155,8 @@ function update_live_env() {
|
||||||
|
|
||||||
# Boot config (legacy)
|
# Boot config (legacy)
|
||||||
mkdir -p "$LIVE_DIR/arch"
|
mkdir -p "$LIVE_DIR/arch"
|
||||||
cp "$ROOT_DIR/Images/Pxelinux.jpg" "$LIVE_DIR/arch/pxelinux.jpg"
|
cp "$ROOT_DIR/images/Pxelinux.png" "$LIVE_DIR/arch/pxelinux.png"
|
||||||
cp "$ROOT_DIR/Images/Syslinux.jpg" "$LIVE_DIR/arch/syslinux.jpg"
|
cp "$ROOT_DIR/images/Syslinux.png" "$LIVE_DIR/arch/syslinux.png"
|
||||||
sed -i -r "s/_+/$KIT_NAME_FULL/" "$LIVE_DIR/syslinux/wk_head.cfg"
|
sed -i -r "s/_+/$KIT_NAME_FULL/" "$LIVE_DIR/syslinux/wk_head.cfg"
|
||||||
mkdir -p "$TEMP_DIR" 2>/dev/null
|
mkdir -p "$TEMP_DIR" 2>/dev/null
|
||||||
curl -Lo "$TEMP_DIR/wimboot.zip" "http://git.ipxe.org/releases/wimboot/wimboot-latest.zip"
|
curl -Lo "$TEMP_DIR/wimboot.zip" "http://git.ipxe.org/releases/wimboot/wimboot-latest.zip"
|
||||||
|
|
@ -165,7 +165,7 @@ function update_live_env() {
|
||||||
# Boot config (UEFI)
|
# Boot config (UEFI)
|
||||||
mkdir -p "$LIVE_DIR/EFI/boot"
|
mkdir -p "$LIVE_DIR/EFI/boot"
|
||||||
cp "/usr/share/refind/refind_x64.efi" "$LIVE_DIR/EFI/boot/bootx64.efi"
|
cp "/usr/share/refind/refind_x64.efi" "$LIVE_DIR/EFI/boot/bootx64.efi"
|
||||||
cp "$ROOT_DIR/Images/rEFInd.png" "$LIVE_DIR/EFI/boot/rEFInd.png"
|
cp "$ROOT_DIR/images/rEFInd.png" "$LIVE_DIR/EFI/boot/rEFInd.png"
|
||||||
rsync -aI "/usr/share/refind/drivers_x64/" "$LIVE_DIR/EFI/boot/drivers_x64/"
|
rsync -aI "/usr/share/refind/drivers_x64/" "$LIVE_DIR/EFI/boot/drivers_x64/"
|
||||||
rsync -aI "/usr/share/refind/icons/" "$LIVE_DIR/EFI/boot/icons/" --exclude "/usr/share/refind/icons/svg"
|
rsync -aI "/usr/share/refind/icons/" "$LIVE_DIR/EFI/boot/icons/" --exclude "/usr/share/refind/icons/svg"
|
||||||
sed -i "s/%ARCHISO_LABEL%/${label}/" "$LIVE_DIR/EFI/boot/refind.conf"
|
sed -i "s/%ARCHISO_LABEL%/${label}/" "$LIVE_DIR/EFI/boot/refind.conf"
|
||||||
|
|
@ -199,12 +199,12 @@ function update_live_env() {
|
||||||
# Live packages
|
# Live packages
|
||||||
while read -r p; do
|
while read -r p; do
|
||||||
sed -i "/$p/d" "$LIVE_DIR/packages.x86_64"
|
sed -i "/$p/d" "$LIVE_DIR/packages.x86_64"
|
||||||
done < "$ROOT_DIR/.linux_items/packages/live_remove"
|
done < "$ROOT_DIR/setup/linux/packages/live_remove"
|
||||||
cat "$ROOT_DIR/.linux_items/packages/live_add" >> "$LIVE_DIR/packages.x86_64"
|
cat "$ROOT_DIR/setup/linux/packages/live_add" >> "$LIVE_DIR/packages.x86_64"
|
||||||
if [[ "${1:-}" == "--minimal" ]]; then
|
if [[ "${1:-}" == "--minimal" ]]; then
|
||||||
cat "$ROOT_DIR/.linux_items/packages/live_add_min" >> "$LIVE_DIR/packages.x86_64"
|
cat "$ROOT_DIR/setup/linux/packages/live_add_min" >> "$LIVE_DIR/packages.x86_64"
|
||||||
else
|
else
|
||||||
cat "$ROOT_DIR/.linux_items/packages/live_add_x" >> "$LIVE_DIR/packages.x86_64"
|
cat "$ROOT_DIR/setup/linux/packages/live_add_x" >> "$LIVE_DIR/packages.x86_64"
|
||||||
fi
|
fi
|
||||||
echo "[custom]" >> "$LIVE_DIR/pacman.conf"
|
echo "[custom]" >> "$LIVE_DIR/pacman.conf"
|
||||||
echo "SigLevel = Optional TrustAll" >> "$LIVE_DIR/pacman.conf"
|
echo "SigLevel = Optional TrustAll" >> "$LIVE_DIR/pacman.conf"
|
||||||
|
|
@ -246,7 +246,7 @@ function update_live_env() {
|
||||||
echo 'rm /root/.zlogin' >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh"
|
echo 'rm /root/.zlogin' >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh"
|
||||||
sed -i -r '/.*PermitRootLogin.*/d' "$LIVE_DIR/airootfs/root/customize_airootfs.sh"
|
sed -i -r '/.*PermitRootLogin.*/d' "$LIVE_DIR/airootfs/root/customize_airootfs.sh"
|
||||||
echo "sed -i -r '/.*PermitRootLogin.*/d' /etc/ssh/sshd_config" >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh"
|
echo "sed -i -r '/.*PermitRootLogin.*/d' /etc/ssh/sshd_config" >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh"
|
||||||
cp "$ROOT_DIR/.linux_items/authorized_keys" "$SKEL_DIR/.ssh/authorized_keys"
|
cp "$ROOT_DIR/setup/linux/authorized_keys" "$SKEL_DIR/.ssh/authorized_keys"
|
||||||
|
|
||||||
# Root user
|
# Root user
|
||||||
echo "echo 'root:$ROOT_PASSWORD' | chpasswd" >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh"
|
echo "echo 'root:$ROOT_PASSWORD' | chpasswd" >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh"
|
||||||
|
|
@ -279,11 +279,11 @@ function update_live_env() {
|
||||||
|
|
||||||
# Wallpaper
|
# Wallpaper
|
||||||
mkdir -p "$LIVE_DIR/airootfs/usr/share/wallpaper"
|
mkdir -p "$LIVE_DIR/airootfs/usr/share/wallpaper"
|
||||||
cp "$ROOT_DIR/Images/Linux.jpg" "$LIVE_DIR/airootfs/usr/share/wallpaper/burned.in"
|
cp "$ROOT_DIR/images/Linux.png" "$LIVE_DIR/airootfs/usr/share/wallpaper/burned.in"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# WiFi
|
# WiFi
|
||||||
cp "$ROOT_DIR/.linux_items/known_networks" "$LIVE_DIR/airootfs/root/known_networks"
|
cp "$ROOT_DIR/setup/linux/known_networks" "$LIVE_DIR/airootfs/root/known_networks"
|
||||||
echo "add-known-networks --user=$username" >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh"
|
echo "add-known-networks --user=$username" >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,7 +315,7 @@ function update_repo() {
|
||||||
makepkg -d
|
makepkg -d
|
||||||
popd >/dev/null
|
popd >/dev/null
|
||||||
mv -n $p/*xz "$REPO_DIR"/
|
mv -n $p/*xz "$REPO_DIR"/
|
||||||
done < "$ROOT_DIR/.linux_items/packages/aur"
|
done < "$ROOT_DIR/setup/linux/packages/aur"
|
||||||
popd >/dev/null
|
popd >/dev/null
|
||||||
|
|
||||||
# Build custom repo database
|
# Build custom repo database
|
||||||
|
|
@ -329,7 +329,7 @@ function install_deps() {
|
||||||
packages=
|
packages=
|
||||||
while read -r line; do
|
while read -r line; do
|
||||||
packages="$packages $line"
|
packages="$packages $line"
|
||||||
done < "$ROOT_DIR/.linux_items/packages/dependencies"
|
done < "$ROOT_DIR/setup/linux/packages/dependencies"
|
||||||
run_elevated pacman -Syu --needed --noconfirm $packages
|
run_elevated pacman -Syu --needed --noconfirm $packages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -347,7 +347,7 @@ function build_iso() {
|
||||||
chmod 600 "$LIVE_DIR/airootfs/etc/skel/.ssh/id_rsa"
|
chmod 600 "$LIVE_DIR/airootfs/etc/skel/.ssh/id_rsa"
|
||||||
|
|
||||||
# Removing cached (and possibly outdated) custom repo packages
|
# Removing cached (and possibly outdated) custom repo packages
|
||||||
for package in $(cat "$ROOT_DIR/.linux_items/packages/aur"); do
|
for package in $(cat "$ROOT_DIR/setup/linux/packages/aur"); do
|
||||||
for p in /var/cache/pacman/pkg/*${package}*; do
|
for p in /var/cache/pacman/pkg/*${package}*; do
|
||||||
if [[ -f "${p}" ]]; then
|
if [[ -f "${p}" ]]; then
|
||||||
rm "${p}"
|
rm "${p}"
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,6 @@ PS1='[\u@\h \W]\$ '
|
||||||
|
|
||||||
# Update LS_COLORS
|
# Update LS_COLORS
|
||||||
eval $(dircolors ~/.dircolors)
|
eval $(dircolors ~/.dircolors)
|
||||||
|
|
||||||
|
# WizardKit
|
||||||
|
export PYTHONPATH='/usr/local/bin'
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,4 @@ source $ZSH/oh-my-zsh.sh
|
||||||
# Wizard Kit
|
# Wizard Kit
|
||||||
. $HOME/.aliases
|
. $HOME/.aliases
|
||||||
eval $(dircolors ~/.dircolors)
|
eval $(dircolors ~/.dircolors)
|
||||||
|
export PYTHONPATH="/usr/local/bin"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue