mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-14 03:50:57 +01:00
Split three more scripts; make the verifier catch heredoc cuts
transcribe.sh 473 -> 39, setup_activitywatch.sh 457 -> 102 and setup_wireguard_ssh.sh 464 -> 136. enforce_vbox_hosts.sh is reverted: its seam landed inside a heredoc and the resulting lib had an unterminated here-document. That is the worst failure yet, because shellcheck reported it only as SC1094 on the caller - which reads like a nicety, not a file that will not parse. The verifier now runs bash -n over every lib before linting the entry and names the offending file; proven by corrupting a copy and restoring it. Also guarded a cd before `git clone ... .`, which would otherwise have cloned an AUR repo into the caller's directory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
parent
63650fbc45
commit
0aa78ea7aa
@ -0,0 +1,19 @@
|
||||
{
|
||||
"title": "Split three more linux_configuration scripts and harden the split verifier",
|
||||
"objective": "Bring transcribe.sh (473), setup_activitywatch.sh (457) and setup_wireguard_ssh.sh (464) under the 250-line cap, and make the verifier catch a split that lands inside a heredoc.",
|
||||
"acceptance_criteria": [
|
||||
"All three entry scripts and all nine new libs are at most 250 lines.",
|
||||
"The pre-commit shellcheck hook passes on every touched file, with no new suppression and no disable directive.",
|
||||
"No lib sources a sibling.",
|
||||
"meta/scripts/verify_shell_split.sh parses every lib with bash -n BEFORE linting the entry script, and reports the lib by name when it fails. A heredoc cut otherwise surfaces as a confusing SC1094 on the caller.",
|
||||
"That new check is proven in both directions: it fails on a deliberately unterminated heredoc and passes once restored.",
|
||||
"No script is executed. These install CUDA runtimes, start services and rewrite sshd and nftables config.",
|
||||
"enforce_vbox_hosts.sh is fully restored, leaving no partial split and no orphan lib directory."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Executing any install or firewall step.",
|
||||
"enforce_vbox_hosts.sh, whose seam fell inside a heredoc.",
|
||||
"Changing what any script does when run, beyond the cd guard."
|
||||
],
|
||||
"verifier": "meta/scripts/verify_shell_split.sh per script, plus a deliberate heredoc-corruption test of the verifier itself, plus pre-commit run --files and check_file_length.sh."
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
{
|
||||
"intent": "Split three more linux_configuration scripts, and harden the verifier after a split silently corrupted a file.",
|
||||
"scope": [
|
||||
"misc/testsAndMisc-bash/transcribe.sh (473 -> 39) + transcribe_pkgmgr, transcribe_deps, transcribe_venv",
|
||||
"single_use/features/setup_activitywatch.sh (457 -> 102) + aw_install, aw_service, aw_autostart",
|
||||
"single_use/features/setup_wireguard_ssh.sh (464 -> 136) + wg_keys, wg_firewall, wg_sshd",
|
||||
"meta/scripts/verify_shell_split.sh (lib parse check moved ahead of the entry lint)",
|
||||
"Deferred and fully restored: virtualbox/enforce_vbox_hosts.sh (443)",
|
||||
"Non-goal: no script executed"
|
||||
],
|
||||
"changes": [
|
||||
"Nine libs across three scripts, each needing a second carve after the first pass.",
|
||||
"enforce_vbox_hosts.sh was reverted because the split landed INSIDE a heredoc: the resulting lib had an unterminated here-document and bash -n rejected it outright. This is the first split in the whole effort to produce a file that does not parse, and it is the most dangerous failure mode yet - shellcheck reported it only as SC1094 'parsing of sourced file failed' on the caller, which reads like a linting nicety rather than a broken file.",
|
||||
"The verifier now runs bash -n over every lib BEFORE linting the entry, and names the offending lib with a hint about heredocs. Proven in both directions on a deliberately corrupted copy.",
|
||||
"Fixed another unguarded cd, this one before `git clone ... .` - a failed cd would have cloned an AUR repo into the caller's working directory.",
|
||||
"ALLOW_WEB and ALLOW_DNS are declared in the entry, set by the arg parser in one lib and read by the ruleset builder in another; exported, which is what SC2034 asks for and makes the three-way contract explicit."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "meta/scripts/verify_shell_split.sh per script",
|
||||
"result": "fail then pass",
|
||||
"evidence": "All three now report 'libs sourced, functions defined'. Four real failures on the way: chained libs in all three, the unguarded cd, the exported flags, and the vbox heredoc corruption."
|
||||
},
|
||||
{
|
||||
"command": "append an unterminated heredoc to a lib, re-run the verifier, restore",
|
||||
"result": "pass",
|
||||
"evidence": "Reports the here-document parse failure and exits non-zero; passes again once restored. Tested in both directions, because a check that never fires is worse than none."
|
||||
},
|
||||
{
|
||||
"command": "git checkout HEAD -- enforce_vbox_hosts.sh, rm its lib dir, bash -n and shellcheck -x",
|
||||
"result": "pass",
|
||||
"evidence": "Restored to 443 lines, parses and lints clean."
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <13 changed files>",
|
||||
"result": "pass",
|
||||
"evidence": "Clean, including shellcheck over each lib in isolation."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"None of the three was run: they install CUDA runtimes and AUR packages, start user services, and rewrite sshd and nftables configuration.",
|
||||
"The heredoc failure means the function-boundary heuristic the splitter uses is not safe on files with heredocs at top level. bash -n now catches it, but only after the fact - the splitter itself still has no awareness of heredocs.",
|
||||
"enforce_vbox_hosts.sh stays at 443 lines and needs a seam chosen by hand, outside any heredoc."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; the three scripts return to their previous sizes, the nine libs disappear, and the verifier loses its parse check.",
|
||||
"After rollback, re-run bash -n and shellcheck on the restored scripts."
|
||||
]
|
||||
}
|
||||
241
linux_configuration/scripts/single_use/features/lib/aw_autostart.sh
Executable file
241
linux_configuration/scripts/single_use/features/lib/aw_autostart.sh
Executable file
@ -0,0 +1,241 @@
|
||||
#!/bin/bash
|
||||
# Autostart unit and the installed/running checks.
|
||||
#
|
||||
# Sourced by setup_activitywatch.sh; split out to keep aw_service.sh under the
|
||||
# 250-line cap. Sourced rather than run, so it inherits the caller's
|
||||
# strict mode and the variables defined above the source line.
|
||||
|
||||
# Function to setup autostart
|
||||
setup_autostart() {
|
||||
echo ""
|
||||
echo "5. Setting Up Autostart..."
|
||||
echo "========================="
|
||||
|
||||
local autostart_dir="$USER_HOME/.config/autostart"
|
||||
local desktop_file="$autostart_dir/activitywatch.desktop"
|
||||
local i3_config="$USER_HOME/.config/i3/config"
|
||||
|
||||
# Method 1: XDG Autostart (works with most desktop environments)
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
sudo -u "$ACTUAL_USER" mkdir -p "$autostart_dir"
|
||||
else
|
||||
mkdir -p "$autostart_dir"
|
||||
fi
|
||||
|
||||
# Create desktop file for autostart
|
||||
cat >"$desktop_file" <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=ActivityWatch
|
||||
Comment=Automated time tracker
|
||||
Exec=aw-qt
|
||||
Icon=activitywatch
|
||||
Hidden=false
|
||||
NoDisplay=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
StartupNotify=false
|
||||
Terminal=false
|
||||
Categories=Utility;
|
||||
EOF
|
||||
|
||||
# Set proper ownership if running as root
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$desktop_file"
|
||||
fi
|
||||
|
||||
echo "✓ Created XDG autostart entry: $desktop_file"
|
||||
|
||||
# Method 2: i3 config autostart (specific to i3)
|
||||
if [[ -f $i3_config ]]; then
|
||||
# Check if autostart entry already exists
|
||||
if ! grep -q "aw-qt" "$i3_config"; then
|
||||
# Add autostart entry to i3 config
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
# Running as root
|
||||
sudo -u "$ACTUAL_USER" bash -c "cat <<'EOF' >> '$i3_config'
|
||||
|
||||
# Auto-start ActivityWatch
|
||||
exec --no-startup-id aw-qt
|
||||
EOF"
|
||||
else
|
||||
{
|
||||
printf '\n'
|
||||
printf '# Auto-start ActivityWatch\n'
|
||||
printf 'exec --no-startup-id aw-qt\n'
|
||||
} >>"$i3_config"
|
||||
fi
|
||||
|
||||
echo "✓ Added ActivityWatch to i3 config autostart"
|
||||
else
|
||||
echo "✓ ActivityWatch autostart already exists in i3 config"
|
||||
fi
|
||||
else
|
||||
echo "! i3 config not found at $i3_config"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create i3blocks status script
|
||||
create_i3blocks_status() {
|
||||
echo ""
|
||||
echo "6. Creating i3blocks Status Script..."
|
||||
echo "===================================="
|
||||
|
||||
local i3blocks_dir="$USER_HOME/.config/i3blocks"
|
||||
local status_script="$i3blocks_dir/activitywatch_status.sh"
|
||||
|
||||
# Create i3blocks directory if it doesn't exist
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
sudo -u "$ACTUAL_USER" mkdir -p "$i3blocks_dir"
|
||||
else
|
||||
mkdir -p "$i3blocks_dir"
|
||||
fi
|
||||
|
||||
# Create the status script
|
||||
cat >"$status_script" <<'EOF'
|
||||
#!/bin/bash
|
||||
# ActivityWatch status script for i3blocks
|
||||
# Shows ActivityWatch installation and running status
|
||||
|
||||
# Check if ActivityWatch is installed
|
||||
check_installed() {
|
||||
# Check if activitywatch-bin package is installed
|
||||
if pacman -Qi activitywatch-bin &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if aw-qt binary exists
|
||||
if command -v aw-qt &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if ActivityWatch is running
|
||||
check_running() {
|
||||
# Check for aw-qt process
|
||||
if pgrep -f "aw-qt" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for aw-server process
|
||||
if pgrep -f "aw-server" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Main logic
|
||||
if ! check_installed; then
|
||||
echo "AW uninstalled"
|
||||
echo
|
||||
echo "#FF0000" # Red
|
||||
elif check_running; then
|
||||
echo "AW on"
|
||||
echo
|
||||
echo "#00FF00" # Green
|
||||
else
|
||||
echo "AW off"
|
||||
echo
|
||||
echo "#FF0000" # Red
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x "$status_script"
|
||||
|
||||
# Set proper ownership if running as root
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$status_script"
|
||||
fi
|
||||
|
||||
echo "✓ Created i3blocks status script: $status_script"
|
||||
|
||||
# Show configuration instructions
|
||||
echo ""
|
||||
echo "To add to your i3blocks config, add this block:"
|
||||
echo ""
|
||||
echo "[activitywatch]"
|
||||
echo "command=~/.config/i3blocks/activitywatch_status.sh"
|
||||
echo "interval=10"
|
||||
echo "color=#FFFFFF"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Function to test the setup
|
||||
test_setup() {
|
||||
echo ""
|
||||
echo "7. Testing Setup..."
|
||||
echo "=================="
|
||||
|
||||
echo "Installation status:"
|
||||
if check_activitywatch_installed >/dev/null 2>&1; then
|
||||
echo "✓ ActivityWatch is installed"
|
||||
else
|
||||
echo "✗ ActivityWatch is not installed"
|
||||
fi
|
||||
|
||||
echo "Running status:"
|
||||
if check_activitywatch_running >/dev/null 2>&1; then
|
||||
echo "✓ ActivityWatch is running"
|
||||
else
|
||||
echo "✗ ActivityWatch is not running"
|
||||
fi
|
||||
|
||||
echo "Autostart files:"
|
||||
if [[ -f "$USER_HOME/.config/autostart/activitywatch.desktop" ]]; then
|
||||
echo "✓ XDG autostart file exists"
|
||||
else
|
||||
echo "✗ XDG autostart file missing"
|
||||
fi
|
||||
|
||||
if [[ -f "$USER_HOME/.config/i3/config" ]] && grep -q "aw-qt" "$USER_HOME/.config/i3/config"; then
|
||||
echo "✓ i3 autostart configured"
|
||||
else
|
||||
echo "! i3 autostart may not be configured"
|
||||
fi
|
||||
|
||||
echo "i3blocks status script:"
|
||||
if [[ -x "$USER_HOME/.config/i3blocks/activitywatch_status.sh" ]]; then
|
||||
echo "✓ i3blocks status script created"
|
||||
echo "Testing status script:"
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
sudo -u "$ACTUAL_USER" "$USER_HOME/.config/i3blocks/activitywatch_status.sh"
|
||||
else
|
||||
"$USER_HOME/.config/i3blocks/activitywatch_status.sh"
|
||||
fi
|
||||
else
|
||||
echo "✗ i3blocks status script missing"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to show final instructions
|
||||
show_instructions() {
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "ActivityWatch Setup Complete"
|
||||
echo "=========================================="
|
||||
echo "Summary:"
|
||||
echo "✓ ActivityWatch installation checked/completed"
|
||||
echo "✓ ActivityWatch startup configured"
|
||||
echo "✓ Autostart configured (XDG + i3)"
|
||||
echo "✓ i3blocks status script created"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Add the i3blocks configuration to your config file:"
|
||||
echo " ~/.config/i3blocks/config"
|
||||
echo ""
|
||||
echo "2. Reload i3 configuration:"
|
||||
echo " Super+Shift+R"
|
||||
echo ""
|
||||
echo "3. ActivityWatch web interface should be available at:"
|
||||
echo " http://localhost:5600"
|
||||
echo ""
|
||||
echo "4. Check system tray for ActivityWatch icon"
|
||||
echo ""
|
||||
echo "Files created:"
|
||||
echo " ~/.config/autostart/activitywatch.desktop"
|
||||
echo " ~/.config/i3blocks/activitywatch_status.sh"
|
||||
echo " ~/.config/i3/config (modified)"
|
||||
echo ""
|
||||
}
|
||||
75
linux_configuration/scripts/single_use/features/lib/aw_install.sh
Executable file
75
linux_configuration/scripts/single_use/features/lib/aw_install.sh
Executable file
@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
# ActivityWatch install, both packaged and manual.
|
||||
#
|
||||
# Sourced by setup_activitywatch.sh; split out to keep it under the 250-line
|
||||
# cap. Sourced rather than run, so it inherits the caller's strict mode
|
||||
# and the variables defined above the source line.
|
||||
|
||||
# Function to install ActivityWatch
|
||||
install_activitywatch() {
|
||||
echo ""
|
||||
echo "2. Installing ActivityWatch..."
|
||||
echo "============================="
|
||||
|
||||
# Check if we need sudo for installation
|
||||
check_sudo "install"
|
||||
|
||||
echo "Installing activitywatch-bin from AUR..."
|
||||
|
||||
# Check if an AUR helper is available
|
||||
local aur_helpers=("yay" "paru" "makepkg")
|
||||
local helper_found=""
|
||||
|
||||
for helper in "${aur_helpers[@]}"; do
|
||||
if command -v "$helper" &>/dev/null; then
|
||||
helper_found="$helper"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n $helper_found && $helper_found != "makepkg" ]]; then
|
||||
echo "Using AUR helper: $helper_found"
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
# Running as root, need to install as user
|
||||
sudo -u "$ACTUAL_USER" "$helper_found" -S --noconfirm activitywatch-bin
|
||||
else
|
||||
"$helper_found" -S --noconfirm activitywatch-bin
|
||||
fi
|
||||
else
|
||||
echo "No AUR helper found. Installing manually with makepkg..."
|
||||
install_activitywatch_manual
|
||||
fi
|
||||
|
||||
echo "✓ ActivityWatch installation completed"
|
||||
}
|
||||
|
||||
# Function to manually install ActivityWatch via makepkg
|
||||
install_activitywatch_manual() {
|
||||
local temp_dir="/tmp/activitywatch-install"
|
||||
local original_user="$ACTUAL_USER"
|
||||
|
||||
# Create temp directory
|
||||
mkdir -p "$temp_dir"
|
||||
# Guarded: the next command is `git clone ... .`, which would otherwise
|
||||
# clone into whatever directory the caller happened to be in.
|
||||
cd "$temp_dir" || {
|
||||
print_error "Cannot enter $temp_dir"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Download PKGBUILD
|
||||
if command -v git &>/dev/null; then
|
||||
sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git .
|
||||
else
|
||||
echo "Installing git..."
|
||||
pacman -S --noconfirm git
|
||||
sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git .
|
||||
fi
|
||||
|
||||
# Build and install package
|
||||
sudo -u "$original_user" makepkg -si --noconfirm
|
||||
|
||||
# Cleanup
|
||||
cd /
|
||||
rm -rf "$temp_dir"
|
||||
}
|
||||
68
linux_configuration/scripts/single_use/features/lib/aw_service.sh
Executable file
68
linux_configuration/scripts/single_use/features/lib/aw_service.sh
Executable file
@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
# Service checks, startup and the autostart unit.
|
||||
#
|
||||
# Sourced by setup_activitywatch.sh; split out to keep it under the 250-line
|
||||
# cap. Sourced rather than run, so it inherits the caller's strict mode
|
||||
# and the variables defined above the source line.
|
||||
|
||||
# Function to check if ActivityWatch is running
|
||||
check_activitywatch_running() {
|
||||
echo ""
|
||||
echo "3. Checking ActivityWatch Status..."
|
||||
echo "=================================="
|
||||
|
||||
# Check for aw-qt process
|
||||
if pgrep -f "aw-qt" >/dev/null; then
|
||||
echo "✓ ActivityWatch (aw-qt) is running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for aw-server process
|
||||
if pgrep -f "aw-server" >/dev/null; then
|
||||
echo "✓ ActivityWatch server is running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "✗ ActivityWatch is not running"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to start ActivityWatch
|
||||
start_activitywatch() {
|
||||
echo ""
|
||||
echo "4. Starting ActivityWatch..."
|
||||
echo "==========================="
|
||||
|
||||
# Find aw-qt executable
|
||||
local aw_qt_path=""
|
||||
|
||||
if command -v aw-qt &>/dev/null; then
|
||||
aw_qt_path="$(which aw-qt)"
|
||||
elif [[ -x "/usr/bin/aw-qt" ]]; then
|
||||
aw_qt_path="/usr/bin/aw-qt"
|
||||
else
|
||||
echo "✗ Could not find aw-qt executable"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Starting ActivityWatch as user: $ACTUAL_USER"
|
||||
echo "Using aw-qt from: $aw_qt_path"
|
||||
|
||||
# Start as the actual user in the background
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
# Running as root, start as user
|
||||
sudo -u "$ACTUAL_USER" env DISPLAY=:0 "$aw_qt_path" &
|
||||
else
|
||||
# Running as user
|
||||
"$aw_qt_path" &
|
||||
fi
|
||||
|
||||
# Give it time to start
|
||||
sleep 3
|
||||
|
||||
if check_activitywatch_running >/dev/null 2>&1; then
|
||||
echo "✓ ActivityWatch started successfully"
|
||||
else
|
||||
echo "! ActivityWatch may be starting (check system tray)"
|
||||
fi
|
||||
}
|
||||
74
linux_configuration/scripts/single_use/features/lib/wg_firewall.sh
Executable file
74
linux_configuration/scripts/single_use/features/lib/wg_firewall.sh
Executable file
@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# nftables ruleset, its verification and sshd hardening.
|
||||
#
|
||||
# Sourced by setup_wireguard_ssh.sh; split out to keep wg_keys.sh under the
|
||||
# 250-line cap. Sourced rather than run, so it inherits the caller's
|
||||
# strict mode and the variables defined above the source line.
|
||||
|
||||
write_nftables_ruleset() {
|
||||
detect_lan_subnet
|
||||
if [[ -f $NFT_CONF ]]; then
|
||||
cp "$NFT_CONF" "${NFT_CONF}.bak.$(date +%s)"
|
||||
log_warn "Backed up existing ${NFT_CONF} before overwriting."
|
||||
fi
|
||||
local web_rule=""
|
||||
if [[ $ALLOW_WEB == "true" ]]; then
|
||||
web_rule=$'\n\t\ttcp dport { 80, 443 } accept'
|
||||
fi
|
||||
# DNS blocker (setup_dns_blocker.sh): serve DNS (53) -- and DHCP (67) when
|
||||
# this PC is the LAN DHCP server -- to LAN clients only. Restricted to the
|
||||
# LAN subnet so nothing is ever exposed to the internet.
|
||||
local dns_rule="" dhcp_rule=""
|
||||
if [[ $ALLOW_DNS == "true" ]]; then
|
||||
dns_rule=$'\n\t\tip saddr '"${LAN_SUBNET}"$' udp dport 53 accept'
|
||||
dns_rule+=$'\n\t\tip saddr '"${LAN_SUBNET}"$' tcp dport 53 accept'
|
||||
# DHCP clients have no IP yet (saddr 0.0.0.0 -> 255.255.255.255) and the
|
||||
# broadcast is classified INVALID by conntrack -- so this rule MUST sit
|
||||
# BEFORE 'ct state invalid drop' and match the client source port, not a
|
||||
# saddr. Link-local only: :67 broadcasts never route in from the internet.
|
||||
dhcp_rule=$'\n\t\tudp sport 68 udp dport 67 accept'
|
||||
fi
|
||||
cat >"${NFT_CONF}.new" <<EOF
|
||||
#!/usr/sbin/nft -f
|
||||
flush ruleset
|
||||
|
||||
table inet filter {
|
||||
chain input {
|
||||
type filter hook input priority 0; policy drop;
|
||||
|
||||
iif "lo" accept
|
||||
ct state established,related accept${dhcp_rule}
|
||||
ct state invalid drop
|
||||
|
||||
icmp type { destination-unreachable, time-exceeded, parameter-problem, echo-request } accept
|
||||
icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-neighbor-solicit, nd-neighbor-advert, echo-request } accept
|
||||
|
||||
udp dport ${WG_PORT} accept
|
||||
|
||||
iifname "${WG_IFACE}" tcp dport 22 accept
|
||||
ip saddr ${LAN_SUBNET} tcp dport 22 accept${web_rule}${dns_rule}
|
||||
}
|
||||
chain forward {
|
||||
type filter hook forward priority 0; policy drop;
|
||||
}
|
||||
chain output {
|
||||
type filter hook output priority 0; policy accept;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
verify_nftables_then_apply() {
|
||||
nft -c -f "${NFT_CONF}.new" || die "nftables ruleset failed syntax check -- not applying."
|
||||
mv "${NFT_CONF}.new" "$NFT_CONF"
|
||||
log_warn "Applying a default-drop firewall now."
|
||||
nft -f "$NFT_CONF"
|
||||
sleep 2
|
||||
if ! is_service_active sshd; then
|
||||
nft flush ruleset
|
||||
die "sshd died after applying nftables -- rolled back. Investigate before retrying."
|
||||
fi
|
||||
log_ok "nftables applied; sshd is still active."
|
||||
log_warn "Before closing this terminal, open a SECOND ssh session now and confirm it connects."
|
||||
enable_service nftables
|
||||
}
|
||||
42
linux_configuration/scripts/single_use/features/lib/wg_keys.sh
Executable file
42
linux_configuration/scripts/single_use/features/lib/wg_keys.sh
Executable file
@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# Key generation and the WireGuard service.
|
||||
#
|
||||
# Sourced by setup_wireguard_ssh.sh; split out to keep it under the 250-line
|
||||
# cap. Sourced rather than run, so it inherits the caller's strict mode
|
||||
# and the variables defined above the source line.
|
||||
|
||||
generate_server_keys() {
|
||||
ensure_dir "$WG_DIR"
|
||||
chmod 700 "$WG_DIR"
|
||||
if [[ -f "${WG_DIR}/server_private.key" ]]; then
|
||||
log_info "Server keypair already exists -- not rotating (would break existing peer configs)."
|
||||
return 0
|
||||
fi
|
||||
umask 077
|
||||
wg genkey | tee "${WG_DIR}/server_private.key" | wg pubkey >"${WG_DIR}/server_public.key"
|
||||
log_ok "Generated server keypair."
|
||||
}
|
||||
|
||||
write_wg0_conf() {
|
||||
if [[ -f $WG_CONF ]]; then
|
||||
log_info "${WG_CONF} already exists -- leaving peers intact, not regenerating."
|
||||
return 0
|
||||
fi
|
||||
local server_private_key
|
||||
server_private_key=$(<"${WG_DIR}/server_private.key")
|
||||
umask 077
|
||||
cat >"$WG_CONF" <<EOF
|
||||
[Interface]
|
||||
Address = ${WG_SERVER_IP}/24
|
||||
ListenPort = ${WG_PORT}
|
||||
PrivateKey = ${server_private_key}
|
||||
# No PostUp/PostDown NAT: this is a host-only tunnel, not a routed VPN.
|
||||
EOF
|
||||
chmod 600 "$WG_CONF"
|
||||
log_ok "Wrote ${WG_CONF}."
|
||||
}
|
||||
|
||||
enable_wg_service() {
|
||||
enable_service "wg-quick@${WG_IFACE}"
|
||||
log_ok "wg-quick@${WG_IFACE} enabled and started."
|
||||
}
|
||||
234
linux_configuration/scripts/single_use/features/lib/wg_sshd.sh
Executable file
234
linux_configuration/scripts/single_use/features/lib/wg_sshd.sh
Executable file
@ -0,0 +1,234 @@
|
||||
#!/bin/bash
|
||||
# sshd hardening and the DuckDNS record update.
|
||||
#
|
||||
# Sourced by setup_wireguard_ssh.sh; split out to keep wg_firewall.sh under the
|
||||
# 250-line cap. Sourced rather than run, so it inherits the caller's
|
||||
# strict mode and the variables defined above the source line.
|
||||
|
||||
harden_sshd() {
|
||||
log_warn "Before disabling password auth, confirm key-based login works."
|
||||
log_warn "In ANOTHER terminal, run: ssh -o PreferredAuthentications=publickey $(get_actual_user)@localhost echo ok"
|
||||
if ! ask_yes_no "Did key-based login succeed?"; then
|
||||
die "Aborting -- run ssh-copy-id to set up key auth first, then re-run setup."
|
||||
fi
|
||||
cat >"$SSHD_DROPIN" <<'EOF'
|
||||
# Managed by setup_wireguard_ssh.sh -- drop-in, does not touch sshd_config.
|
||||
PasswordAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
PermitRootLogin no
|
||||
EOF
|
||||
sshd -t || {
|
||||
rm -f "$SSHD_DROPIN"
|
||||
die "sshd config invalid after adding drop-in -- removed it."
|
||||
}
|
||||
systemctl reload sshd
|
||||
log_ok "sshd hardened: key-only auth, no root login."
|
||||
}
|
||||
|
||||
duckdns_already_updated() {
|
||||
local domain="$1" actual_user line script
|
||||
actual_user=$(get_actual_user)
|
||||
while IFS= read -r line; do
|
||||
[[ $line =~ ^[[:space:]]*(#|$) ]] && continue
|
||||
for script in $line; do
|
||||
[[ -f $script ]] || continue
|
||||
if grep -q "duckdns.org/update?domains=${domain}" "$script" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done < <(crontab -u "$actual_user" -l 2>/dev/null || true)
|
||||
return 1
|
||||
}
|
||||
|
||||
setup_duckdns() {
|
||||
if [[ -z $DUCKDNS_DOMAIN || -z $DUCKDNS_TOKEN ]]; then
|
||||
read -r -p "DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN
|
||||
read -r -p "DuckDNS token: " DUCKDNS_TOKEN
|
||||
[[ -n $DUCKDNS_DOMAIN && -n $DUCKDNS_TOKEN ]] || die "Both fields are required."
|
||||
save_config
|
||||
fi
|
||||
if duckdns_already_updated "$DUCKDNS_DOMAIN"; then
|
||||
log_info "An existing cron job already keeps ${DUCKDNS_DOMAIN}.duckdns.org updated -- not adding a duplicate."
|
||||
return 0
|
||||
fi
|
||||
ensure_dir "$DUCKDNS_DIR"
|
||||
cat >"${DUCKDNS_DIR}/duck.sh" <<EOF
|
||||
#!/bin/bash
|
||||
echo url="https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=" | curl -fsS -o "${DUCKDNS_DIR}/duck.log" -K -
|
||||
EOF
|
||||
chmod 700 "${DUCKDNS_DIR}/duck.sh"
|
||||
bash "${DUCKDNS_DIR}/duck.sh"
|
||||
(
|
||||
crontab -l 2>/dev/null || true
|
||||
) | grep -vF "${DUCKDNS_DIR}/duck.sh" | {
|
||||
cat
|
||||
echo "*/5 * * * * ${DUCKDNS_DIR}/duck.sh >/dev/null 2>&1"
|
||||
} | crontab -
|
||||
log_ok "DuckDNS configured: ${DUCKDNS_DOMAIN}.duckdns.org (refreshed every 5 min via cron)."
|
||||
}
|
||||
|
||||
next_free_wg_ip() {
|
||||
local used octet
|
||||
used=$(grep -oP 'AllowedIPs\s*=\s*10\.8\.0\.\K[0-9]+' "$WG_CONF" 2>/dev/null || true)
|
||||
for ((octet = 2; octet <= 254; octet++)); do
|
||||
if ! grep -qx "$octet" <<<"$used"; then
|
||||
echo "10.8.0.${octet}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
die "No free IPs left in ${WG_SUBNET}."
|
||||
}
|
||||
|
||||
add_phone_peer() {
|
||||
local name="${1:?usage: add-peer <name>}"
|
||||
[[ -f $WG_CONF ]] || die "Run 'setup' first -- ${WG_CONF} does not exist yet."
|
||||
[[ -n $DUCKDNS_DOMAIN ]] || die "DuckDNS domain not configured -- run 'setup' first."
|
||||
if grep -q "# peer:${name}\$" "$WG_CONF"; then
|
||||
die "A peer named '${name}' already exists in ${WG_CONF}. Use 'revoke ${name}' first to replace it."
|
||||
fi
|
||||
|
||||
ensure_dir "$WG_CLIENTS_DIR"
|
||||
chmod 700 "$WG_CLIENTS_DIR"
|
||||
|
||||
local tmpdir
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"; trap - RETURN' RETURN
|
||||
|
||||
umask 077
|
||||
wg genkey | tee "${tmpdir}/priv" | wg pubkey >"${tmpdir}/pub"
|
||||
local client_priv client_pub server_pub peer_ip
|
||||
client_priv=$(<"${tmpdir}/priv")
|
||||
client_pub=$(<"${tmpdir}/pub")
|
||||
server_pub=$(<"${WG_DIR}/server_public.key")
|
||||
peer_ip=$(next_free_wg_ip)
|
||||
|
||||
cat >>"$WG_CONF" <<EOF
|
||||
|
||||
[Peer] # peer:${name}
|
||||
PublicKey = ${client_pub}
|
||||
AllowedIPs = ${peer_ip}/32
|
||||
EOF
|
||||
if is_service_active "wg-quick@${WG_IFACE}"; then
|
||||
wg syncconf "$WG_IFACE" <(wg-quick strip "$WG_IFACE")
|
||||
fi
|
||||
|
||||
local client_conf="${WG_CLIENTS_DIR}/${name}.conf"
|
||||
cat >"$client_conf" <<EOF
|
||||
[Interface]
|
||||
PrivateKey = ${client_priv}
|
||||
Address = ${peer_ip}/32
|
||||
# No DNS server here on purpose: AllowedIPs below is scoped to the WireGuard
|
||||
# subnet only (host-only tunnel), so a DNS server outside that subnet would
|
||||
# be unreachable through the tunnel -- Android would then have no working
|
||||
# resolver at all while the tunnel is active, breaking DNS for everything.
|
||||
|
||||
[Peer]
|
||||
PublicKey = ${server_pub}
|
||||
Endpoint = ${DUCKDNS_DOMAIN}.duckdns.org:${WG_PORT}
|
||||
AllowedIPs = ${WG_SUBNET}
|
||||
PersistentKeepalive = 25
|
||||
EOF
|
||||
chmod 600 "$client_conf"
|
||||
log_ok "Phone config written to ${client_conf}"
|
||||
log_info "Scan this QR code with the WireGuard Android app:"
|
||||
qrencode -t ansiutf8 <"$client_conf"
|
||||
}
|
||||
|
||||
revoke_peer() {
|
||||
local name="${1:?usage: revoke <name>}"
|
||||
[[ -f $WG_CONF ]] || die "${WG_CONF} does not exist."
|
||||
grep -q "# peer:${name}\$" "$WG_CONF" || die "No peer named '${name}' found."
|
||||
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
trap 'rm -f "$tmp"; trap - RETURN' RETURN
|
||||
awk -v marker="# peer:${name}" '
|
||||
$0 ~ ("^\\[Peer\\].*" marker "$") { skip = 1; next }
|
||||
skip && /^$/ { skip = 0; next }
|
||||
skip { next }
|
||||
{ print }
|
||||
' "$WG_CONF" >"$tmp"
|
||||
cat "$tmp" >"$WG_CONF"
|
||||
chmod 600 "$WG_CONF"
|
||||
|
||||
if is_service_active "wg-quick@${WG_IFACE}"; then
|
||||
wg syncconf "$WG_IFACE" <(wg-quick strip "$WG_IFACE")
|
||||
fi
|
||||
rm -f "${WG_CLIENTS_DIR}/${name}.conf"
|
||||
log_ok "Revoked peer '${name}'."
|
||||
}
|
||||
|
||||
print_router_instructions() {
|
||||
local lan_ip
|
||||
lan_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}')
|
||||
cat <<EOF
|
||||
|
||||
=== Manual step: forward a port on your router (cannot be automated) ===
|
||||
1. Log into your router admin page (often http://192.168.1.1 or http://192.168.0.1).
|
||||
2. Find "Port Forwarding" / "Virtual Server" / "NAT" settings.
|
||||
3. Forward UDP port ${WG_PORT} -> ${lan_ip} (UDP only -- do NOT forward TCP/22).
|
||||
4. Save (and reboot the router if it requires it).
|
||||
5. Confirm your ISP gives you a real public IPv4 (not CGNAT):
|
||||
curl -s https://api.ipify.org
|
||||
getent hosts ${DUCKDNS_DOMAIN:-<your-domain>}.duckdns.org
|
||||
These two must match and must NOT be in 100.64.0.0/10 (CGNAT range).
|
||||
EOF
|
||||
}
|
||||
|
||||
print_android_instructions() {
|
||||
cat <<EOF
|
||||
|
||||
=== Manual step: install FOSS apps on your Android phone ===
|
||||
1. Install F-Droid (https://f-droid.org/) if you don't have it.
|
||||
2. From F-Droid, install "WireGuard" (official app, org.wireguard.android).
|
||||
3. From F-Droid, install "Termux" (then run: pkg install openssh) or "ConnectBot".
|
||||
4. Open WireGuard -> "+" -> "Scan from QR code" -> scan the code printed above.
|
||||
5. Toggle the tunnel on.
|
||||
6. From Termux/ConnectBot: ssh $(get_actual_user)@${WG_SERVER_IP}
|
||||
EOF
|
||||
}
|
||||
|
||||
allow_web() {
|
||||
export ALLOW_WEB=true
|
||||
save_config
|
||||
write_nftables_ruleset
|
||||
verify_nftables_then_apply
|
||||
log_ok "Opened tcp/80 and tcp/443 in the input chain (persisted -- future 'setup' re-runs keep this rule)."
|
||||
}
|
||||
|
||||
allow_dns() {
|
||||
export ALLOW_DNS=true
|
||||
save_config
|
||||
write_nftables_ruleset
|
||||
verify_nftables_then_apply
|
||||
log_ok "Opened udp/tcp 53 from ${LAN_SUBNET:-the LAN} in the input chain (persisted -- future 'setup' re-runs keep this rule)."
|
||||
}
|
||||
|
||||
status_cmd() {
|
||||
echo "=== WireGuard ==="
|
||||
wg show 2>/dev/null || echo "(interface not up)"
|
||||
echo
|
||||
echo "=== wg-quick@${WG_IFACE} service ==="
|
||||
systemctl status "wg-quick@${WG_IFACE}" --no-pager 2>/dev/null || echo "(not installed)"
|
||||
echo
|
||||
echo "=== nftables (input chain) ==="
|
||||
nft list ruleset 2>/dev/null | sed -n '/chain input/,/}/p' || echo "(nftables not active)"
|
||||
echo
|
||||
echo "=== sshd password auth ==="
|
||||
sshd -T 2>/dev/null | grep -i passwordauthentication || echo "(could not query sshd)"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 <command> [args]
|
||||
|
||||
Commands:
|
||||
setup Full first-time setup (WireGuard, firewall, sshd, DuckDNS).
|
||||
add-peer <name> Provision a new phone/laptop and print its QR code.
|
||||
allow-web Open tcp/80 and tcp/443 in the firewall (for a web server on this host).
|
||||
allow-dns Open udp/tcp 53 from the LAN in the firewall (for the DNS blocker on this host).
|
||||
status Show WireGuard/firewall/sshd status.
|
||||
revoke <name> Remove a peer.
|
||||
help Show this message.
|
||||
EOF
|
||||
}
|
||||
@ -60,366 +60,13 @@ check_activitywatch_installed() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to install ActivityWatch
|
||||
install_activitywatch() {
|
||||
echo ""
|
||||
echo "2. Installing ActivityWatch..."
|
||||
echo "============================="
|
||||
# shellcheck source=lib/aw_install.sh
|
||||
source "$SCRIPT_DIR/lib/aw_install.sh"
|
||||
# shellcheck source=lib/aw_service.sh
|
||||
source "$SCRIPT_DIR/lib/aw_service.sh"
|
||||
# shellcheck source=lib/aw_autostart.sh
|
||||
source "$SCRIPT_DIR/lib/aw_autostart.sh"
|
||||
|
||||
# Check if we need sudo for installation
|
||||
check_sudo "install"
|
||||
|
||||
echo "Installing activitywatch-bin from AUR..."
|
||||
|
||||
# Check if an AUR helper is available
|
||||
local aur_helpers=("yay" "paru" "makepkg")
|
||||
local helper_found=""
|
||||
|
||||
for helper in "${aur_helpers[@]}"; do
|
||||
if command -v "$helper" &>/dev/null; then
|
||||
helper_found="$helper"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n $helper_found && $helper_found != "makepkg" ]]; then
|
||||
echo "Using AUR helper: $helper_found"
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
# Running as root, need to install as user
|
||||
sudo -u "$ACTUAL_USER" "$helper_found" -S --noconfirm activitywatch-bin
|
||||
else
|
||||
"$helper_found" -S --noconfirm activitywatch-bin
|
||||
fi
|
||||
else
|
||||
echo "No AUR helper found. Installing manually with makepkg..."
|
||||
install_activitywatch_manual
|
||||
fi
|
||||
|
||||
echo "✓ ActivityWatch installation completed"
|
||||
}
|
||||
|
||||
# Function to manually install ActivityWatch via makepkg
|
||||
install_activitywatch_manual() {
|
||||
local temp_dir="/tmp/activitywatch-install"
|
||||
local original_user="$ACTUAL_USER"
|
||||
|
||||
# Create temp directory
|
||||
mkdir -p "$temp_dir"
|
||||
cd "$temp_dir"
|
||||
|
||||
# Download PKGBUILD
|
||||
if command -v git &>/dev/null; then
|
||||
sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git .
|
||||
else
|
||||
echo "Installing git..."
|
||||
pacman -S --noconfirm git
|
||||
sudo -u "$original_user" git clone https://aur.archlinux.org/activitywatch-bin.git .
|
||||
fi
|
||||
|
||||
# Build and install package
|
||||
sudo -u "$original_user" makepkg -si --noconfirm
|
||||
|
||||
# Cleanup
|
||||
cd /
|
||||
rm -rf "$temp_dir"
|
||||
}
|
||||
|
||||
# Function to check if ActivityWatch is running
|
||||
check_activitywatch_running() {
|
||||
echo ""
|
||||
echo "3. Checking ActivityWatch Status..."
|
||||
echo "=================================="
|
||||
|
||||
# Check for aw-qt process
|
||||
if pgrep -f "aw-qt" >/dev/null; then
|
||||
echo "✓ ActivityWatch (aw-qt) is running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for aw-server process
|
||||
if pgrep -f "aw-server" >/dev/null; then
|
||||
echo "✓ ActivityWatch server is running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "✗ ActivityWatch is not running"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to start ActivityWatch
|
||||
start_activitywatch() {
|
||||
echo ""
|
||||
echo "4. Starting ActivityWatch..."
|
||||
echo "==========================="
|
||||
|
||||
# Find aw-qt executable
|
||||
local aw_qt_path=""
|
||||
|
||||
if command -v aw-qt &>/dev/null; then
|
||||
aw_qt_path="$(which aw-qt)"
|
||||
elif [[ -x "/usr/bin/aw-qt" ]]; then
|
||||
aw_qt_path="/usr/bin/aw-qt"
|
||||
else
|
||||
echo "✗ Could not find aw-qt executable"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Starting ActivityWatch as user: $ACTUAL_USER"
|
||||
echo "Using aw-qt from: $aw_qt_path"
|
||||
|
||||
# Start as the actual user in the background
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
# Running as root, start as user
|
||||
sudo -u "$ACTUAL_USER" env DISPLAY=:0 "$aw_qt_path" &
|
||||
else
|
||||
# Running as user
|
||||
"$aw_qt_path" &
|
||||
fi
|
||||
|
||||
# Give it time to start
|
||||
sleep 3
|
||||
|
||||
if check_activitywatch_running >/dev/null 2>&1; then
|
||||
echo "✓ ActivityWatch started successfully"
|
||||
else
|
||||
echo "! ActivityWatch may be starting (check system tray)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to setup autostart
|
||||
setup_autostart() {
|
||||
echo ""
|
||||
echo "5. Setting Up Autostart..."
|
||||
echo "========================="
|
||||
|
||||
local autostart_dir="$USER_HOME/.config/autostart"
|
||||
local desktop_file="$autostart_dir/activitywatch.desktop"
|
||||
local i3_config="$USER_HOME/.config/i3/config"
|
||||
|
||||
# Method 1: XDG Autostart (works with most desktop environments)
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
sudo -u "$ACTUAL_USER" mkdir -p "$autostart_dir"
|
||||
else
|
||||
mkdir -p "$autostart_dir"
|
||||
fi
|
||||
|
||||
# Create desktop file for autostart
|
||||
cat >"$desktop_file" <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=ActivityWatch
|
||||
Comment=Automated time tracker
|
||||
Exec=aw-qt
|
||||
Icon=activitywatch
|
||||
Hidden=false
|
||||
NoDisplay=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
StartupNotify=false
|
||||
Terminal=false
|
||||
Categories=Utility;
|
||||
EOF
|
||||
|
||||
# Set proper ownership if running as root
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$desktop_file"
|
||||
fi
|
||||
|
||||
echo "✓ Created XDG autostart entry: $desktop_file"
|
||||
|
||||
# Method 2: i3 config autostart (specific to i3)
|
||||
if [[ -f $i3_config ]]; then
|
||||
# Check if autostart entry already exists
|
||||
if ! grep -q "aw-qt" "$i3_config"; then
|
||||
# Add autostart entry to i3 config
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
# Running as root
|
||||
sudo -u "$ACTUAL_USER" bash -c "cat <<'EOF' >> '$i3_config'
|
||||
|
||||
# Auto-start ActivityWatch
|
||||
exec --no-startup-id aw-qt
|
||||
EOF"
|
||||
else
|
||||
{
|
||||
printf '\n'
|
||||
printf '# Auto-start ActivityWatch\n'
|
||||
printf 'exec --no-startup-id aw-qt\n'
|
||||
} >>"$i3_config"
|
||||
fi
|
||||
|
||||
echo "✓ Added ActivityWatch to i3 config autostart"
|
||||
else
|
||||
echo "✓ ActivityWatch autostart already exists in i3 config"
|
||||
fi
|
||||
else
|
||||
echo "! i3 config not found at $i3_config"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create i3blocks status script
|
||||
create_i3blocks_status() {
|
||||
echo ""
|
||||
echo "6. Creating i3blocks Status Script..."
|
||||
echo "===================================="
|
||||
|
||||
local i3blocks_dir="$USER_HOME/.config/i3blocks"
|
||||
local status_script="$i3blocks_dir/activitywatch_status.sh"
|
||||
|
||||
# Create i3blocks directory if it doesn't exist
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
sudo -u "$ACTUAL_USER" mkdir -p "$i3blocks_dir"
|
||||
else
|
||||
mkdir -p "$i3blocks_dir"
|
||||
fi
|
||||
|
||||
# Create the status script
|
||||
cat >"$status_script" <<'EOF'
|
||||
#!/bin/bash
|
||||
# ActivityWatch status script for i3blocks
|
||||
# Shows ActivityWatch installation and running status
|
||||
|
||||
# Check if ActivityWatch is installed
|
||||
check_installed() {
|
||||
# Check if activitywatch-bin package is installed
|
||||
if pacman -Qi activitywatch-bin &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if aw-qt binary exists
|
||||
if command -v aw-qt &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if ActivityWatch is running
|
||||
check_running() {
|
||||
# Check for aw-qt process
|
||||
if pgrep -f "aw-qt" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for aw-server process
|
||||
if pgrep -f "aw-server" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Main logic
|
||||
if ! check_installed; then
|
||||
echo "AW uninstalled"
|
||||
echo
|
||||
echo "#FF0000" # Red
|
||||
elif check_running; then
|
||||
echo "AW on"
|
||||
echo
|
||||
echo "#00FF00" # Green
|
||||
else
|
||||
echo "AW off"
|
||||
echo
|
||||
echo "#FF0000" # Red
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x "$status_script"
|
||||
|
||||
# Set proper ownership if running as root
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$status_script"
|
||||
fi
|
||||
|
||||
echo "✓ Created i3blocks status script: $status_script"
|
||||
|
||||
# Show configuration instructions
|
||||
echo ""
|
||||
echo "To add to your i3blocks config, add this block:"
|
||||
echo ""
|
||||
echo "[activitywatch]"
|
||||
echo "command=~/.config/i3blocks/activitywatch_status.sh"
|
||||
echo "interval=10"
|
||||
echo "color=#FFFFFF"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Function to test the setup
|
||||
test_setup() {
|
||||
echo ""
|
||||
echo "7. Testing Setup..."
|
||||
echo "=================="
|
||||
|
||||
echo "Installation status:"
|
||||
if check_activitywatch_installed >/dev/null 2>&1; then
|
||||
echo "✓ ActivityWatch is installed"
|
||||
else
|
||||
echo "✗ ActivityWatch is not installed"
|
||||
fi
|
||||
|
||||
echo "Running status:"
|
||||
if check_activitywatch_running >/dev/null 2>&1; then
|
||||
echo "✓ ActivityWatch is running"
|
||||
else
|
||||
echo "✗ ActivityWatch is not running"
|
||||
fi
|
||||
|
||||
echo "Autostart files:"
|
||||
if [[ -f "$USER_HOME/.config/autostart/activitywatch.desktop" ]]; then
|
||||
echo "✓ XDG autostart file exists"
|
||||
else
|
||||
echo "✗ XDG autostart file missing"
|
||||
fi
|
||||
|
||||
if [[ -f "$USER_HOME/.config/i3/config" ]] && grep -q "aw-qt" "$USER_HOME/.config/i3/config"; then
|
||||
echo "✓ i3 autostart configured"
|
||||
else
|
||||
echo "! i3 autostart may not be configured"
|
||||
fi
|
||||
|
||||
echo "i3blocks status script:"
|
||||
if [[ -x "$USER_HOME/.config/i3blocks/activitywatch_status.sh" ]]; then
|
||||
echo "✓ i3blocks status script created"
|
||||
echo "Testing status script:"
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
sudo -u "$ACTUAL_USER" "$USER_HOME/.config/i3blocks/activitywatch_status.sh"
|
||||
else
|
||||
"$USER_HOME/.config/i3blocks/activitywatch_status.sh"
|
||||
fi
|
||||
else
|
||||
echo "✗ i3blocks status script missing"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to show final instructions
|
||||
show_instructions() {
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "ActivityWatch Setup Complete"
|
||||
echo "=========================================="
|
||||
echo "Summary:"
|
||||
echo "✓ ActivityWatch installation checked/completed"
|
||||
echo "✓ ActivityWatch startup configured"
|
||||
echo "✓ Autostart configured (XDG + i3)"
|
||||
echo "✓ i3blocks status script created"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Add the i3blocks configuration to your config file:"
|
||||
echo " ~/.config/i3blocks/config"
|
||||
echo ""
|
||||
echo "2. Reload i3 configuration:"
|
||||
echo " Super+Shift+R"
|
||||
echo ""
|
||||
echo "3. ActivityWatch web interface should be available at:"
|
||||
echo " http://localhost:5600"
|
||||
echo ""
|
||||
echo "4. Check system tray for ActivityWatch icon"
|
||||
echo ""
|
||||
echo "Files created:"
|
||||
echo " ~/.config/autostart/activitywatch.desktop"
|
||||
echo " ~/.config/i3blocks/activitywatch_status.sh"
|
||||
echo " ~/.config/i3/config (modified)"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main execution flow
|
||||
main() {
|
||||
|
||||
@ -37,8 +37,8 @@ fi
|
||||
DUCKDNS_DOMAIN="${DUCKDNS_DOMAIN:-}"
|
||||
DUCKDNS_TOKEN="${DUCKDNS_TOKEN:-}"
|
||||
LAN_SUBNET="${LAN_SUBNET:-}"
|
||||
ALLOW_WEB="${ALLOW_WEB:-false}"
|
||||
ALLOW_DNS="${ALLOW_DNS:-false}"
|
||||
export ALLOW_WEB="${ALLOW_WEB:-false}"
|
||||
export ALLOW_DNS="${ALLOW_DNS:-false}"
|
||||
|
||||
die() {
|
||||
log_error "$1"
|
||||
@ -75,337 +75,13 @@ install_dependencies() {
|
||||
install_missing_pacman_packages wireguard-tools qrencode nftables openssh
|
||||
}
|
||||
|
||||
generate_server_keys() {
|
||||
ensure_dir "$WG_DIR"
|
||||
chmod 700 "$WG_DIR"
|
||||
if [[ -f "${WG_DIR}/server_private.key" ]]; then
|
||||
log_info "Server keypair already exists -- not rotating (would break existing peer configs)."
|
||||
return 0
|
||||
fi
|
||||
umask 077
|
||||
wg genkey | tee "${WG_DIR}/server_private.key" | wg pubkey >"${WG_DIR}/server_public.key"
|
||||
log_ok "Generated server keypair."
|
||||
}
|
||||
# shellcheck source=lib/wg_keys.sh
|
||||
source "$SCRIPT_DIR/lib/wg_keys.sh"
|
||||
# shellcheck source=lib/wg_firewall.sh
|
||||
source "$SCRIPT_DIR/lib/wg_firewall.sh"
|
||||
# shellcheck source=lib/wg_sshd.sh
|
||||
source "$SCRIPT_DIR/lib/wg_sshd.sh"
|
||||
|
||||
write_wg0_conf() {
|
||||
if [[ -f $WG_CONF ]]; then
|
||||
log_info "${WG_CONF} already exists -- leaving peers intact, not regenerating."
|
||||
return 0
|
||||
fi
|
||||
local server_private_key
|
||||
server_private_key=$(<"${WG_DIR}/server_private.key")
|
||||
umask 077
|
||||
cat >"$WG_CONF" <<EOF
|
||||
[Interface]
|
||||
Address = ${WG_SERVER_IP}/24
|
||||
ListenPort = ${WG_PORT}
|
||||
PrivateKey = ${server_private_key}
|
||||
# No PostUp/PostDown NAT: this is a host-only tunnel, not a routed VPN.
|
||||
EOF
|
||||
chmod 600 "$WG_CONF"
|
||||
log_ok "Wrote ${WG_CONF}."
|
||||
}
|
||||
|
||||
enable_wg_service() {
|
||||
enable_service "wg-quick@${WG_IFACE}"
|
||||
log_ok "wg-quick@${WG_IFACE} enabled and started."
|
||||
}
|
||||
|
||||
write_nftables_ruleset() {
|
||||
detect_lan_subnet
|
||||
if [[ -f $NFT_CONF ]]; then
|
||||
cp "$NFT_CONF" "${NFT_CONF}.bak.$(date +%s)"
|
||||
log_warn "Backed up existing ${NFT_CONF} before overwriting."
|
||||
fi
|
||||
local web_rule=""
|
||||
if [[ $ALLOW_WEB == "true" ]]; then
|
||||
web_rule=$'\n\t\ttcp dport { 80, 443 } accept'
|
||||
fi
|
||||
# DNS blocker (setup_dns_blocker.sh): serve DNS (53) -- and DHCP (67) when
|
||||
# this PC is the LAN DHCP server -- to LAN clients only. Restricted to the
|
||||
# LAN subnet so nothing is ever exposed to the internet.
|
||||
local dns_rule="" dhcp_rule=""
|
||||
if [[ $ALLOW_DNS == "true" ]]; then
|
||||
dns_rule=$'\n\t\tip saddr '"${LAN_SUBNET}"$' udp dport 53 accept'
|
||||
dns_rule+=$'\n\t\tip saddr '"${LAN_SUBNET}"$' tcp dport 53 accept'
|
||||
# DHCP clients have no IP yet (saddr 0.0.0.0 -> 255.255.255.255) and the
|
||||
# broadcast is classified INVALID by conntrack -- so this rule MUST sit
|
||||
# BEFORE 'ct state invalid drop' and match the client source port, not a
|
||||
# saddr. Link-local only: :67 broadcasts never route in from the internet.
|
||||
dhcp_rule=$'\n\t\tudp sport 68 udp dport 67 accept'
|
||||
fi
|
||||
cat >"${NFT_CONF}.new" <<EOF
|
||||
#!/usr/sbin/nft -f
|
||||
flush ruleset
|
||||
|
||||
table inet filter {
|
||||
chain input {
|
||||
type filter hook input priority 0; policy drop;
|
||||
|
||||
iif "lo" accept
|
||||
ct state established,related accept${dhcp_rule}
|
||||
ct state invalid drop
|
||||
|
||||
icmp type { destination-unreachable, time-exceeded, parameter-problem, echo-request } accept
|
||||
icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-neighbor-solicit, nd-neighbor-advert, echo-request } accept
|
||||
|
||||
udp dport ${WG_PORT} accept
|
||||
|
||||
iifname "${WG_IFACE}" tcp dport 22 accept
|
||||
ip saddr ${LAN_SUBNET} tcp dport 22 accept${web_rule}${dns_rule}
|
||||
}
|
||||
chain forward {
|
||||
type filter hook forward priority 0; policy drop;
|
||||
}
|
||||
chain output {
|
||||
type filter hook output priority 0; policy accept;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
verify_nftables_then_apply() {
|
||||
nft -c -f "${NFT_CONF}.new" || die "nftables ruleset failed syntax check -- not applying."
|
||||
mv "${NFT_CONF}.new" "$NFT_CONF"
|
||||
log_warn "Applying a default-drop firewall now."
|
||||
nft -f "$NFT_CONF"
|
||||
sleep 2
|
||||
if ! is_service_active sshd; then
|
||||
nft flush ruleset
|
||||
die "sshd died after applying nftables -- rolled back. Investigate before retrying."
|
||||
fi
|
||||
log_ok "nftables applied; sshd is still active."
|
||||
log_warn "Before closing this terminal, open a SECOND ssh session now and confirm it connects."
|
||||
enable_service nftables
|
||||
}
|
||||
|
||||
harden_sshd() {
|
||||
log_warn "Before disabling password auth, confirm key-based login works."
|
||||
log_warn "In ANOTHER terminal, run: ssh -o PreferredAuthentications=publickey $(get_actual_user)@localhost echo ok"
|
||||
if ! ask_yes_no "Did key-based login succeed?"; then
|
||||
die "Aborting -- run ssh-copy-id to set up key auth first, then re-run setup."
|
||||
fi
|
||||
cat >"$SSHD_DROPIN" <<'EOF'
|
||||
# Managed by setup_wireguard_ssh.sh -- drop-in, does not touch sshd_config.
|
||||
PasswordAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
PermitRootLogin no
|
||||
EOF
|
||||
sshd -t || {
|
||||
rm -f "$SSHD_DROPIN"
|
||||
die "sshd config invalid after adding drop-in -- removed it."
|
||||
}
|
||||
systemctl reload sshd
|
||||
log_ok "sshd hardened: key-only auth, no root login."
|
||||
}
|
||||
|
||||
duckdns_already_updated() {
|
||||
local domain="$1" actual_user line script
|
||||
actual_user=$(get_actual_user)
|
||||
while IFS= read -r line; do
|
||||
[[ $line =~ ^[[:space:]]*(#|$) ]] && continue
|
||||
for script in $line; do
|
||||
[[ -f $script ]] || continue
|
||||
if grep -q "duckdns.org/update?domains=${domain}" "$script" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done < <(crontab -u "$actual_user" -l 2>/dev/null || true)
|
||||
return 1
|
||||
}
|
||||
|
||||
setup_duckdns() {
|
||||
if [[ -z $DUCKDNS_DOMAIN || -z $DUCKDNS_TOKEN ]]; then
|
||||
read -r -p "DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN
|
||||
read -r -p "DuckDNS token: " DUCKDNS_TOKEN
|
||||
[[ -n $DUCKDNS_DOMAIN && -n $DUCKDNS_TOKEN ]] || die "Both fields are required."
|
||||
save_config
|
||||
fi
|
||||
if duckdns_already_updated "$DUCKDNS_DOMAIN"; then
|
||||
log_info "An existing cron job already keeps ${DUCKDNS_DOMAIN}.duckdns.org updated -- not adding a duplicate."
|
||||
return 0
|
||||
fi
|
||||
ensure_dir "$DUCKDNS_DIR"
|
||||
cat >"${DUCKDNS_DIR}/duck.sh" <<EOF
|
||||
#!/bin/bash
|
||||
echo url="https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=" | curl -fsS -o "${DUCKDNS_DIR}/duck.log" -K -
|
||||
EOF
|
||||
chmod 700 "${DUCKDNS_DIR}/duck.sh"
|
||||
bash "${DUCKDNS_DIR}/duck.sh"
|
||||
(
|
||||
crontab -l 2>/dev/null || true
|
||||
) | grep -vF "${DUCKDNS_DIR}/duck.sh" | {
|
||||
cat
|
||||
echo "*/5 * * * * ${DUCKDNS_DIR}/duck.sh >/dev/null 2>&1"
|
||||
} | crontab -
|
||||
log_ok "DuckDNS configured: ${DUCKDNS_DOMAIN}.duckdns.org (refreshed every 5 min via cron)."
|
||||
}
|
||||
|
||||
next_free_wg_ip() {
|
||||
local used octet
|
||||
used=$(grep -oP 'AllowedIPs\s*=\s*10\.8\.0\.\K[0-9]+' "$WG_CONF" 2>/dev/null || true)
|
||||
for ((octet = 2; octet <= 254; octet++)); do
|
||||
if ! grep -qx "$octet" <<<"$used"; then
|
||||
echo "10.8.0.${octet}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
die "No free IPs left in ${WG_SUBNET}."
|
||||
}
|
||||
|
||||
add_phone_peer() {
|
||||
local name="${1:?usage: add-peer <name>}"
|
||||
[[ -f $WG_CONF ]] || die "Run 'setup' first -- ${WG_CONF} does not exist yet."
|
||||
[[ -n $DUCKDNS_DOMAIN ]] || die "DuckDNS domain not configured -- run 'setup' first."
|
||||
if grep -q "# peer:${name}\$" "$WG_CONF"; then
|
||||
die "A peer named '${name}' already exists in ${WG_CONF}. Use 'revoke ${name}' first to replace it."
|
||||
fi
|
||||
|
||||
ensure_dir "$WG_CLIENTS_DIR"
|
||||
chmod 700 "$WG_CLIENTS_DIR"
|
||||
|
||||
local tmpdir
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"; trap - RETURN' RETURN
|
||||
|
||||
umask 077
|
||||
wg genkey | tee "${tmpdir}/priv" | wg pubkey >"${tmpdir}/pub"
|
||||
local client_priv client_pub server_pub peer_ip
|
||||
client_priv=$(<"${tmpdir}/priv")
|
||||
client_pub=$(<"${tmpdir}/pub")
|
||||
server_pub=$(<"${WG_DIR}/server_public.key")
|
||||
peer_ip=$(next_free_wg_ip)
|
||||
|
||||
cat >>"$WG_CONF" <<EOF
|
||||
|
||||
[Peer] # peer:${name}
|
||||
PublicKey = ${client_pub}
|
||||
AllowedIPs = ${peer_ip}/32
|
||||
EOF
|
||||
if is_service_active "wg-quick@${WG_IFACE}"; then
|
||||
wg syncconf "$WG_IFACE" <(wg-quick strip "$WG_IFACE")
|
||||
fi
|
||||
|
||||
local client_conf="${WG_CLIENTS_DIR}/${name}.conf"
|
||||
cat >"$client_conf" <<EOF
|
||||
[Interface]
|
||||
PrivateKey = ${client_priv}
|
||||
Address = ${peer_ip}/32
|
||||
# No DNS server here on purpose: AllowedIPs below is scoped to the WireGuard
|
||||
# subnet only (host-only tunnel), so a DNS server outside that subnet would
|
||||
# be unreachable through the tunnel -- Android would then have no working
|
||||
# resolver at all while the tunnel is active, breaking DNS for everything.
|
||||
|
||||
[Peer]
|
||||
PublicKey = ${server_pub}
|
||||
Endpoint = ${DUCKDNS_DOMAIN}.duckdns.org:${WG_PORT}
|
||||
AllowedIPs = ${WG_SUBNET}
|
||||
PersistentKeepalive = 25
|
||||
EOF
|
||||
chmod 600 "$client_conf"
|
||||
log_ok "Phone config written to ${client_conf}"
|
||||
log_info "Scan this QR code with the WireGuard Android app:"
|
||||
qrencode -t ansiutf8 <"$client_conf"
|
||||
}
|
||||
|
||||
revoke_peer() {
|
||||
local name="${1:?usage: revoke <name>}"
|
||||
[[ -f $WG_CONF ]] || die "${WG_CONF} does not exist."
|
||||
grep -q "# peer:${name}\$" "$WG_CONF" || die "No peer named '${name}' found."
|
||||
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
trap 'rm -f "$tmp"; trap - RETURN' RETURN
|
||||
awk -v marker="# peer:${name}" '
|
||||
$0 ~ ("^\\[Peer\\].*" marker "$") { skip = 1; next }
|
||||
skip && /^$/ { skip = 0; next }
|
||||
skip { next }
|
||||
{ print }
|
||||
' "$WG_CONF" >"$tmp"
|
||||
cat "$tmp" >"$WG_CONF"
|
||||
chmod 600 "$WG_CONF"
|
||||
|
||||
if is_service_active "wg-quick@${WG_IFACE}"; then
|
||||
wg syncconf "$WG_IFACE" <(wg-quick strip "$WG_IFACE")
|
||||
fi
|
||||
rm -f "${WG_CLIENTS_DIR}/${name}.conf"
|
||||
log_ok "Revoked peer '${name}'."
|
||||
}
|
||||
|
||||
print_router_instructions() {
|
||||
local lan_ip
|
||||
lan_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}')
|
||||
cat <<EOF
|
||||
|
||||
=== Manual step: forward a port on your router (cannot be automated) ===
|
||||
1. Log into your router admin page (often http://192.168.1.1 or http://192.168.0.1).
|
||||
2. Find "Port Forwarding" / "Virtual Server" / "NAT" settings.
|
||||
3. Forward UDP port ${WG_PORT} -> ${lan_ip} (UDP only -- do NOT forward TCP/22).
|
||||
4. Save (and reboot the router if it requires it).
|
||||
5. Confirm your ISP gives you a real public IPv4 (not CGNAT):
|
||||
curl -s https://api.ipify.org
|
||||
getent hosts ${DUCKDNS_DOMAIN:-<your-domain>}.duckdns.org
|
||||
These two must match and must NOT be in 100.64.0.0/10 (CGNAT range).
|
||||
EOF
|
||||
}
|
||||
|
||||
print_android_instructions() {
|
||||
cat <<EOF
|
||||
|
||||
=== Manual step: install FOSS apps on your Android phone ===
|
||||
1. Install F-Droid (https://f-droid.org/) if you don't have it.
|
||||
2. From F-Droid, install "WireGuard" (official app, org.wireguard.android).
|
||||
3. From F-Droid, install "Termux" (then run: pkg install openssh) or "ConnectBot".
|
||||
4. Open WireGuard -> "+" -> "Scan from QR code" -> scan the code printed above.
|
||||
5. Toggle the tunnel on.
|
||||
6. From Termux/ConnectBot: ssh $(get_actual_user)@${WG_SERVER_IP}
|
||||
EOF
|
||||
}
|
||||
|
||||
allow_web() {
|
||||
ALLOW_WEB=true
|
||||
save_config
|
||||
write_nftables_ruleset
|
||||
verify_nftables_then_apply
|
||||
log_ok "Opened tcp/80 and tcp/443 in the input chain (persisted -- future 'setup' re-runs keep this rule)."
|
||||
}
|
||||
|
||||
allow_dns() {
|
||||
ALLOW_DNS=true
|
||||
save_config
|
||||
write_nftables_ruleset
|
||||
verify_nftables_then_apply
|
||||
log_ok "Opened udp/tcp 53 from ${LAN_SUBNET:-the LAN} in the input chain (persisted -- future 'setup' re-runs keep this rule)."
|
||||
}
|
||||
|
||||
status_cmd() {
|
||||
echo "=== WireGuard ==="
|
||||
wg show 2>/dev/null || echo "(interface not up)"
|
||||
echo
|
||||
echo "=== wg-quick@${WG_IFACE} service ==="
|
||||
systemctl status "wg-quick@${WG_IFACE}" --no-pager 2>/dev/null || echo "(not installed)"
|
||||
echo
|
||||
echo "=== nftables (input chain) ==="
|
||||
nft list ruleset 2>/dev/null | sed -n '/chain input/,/}/p' || echo "(nftables not active)"
|
||||
echo
|
||||
echo "=== sshd password auth ==="
|
||||
sshd -T 2>/dev/null | grep -i passwordauthentication || echo "(could not query sshd)"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 <command> [args]
|
||||
|
||||
Commands:
|
||||
setup Full first-time setup (WireGuard, firewall, sshd, DuckDNS).
|
||||
add-peer <name> Provision a new phone/laptop and print its QR code.
|
||||
allow-web Open tcp/80 and tcp/443 in the firewall (for a web server on this host).
|
||||
allow-dns Open udp/tcp 53 from the LAN in the firewall (for the DNS blocker on this host).
|
||||
status Show WireGuard/firewall/sshd status.
|
||||
revoke <name> Remove a peer.
|
||||
help Show this message.
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
local cmd="${1:-help}"
|
||||
|
||||
@ -0,0 +1,92 @@
|
||||
#!/bin/bash
|
||||
# System dependency and CUDA runtime installation.
|
||||
#
|
||||
# Sourced by transcribe.sh; split out to keep it under the 250-line
|
||||
# cap. Sourced rather than run, so it inherits the caller's strict mode
|
||||
# and the variables defined above the source line.
|
||||
|
||||
install_system_deps() {
|
||||
have_cmd() { command -v "$1" > /dev/null 2>&1; }
|
||||
local need_ffmpeg=0 need_espeak=0
|
||||
have_cmd ffmpeg || need_ffmpeg=1
|
||||
have_cmd espeak-ng || need_espeak=1
|
||||
|
||||
# If diarization requested and online, we may also try to ensure libsndfile
|
||||
local need_libsndfile=0
|
||||
if [[ ${FW_DIARIZE:-} == "1" ]]; then
|
||||
# Heuristic: check common library file
|
||||
if [[ ! -e /usr/lib/x86_64-linux-gnu/libsndfile.so && ! -e /usr/lib/libsndfile.so && ! -e /usr/lib64/libsndfile.so ]]; then
|
||||
need_libsndfile=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $need_ffmpeg -eq 0 && $need_espeak -eq 0 && $need_libsndfile -eq 0 ]]; then
|
||||
log "System deps present: ffmpeg, espeak-ng${FW_DIARIZE:+, libsndfile}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ $OFFLINE -eq 1 ]]; then
|
||||
echo "Missing system dependencies (ffmpeg/espeak-ng) but running in offline mode. Install them or rerun with --online." >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
local mgr
|
||||
mgr="$(detect_pkg_mgr)"
|
||||
log "Detected package manager: $mgr (installing missing: $([[ $need_ffmpeg -eq 1 ]] && echo ffmpeg)$([[ $need_espeak -eq 1 ]] && echo espeak-ng)$([[ $need_libsndfile -eq 1 ]] && echo libsndfile))"
|
||||
|
||||
if ! command -v sudo > /dev/null 2>&1; then
|
||||
log "sudo not found; skipping system package installation attempt."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Avoid exiting on install errors; continue best-effort
|
||||
set +e
|
||||
case "$mgr" in
|
||||
apt)
|
||||
sudo apt-get update -y || log "apt-get update failed; continuing"
|
||||
pkgs=(python3-venv python3-pip)
|
||||
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
|
||||
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
|
||||
if [[ $need_libsndfile -eq 1 ]]; then
|
||||
# Try both names across releases
|
||||
pkgs+=(libsndfile1)
|
||||
sudo apt-get install -y libsndfile1 || true
|
||||
# If that failed, try libsndfile2 (newer distros)
|
||||
sudo apt-get install -y libsndfile2 || true
|
||||
fi
|
||||
sudo apt-get install -y "${pkgs[@]}" || log "apt-get install failed; continuing"
|
||||
;;
|
||||
dnf)
|
||||
pkgs=(python3-venv python3-pip)
|
||||
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
|
||||
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
|
||||
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
|
||||
sudo dnf install -y "${pkgs[@]}" || log "dnf install failed; continuing"
|
||||
;;
|
||||
yum)
|
||||
pkgs=(python3-venv python3-pip)
|
||||
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
|
||||
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
|
||||
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
|
||||
sudo yum install -y "${pkgs[@]}" || log "yum install failed; continuing"
|
||||
;;
|
||||
pacman)
|
||||
pkgs=(python-virtualenv python-pip)
|
||||
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
|
||||
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
|
||||
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
|
||||
sudo pacman -Sy --noconfirm "${pkgs[@]}" || log "pacman install failed; continuing"
|
||||
;;
|
||||
zypper)
|
||||
pkgs=(python311-virtualenv python311-pip)
|
||||
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
|
||||
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
|
||||
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile1)
|
||||
sudo zypper install -y "${pkgs[@]}" || log "zypper install failed; continuing"
|
||||
;;
|
||||
*)
|
||||
log "Unknown package manager; please ensure ffmpeg and espeak-ng are installed."
|
||||
;;
|
||||
esac
|
||||
set -e
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
#!/bin/bash
|
||||
# Package manager detection and CUDA runtime checks.
|
||||
#
|
||||
# Sourced by transcribe.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and
|
||||
# the helper functions and variables defined above the source line.
|
||||
|
||||
detect_pkg_mgr() {
|
||||
if command -v apt-get > /dev/null 2>&1; then
|
||||
echo apt
|
||||
return
|
||||
fi
|
||||
if command -v dnf > /dev/null 2>&1; then
|
||||
echo dnf
|
||||
return
|
||||
fi
|
||||
if command -v yum > /dev/null 2>&1; then
|
||||
echo yum
|
||||
return
|
||||
fi
|
||||
if command -v pacman > /dev/null 2>&1; then
|
||||
echo pacman
|
||||
return
|
||||
fi
|
||||
if command -v zypper > /dev/null 2>&1; then
|
||||
echo zypper
|
||||
return
|
||||
fi
|
||||
echo none
|
||||
}
|
||||
|
||||
has_libcublas12() {
|
||||
# Common system locations
|
||||
for d in \
|
||||
/usr/lib \
|
||||
/usr/lib64 \
|
||||
/usr/local/cuda/lib64 \
|
||||
/usr/local/cuda-12*/lib64 \
|
||||
/opt/cuda/lib64 \
|
||||
/opt/cuda/targets/x86_64-linux/lib; do
|
||||
if [[ -e "$d/libcublas.so.12" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
# venv-provided NVIDIA CUDA libs
|
||||
if [[ -x "$VENV_DIR/bin/python" ]]; then
|
||||
local pyver
|
||||
pyver="$("$VENV_DIR"/bin/python "$PY_HELPERS" python-version 2> /dev/null || true)"
|
||||
if [[ -n $pyver ]]; then
|
||||
for d in "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib" \
|
||||
"$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib" \
|
||||
"$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib"; do
|
||||
if [[ -e "$d/libcublas.so.12" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_cuda_runtime() {
|
||||
local mgr
|
||||
mgr="$(detect_pkg_mgr)"
|
||||
if [[ $OFFLINE -eq 1 ]]; then
|
||||
if has_libcublas12; then return 0; fi
|
||||
echo "CUDA runtime (libcublas.so.12) not found and offline mode is enabled. Install CUDA 12 runtime or rerun with --online." >&2
|
||||
exit 6
|
||||
fi
|
||||
if has_libcublas12; then
|
||||
return 0
|
||||
fi
|
||||
if ! command -v sudo > /dev/null 2>&1; then
|
||||
log "sudo not found; skipping CUDA runtime install attempt."
|
||||
else
|
||||
log "CUDA cuBLAS 12 not found; attempting to install CUDA runtime (manager: $mgr)"
|
||||
set +e
|
||||
case "$mgr" in
|
||||
pacman)
|
||||
sudo pacman -Sy --noconfirm cuda cudnn || true
|
||||
;;
|
||||
apt)
|
||||
sudo apt-get update -y || true
|
||||
sudo apt-get install -y nvidia-cuda-toolkit || true
|
||||
;;
|
||||
dnf | yum)
|
||||
sudo "$mgr" install -y cuda cudnn || true
|
||||
;;
|
||||
zypper)
|
||||
sudo zypper install -y cuda cudnn || true
|
||||
;;
|
||||
*) log "Unknown package manager; cannot install CUDA automatically." ;;
|
||||
esac
|
||||
set -e
|
||||
fi
|
||||
# Re-check
|
||||
if ! has_libcublas12; then
|
||||
echo "CUDA runtime (libcublas.so.12) not found after attempted install. Please install CUDA 12 toolkit/runtime and re-run." >&2
|
||||
exit 6
|
||||
fi
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
#!/bin/bash
|
||||
# Virtualenv setup and the model download.
|
||||
#
|
||||
# Sourced by transcribe.sh; split out to keep it under the 250-line
|
||||
# cap. Sourced rather than run, so it inherits the caller's strict mode
|
||||
# and the variables defined above the source line.
|
||||
|
||||
setup_venv() {
|
||||
if [[ ! -d $VENV_DIR ]]; then
|
||||
log "Creating venv at $VENV_DIR"
|
||||
python3 -m venv "$VENV_DIR"
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source "$VENV_DIR/bin/activate"
|
||||
if [[ $OFFLINE -eq 0 ]]; then
|
||||
python -m pip install --upgrade pip wheel setuptools
|
||||
fi
|
||||
}
|
||||
|
||||
install_python_deps() {
|
||||
# Install deps; if NVIDIA GPU is present, prefer CUDA-capable stack (cu12)
|
||||
local has_nvidia_flag="${1:-0}"
|
||||
log "Installing faster-whisper and dependencies"
|
||||
export PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
export PIP_DEFAULT_TIMEOUT=${PIP_DEFAULT_TIMEOUT:-20}
|
||||
if [[ $OFFLINE -eq 1 ]]; then
|
||||
# Offline: do not install, just verify modules
|
||||
if ! python "$PY_HELPERS" check-faster-whisper; then
|
||||
exit 7
|
||||
fi
|
||||
# If diarization requested offline, check for its deps too (warn-only)
|
||||
if [[ ${FW_DIARIZE:-} == "1" ]]; then
|
||||
python "$PY_HELPERS" check-diarization || true
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [[ $has_nvidia_flag -eq 1 ]]; then
|
||||
# If ctranslate2 is not installed, attempt CUDA-enabled wheel (with fallback)
|
||||
if ! "$VENV_DIR/bin/python" "$PY_HELPERS" check-ctranslate2 2> /dev/null; then
|
||||
log "Installing CUDA-enabled CTranslate2 (cu12 wheel)"
|
||||
python -m pip install --progress-bar on --retries 1 --upgrade "ctranslate2<5,>=4.0" --extra-index-url https://download.opennmt.net/ctranslate2/cu12 ||
|
||||
log "Warning: could not reach cu12 wheel index; will proceed with available ctranslate2"
|
||||
fi
|
||||
# Ensure NVIDIA CUDA 12 runtime libs are available inside the venv
|
||||
python -m pip install --progress-bar on --retries 1 --upgrade nvidia-cublas-cu12 nvidia-cuda-runtime-cu12 nvidia-cudnn-cu12 ||
|
||||
log "Warning: failed to install NVIDIA cu12 runtime libs via pip"
|
||||
fi
|
||||
python -m pip install --progress-bar on --retries 1 --upgrade faster-whisper ffmpeg-python
|
||||
|
||||
# If diarization requested and online, install its Python deps best-effort
|
||||
if [[ ${FW_DIARIZE:-} == "1" ]]; then
|
||||
python -m pip install --progress-bar on --retries 1 --upgrade soundfile speechbrain ||
|
||||
log "Warning: failed to install soundfile/speechbrain"
|
||||
# Torch and torchaudio CPU wheels (force to avoid mismatched CUDA builds)
|
||||
python -m pip install --progress-bar on --retries 1 --upgrade --force-reinstall --index-url https://download.pytorch.org/whl/cpu torch torchaudio ||
|
||||
log "Warning: failed to install torch/torchaudio CPU wheels"
|
||||
fi
|
||||
python "$PY_HELPERS" deps-installed
|
||||
}
|
||||
|
||||
ensure_runner() {
|
||||
if [[ ! -f $PY_RUNNER ]]; then
|
||||
echo "Runner not found: $PY_RUNNER" >&2
|
||||
exit 3
|
||||
fi
|
||||
}
|
||||
|
||||
generate_test_audio() {
|
||||
local tmpwav
|
||||
tmpwav="${PROJECT_DIR}/test_fw.wav"
|
||||
if command -v espeak-ng > /dev/null 2>&1; then
|
||||
log "Generating test audio via espeak-ng -> $tmpwav" >&2
|
||||
espeak-ng -w "$tmpwav" "This is a quick test of faster whisper transcription." > /dev/null 2>&1 || true
|
||||
fi
|
||||
# If espeak-ng failed or not present, try espeak
|
||||
if [[ ! -s $tmpwav ]] && command -v espeak > /dev/null 2>&1; then
|
||||
log "espeak-ng unavailable or failed; trying espeak -> $tmpwav" >&2
|
||||
espeak -w "$tmpwav" "This is a quick test of faster whisper transcription." > /dev/null 2>&1 || true
|
||||
fi
|
||||
# Fallback: generate tone via Python stdlib (no external deps)
|
||||
if [[ ! -s $tmpwav ]]; then
|
||||
log "Generating 3s 1kHz WAV via Python stdlib -> $tmpwav" >&2
|
||||
python3 "$PY_HELPERS" generate-wav --file "$tmpwav" || true
|
||||
fi
|
||||
# Final fallback: tone via ffmpeg
|
||||
if [[ ! -s $tmpwav ]]; then
|
||||
log "Creating a 3s sine tone WAV via ffmpeg -> $tmpwav" >&2
|
||||
ffmpeg -f lavfi -i sine=frequency=1000:duration=3 -ar 16000 -ac 1 -f wav -y "$tmpwav" > /dev/null 2>&1 || true
|
||||
fi
|
||||
echo "$tmpwav"
|
||||
}
|
||||
|
||||
prepare_model() {
|
||||
# Download a model for offline use into MODEL_DIR
|
||||
local name="$1"
|
||||
mkdir -p "$MODEL_DIR"
|
||||
# shellcheck disable=SC1091
|
||||
source "$VENV_DIR/bin/activate"
|
||||
log "Preparing model '$name' into $MODEL_DIR"
|
||||
python "$PY_HELPERS" prepare-model --model "$name" --model-dir "$MODEL_DIR"
|
||||
}
|
||||
@ -34,281 +34,12 @@ log() {
|
||||
echo "[$(date +'%H:%M:%S')]" "$@"
|
||||
}
|
||||
|
||||
detect_pkg_mgr() {
|
||||
if command -v apt-get > /dev/null 2>&1; then
|
||||
echo apt
|
||||
return
|
||||
fi
|
||||
if command -v dnf > /dev/null 2>&1; then
|
||||
echo dnf
|
||||
return
|
||||
fi
|
||||
if command -v yum > /dev/null 2>&1; then
|
||||
echo yum
|
||||
return
|
||||
fi
|
||||
if command -v pacman > /dev/null 2>&1; then
|
||||
echo pacman
|
||||
return
|
||||
fi
|
||||
if command -v zypper > /dev/null 2>&1; then
|
||||
echo zypper
|
||||
return
|
||||
fi
|
||||
echo none
|
||||
}
|
||||
|
||||
has_libcublas12() {
|
||||
# Common system locations
|
||||
for d in \
|
||||
/usr/lib \
|
||||
/usr/lib64 \
|
||||
/usr/local/cuda/lib64 \
|
||||
/usr/local/cuda-12*/lib64 \
|
||||
/opt/cuda/lib64 \
|
||||
/opt/cuda/targets/x86_64-linux/lib; do
|
||||
if [[ -e "$d/libcublas.so.12" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
# venv-provided NVIDIA CUDA libs
|
||||
if [[ -x "$VENV_DIR/bin/python" ]]; then
|
||||
local pyver
|
||||
pyver="$("$VENV_DIR"/bin/python "$PY_HELPERS" python-version 2> /dev/null || true)"
|
||||
if [[ -n $pyver ]]; then
|
||||
for d in "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib" \
|
||||
"$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib" \
|
||||
"$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib"; do
|
||||
if [[ -e "$d/libcublas.so.12" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_cuda_runtime() {
|
||||
local mgr
|
||||
mgr="$(detect_pkg_mgr)"
|
||||
if [[ $OFFLINE -eq 1 ]]; then
|
||||
if has_libcublas12; then return 0; fi
|
||||
echo "CUDA runtime (libcublas.so.12) not found and offline mode is enabled. Install CUDA 12 runtime or rerun with --online." >&2
|
||||
exit 6
|
||||
fi
|
||||
if has_libcublas12; then
|
||||
return 0
|
||||
fi
|
||||
if ! command -v sudo > /dev/null 2>&1; then
|
||||
log "sudo not found; skipping CUDA runtime install attempt."
|
||||
else
|
||||
log "CUDA cuBLAS 12 not found; attempting to install CUDA runtime (manager: $mgr)"
|
||||
set +e
|
||||
case "$mgr" in
|
||||
pacman)
|
||||
sudo pacman -Sy --noconfirm cuda cudnn || true
|
||||
;;
|
||||
apt)
|
||||
sudo apt-get update -y || true
|
||||
sudo apt-get install -y nvidia-cuda-toolkit || true
|
||||
;;
|
||||
dnf | yum)
|
||||
sudo "$mgr" install -y cuda cudnn || true
|
||||
;;
|
||||
zypper)
|
||||
sudo zypper install -y cuda cudnn || true
|
||||
;;
|
||||
*) log "Unknown package manager; cannot install CUDA automatically." ;;
|
||||
esac
|
||||
set -e
|
||||
fi
|
||||
# Re-check
|
||||
if ! has_libcublas12; then
|
||||
echo "CUDA runtime (libcublas.so.12) not found after attempted install. Please install CUDA 12 toolkit/runtime and re-run." >&2
|
||||
exit 6
|
||||
fi
|
||||
}
|
||||
|
||||
install_system_deps() {
|
||||
have_cmd() { command -v "$1" > /dev/null 2>&1; }
|
||||
local need_ffmpeg=0 need_espeak=0
|
||||
have_cmd ffmpeg || need_ffmpeg=1
|
||||
have_cmd espeak-ng || need_espeak=1
|
||||
|
||||
# If diarization requested and online, we may also try to ensure libsndfile
|
||||
local need_libsndfile=0
|
||||
if [[ ${FW_DIARIZE:-} == "1" ]]; then
|
||||
# Heuristic: check common library file
|
||||
if [[ ! -e /usr/lib/x86_64-linux-gnu/libsndfile.so && ! -e /usr/lib/libsndfile.so && ! -e /usr/lib64/libsndfile.so ]]; then
|
||||
need_libsndfile=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $need_ffmpeg -eq 0 && $need_espeak -eq 0 && $need_libsndfile -eq 0 ]]; then
|
||||
log "System deps present: ffmpeg, espeak-ng${FW_DIARIZE:+, libsndfile}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ $OFFLINE -eq 1 ]]; then
|
||||
echo "Missing system dependencies (ffmpeg/espeak-ng) but running in offline mode. Install them or rerun with --online." >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
local mgr
|
||||
mgr="$(detect_pkg_mgr)"
|
||||
log "Detected package manager: $mgr (installing missing: $([[ $need_ffmpeg -eq 1 ]] && echo ffmpeg)$([[ $need_espeak -eq 1 ]] && echo espeak-ng)$([[ $need_libsndfile -eq 1 ]] && echo libsndfile))"
|
||||
|
||||
if ! command -v sudo > /dev/null 2>&1; then
|
||||
log "sudo not found; skipping system package installation attempt."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Avoid exiting on install errors; continue best-effort
|
||||
set +e
|
||||
case "$mgr" in
|
||||
apt)
|
||||
sudo apt-get update -y || log "apt-get update failed; continuing"
|
||||
pkgs=(python3-venv python3-pip)
|
||||
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
|
||||
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
|
||||
if [[ $need_libsndfile -eq 1 ]]; then
|
||||
# Try both names across releases
|
||||
pkgs+=(libsndfile1)
|
||||
sudo apt-get install -y libsndfile1 || true
|
||||
# If that failed, try libsndfile2 (newer distros)
|
||||
sudo apt-get install -y libsndfile2 || true
|
||||
fi
|
||||
sudo apt-get install -y "${pkgs[@]}" || log "apt-get install failed; continuing"
|
||||
;;
|
||||
dnf)
|
||||
pkgs=(python3-venv python3-pip)
|
||||
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
|
||||
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
|
||||
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
|
||||
sudo dnf install -y "${pkgs[@]}" || log "dnf install failed; continuing"
|
||||
;;
|
||||
yum)
|
||||
pkgs=(python3-venv python3-pip)
|
||||
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
|
||||
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
|
||||
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
|
||||
sudo yum install -y "${pkgs[@]}" || log "yum install failed; continuing"
|
||||
;;
|
||||
pacman)
|
||||
pkgs=(python-virtualenv python-pip)
|
||||
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
|
||||
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
|
||||
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
|
||||
sudo pacman -Sy --noconfirm "${pkgs[@]}" || log "pacman install failed; continuing"
|
||||
;;
|
||||
zypper)
|
||||
pkgs=(python311-virtualenv python311-pip)
|
||||
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
|
||||
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
|
||||
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile1)
|
||||
sudo zypper install -y "${pkgs[@]}" || log "zypper install failed; continuing"
|
||||
;;
|
||||
*)
|
||||
log "Unknown package manager; please ensure ffmpeg and espeak-ng are installed."
|
||||
;;
|
||||
esac
|
||||
set -e
|
||||
}
|
||||
|
||||
setup_venv() {
|
||||
if [[ ! -d $VENV_DIR ]]; then
|
||||
log "Creating venv at $VENV_DIR"
|
||||
python3 -m venv "$VENV_DIR"
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source "$VENV_DIR/bin/activate"
|
||||
if [[ $OFFLINE -eq 0 ]]; then
|
||||
python -m pip install --upgrade pip wheel setuptools
|
||||
fi
|
||||
}
|
||||
|
||||
install_python_deps() {
|
||||
# Install deps; if NVIDIA GPU is present, prefer CUDA-capable stack (cu12)
|
||||
local has_nvidia_flag="${1:-0}"
|
||||
log "Installing faster-whisper and dependencies"
|
||||
export PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
export PIP_DEFAULT_TIMEOUT=${PIP_DEFAULT_TIMEOUT:-20}
|
||||
if [[ $OFFLINE -eq 1 ]]; then
|
||||
# Offline: do not install, just verify modules
|
||||
if ! python "$PY_HELPERS" check-faster-whisper; then
|
||||
exit 7
|
||||
fi
|
||||
# If diarization requested offline, check for its deps too (warn-only)
|
||||
if [[ ${FW_DIARIZE:-} == "1" ]]; then
|
||||
python "$PY_HELPERS" check-diarization || true
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [[ $has_nvidia_flag -eq 1 ]]; then
|
||||
# If ctranslate2 is not installed, attempt CUDA-enabled wheel (with fallback)
|
||||
if ! "$VENV_DIR/bin/python" "$PY_HELPERS" check-ctranslate2 2> /dev/null; then
|
||||
log "Installing CUDA-enabled CTranslate2 (cu12 wheel)"
|
||||
python -m pip install --progress-bar on --retries 1 --upgrade "ctranslate2<5,>=4.0" --extra-index-url https://download.opennmt.net/ctranslate2/cu12 ||
|
||||
log "Warning: could not reach cu12 wheel index; will proceed with available ctranslate2"
|
||||
fi
|
||||
# Ensure NVIDIA CUDA 12 runtime libs are available inside the venv
|
||||
python -m pip install --progress-bar on --retries 1 --upgrade nvidia-cublas-cu12 nvidia-cuda-runtime-cu12 nvidia-cudnn-cu12 ||
|
||||
log "Warning: failed to install NVIDIA cu12 runtime libs via pip"
|
||||
fi
|
||||
python -m pip install --progress-bar on --retries 1 --upgrade faster-whisper ffmpeg-python
|
||||
|
||||
# If diarization requested and online, install its Python deps best-effort
|
||||
if [[ ${FW_DIARIZE:-} == "1" ]]; then
|
||||
python -m pip install --progress-bar on --retries 1 --upgrade soundfile speechbrain ||
|
||||
log "Warning: failed to install soundfile/speechbrain"
|
||||
# Torch and torchaudio CPU wheels (force to avoid mismatched CUDA builds)
|
||||
python -m pip install --progress-bar on --retries 1 --upgrade --force-reinstall --index-url https://download.pytorch.org/whl/cpu torch torchaudio ||
|
||||
log "Warning: failed to install torch/torchaudio CPU wheels"
|
||||
fi
|
||||
python "$PY_HELPERS" deps-installed
|
||||
}
|
||||
|
||||
ensure_runner() {
|
||||
if [[ ! -f $PY_RUNNER ]]; then
|
||||
echo "Runner not found: $PY_RUNNER" >&2
|
||||
exit 3
|
||||
fi
|
||||
}
|
||||
|
||||
generate_test_audio() {
|
||||
local tmpwav
|
||||
tmpwav="${PROJECT_DIR}/test_fw.wav"
|
||||
if command -v espeak-ng > /dev/null 2>&1; then
|
||||
log "Generating test audio via espeak-ng -> $tmpwav" >&2
|
||||
espeak-ng -w "$tmpwav" "This is a quick test of faster whisper transcription." > /dev/null 2>&1 || true
|
||||
fi
|
||||
# If espeak-ng failed or not present, try espeak
|
||||
if [[ ! -s $tmpwav ]] && command -v espeak > /dev/null 2>&1; then
|
||||
log "espeak-ng unavailable or failed; trying espeak -> $tmpwav" >&2
|
||||
espeak -w "$tmpwav" "This is a quick test of faster whisper transcription." > /dev/null 2>&1 || true
|
||||
fi
|
||||
# Fallback: generate tone via Python stdlib (no external deps)
|
||||
if [[ ! -s $tmpwav ]]; then
|
||||
log "Generating 3s 1kHz WAV via Python stdlib -> $tmpwav" >&2
|
||||
python3 "$PY_HELPERS" generate-wav --file "$tmpwav" || true
|
||||
fi
|
||||
# Final fallback: tone via ffmpeg
|
||||
if [[ ! -s $tmpwav ]]; then
|
||||
log "Creating a 3s sine tone WAV via ffmpeg -> $tmpwav" >&2
|
||||
ffmpeg -f lavfi -i sine=frequency=1000:duration=3 -ar 16000 -ac 1 -f wav -y "$tmpwav" > /dev/null 2>&1 || true
|
||||
fi
|
||||
echo "$tmpwav"
|
||||
}
|
||||
|
||||
prepare_model() {
|
||||
# Download a model for offline use into MODEL_DIR
|
||||
local name="$1"
|
||||
mkdir -p "$MODEL_DIR"
|
||||
# shellcheck disable=SC1091
|
||||
source "$VENV_DIR/bin/activate"
|
||||
log "Preparing model '$name' into $MODEL_DIR"
|
||||
python "$PY_HELPERS" prepare-model --model "$name" --model-dir "$MODEL_DIR"
|
||||
}
|
||||
# shellcheck source=lib/transcribe_pkgmgr.sh
|
||||
source "$SCRIPT_DIR/lib/transcribe_pkgmgr.sh"
|
||||
# shellcheck source=lib/transcribe_deps.sh
|
||||
source "$SCRIPT_DIR/lib/transcribe_deps.sh"
|
||||
# shellcheck source=lib/transcribe_venv.sh
|
||||
source "$SCRIPT_DIR/lib/transcribe_venv.sh"
|
||||
|
||||
main() {
|
||||
# Defaults
|
||||
|
||||
@ -27,13 +27,18 @@ tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
echo "--- $(basename "$script")"
|
||||
bash -n "$script" || exit 1
|
||||
shellcheck -x "$script" || exit 1
|
||||
for lib in "$dir"/lib/*.sh; do
|
||||
[[ -e $lib ]] || continue
|
||||
bash -n "$lib" || exit 1
|
||||
# bash -n first: a split landing inside a heredoc produces a file the
|
||||
# linter only warns about (SC1094 in the caller) but which bash rejects.
|
||||
bash -n "$lib" || {
|
||||
echo " $lib does not parse -- the split probably cut inside a heredoc" >&2
|
||||
exit 1
|
||||
}
|
||||
shellcheck "$lib" || exit 1
|
||||
done
|
||||
bash -n "$script" || exit 1
|
||||
shellcheck -x "$script" || exit 1
|
||||
|
||||
dollar='$'
|
||||
grep -q "source \"${dollar}SCRIPT_DIR/" "$script" || {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user