Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
*.cmd text eol=crlf
*.ps1 text eol=crlf
*.png binary
*.ico binary
71 changes: 71 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Pushing a version tag (v1.2.0) builds HogWatch.exe on a clean GitHub machine, tests
# it, and publishes it as a release download. The exe people download is therefore
# built from the public source at that tag, not on anyone's own PC.
name: release

on:
push:
tags: ["v*"]

permissions:
contents: write # to create the release

jobs:
release:
runs-on: windows-latest
timeout-minutes: 20
env:
TAG: ${{ github.ref_name }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: pip
cache-dependency-path: requirements-build.txt

- name: Install dependencies
run: python -m pip install -r requirements-build.txt

- name: The tag must match the app's version
shell: pwsh
run: |
$version = python -c "import hogwatch; print(hogwatch.__version__)"
if ("v$version" -ne $env:TAG) { throw "Tag $env:TAG does not match hogwatch.__version__ ($version)" }

- name: Tests
run: python -m unittest discover -s tests

- name: Build HogWatch.exe
run: cmd /c build-exe.cmd

- name: Run the exe
shell: pwsh
run: ./scripts/smoke_test_exe.ps1

- name: Checksum
shell: pwsh
run: |
$hash = (Get-FileHash dist/HogWatch.exe -Algorithm SHA256).Hash.ToLower()
"$hash HogWatch.exe" | Out-File dist/HogWatch.exe.sha256 -Encoding ascii
"SHA256=$hash" >> $env:GITHUB_ENV

- name: Publish the release
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$notes = @"
## Install

1. Download **HogWatch.exe** below and put it somewhere it can stay (for example a HogWatch folder in Documents).
2. Double-click it. Windows will warn that it's from an unknown publisher, because the exe isn't code-signed: click **More info**, then **Run anyway**.
3. Click **Yes** on the admin prompt (needed to see which program on the PC is using the internet). The dashboard opens in your browser.
4. On the dashboard, sign in under **Connect the eero**, and tick **Start HogWatch automatically** under HogWatch settings.

Your history and settings are stored on your own PC in ``%LOCALAPPDATA%\HogWatch``. Nothing is sent anywhere except the eero sign-in (to eero) and the daily email, if you set one up.

SHA-256 of HogWatch.exe: ``$env:SHA256``
"@
gh release create $env:TAG dist/HogWatch.exe dist/HogWatch.exe.sha256 --title "HogWatch $env:TAG" --notes $notes --generate-notes
27 changes: 27 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,30 @@ jobs:

- name: Dashboard script syntax
run: node --check hogwatch/static/app.js

# Builds HogWatch.exe and runs it, so a packaging problem shows up in the pull
# request rather than on release day.
package:
runs-on: windows-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: pip
cache-dependency-path: requirements-build.txt

- name: Build HogWatch.exe
run: cmd /c build-exe.cmd

- name: Run the exe
shell: pwsh
run: ./scripts/smoke_test_exe.ps1

- uses: actions/upload-artifact@v4
with:
name: HogWatch-exe
path: dist/HogWatch.exe
retention-days: 7
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
__pycache__/
# History, logs, and the eero login token live here -- never commit it.
data/
# PyInstaller output (build-exe.cmd). Releases are built by the release workflow.
build/
dist/
*.spec
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,31 @@ they're probably doing**, and shows it on a dashboard at <http://127.0.0.1:8765/

<sub>Screenshot uses demo data; device names and rooms are made up.</sub>

## Download (Windows)

**[Download HogWatch.exe](https://github.com/MSabacreationslab/hogwatch/releases/latest/download/HogWatch.exe)**
from the [latest release](https://github.com/MSabacreationslab/hogwatch/releases/latest). It needs nothing else installed.

1. Put the exe somewhere it can stay, such as a `HogWatch` folder in Documents.
2. Double-click it. Windows warns that it's from an unknown publisher, because the exe isn't
code-signed: click **More info**, then **Run anyway**.
3. Click **Yes** on the admin prompt. HogWatch needs it to see which program on the PC is using
the internet. The dashboard then opens in your browser.
4. On the dashboard, sign in under **Connect the eero** (your eero account's email or phone, then
the code eero sends you), and tick **Start HogWatch automatically** under *HogWatch settings*.

Good to know:

- Each release's exe is built by GitHub from the public source at that version, and the release
page lists its SHA-256 checksum.
- History and settings stay on your PC in `%LOCALAPPDATA%\HogWatch`. Logins (eero, email) are
stored encrypted so only your Windows account can read them.
- Double-clicking the exe again just opens the dashboard. **Stop HogWatch** is at the bottom of it.
- To remove it: untick *Start automatically*, click *Stop HogWatch*, then delete the exe and
the `%LOCALAPPDATA%\HogWatch` folder.

The rest of this page covers running from source and how it works.

It watches three things at once:

| What | How | Needs |
Expand All @@ -29,7 +54,9 @@ a **slowdown**. It also writes a plain-English explanation, for example:
It only names someone when they were actually using a big share of the line. If
nobody was, it says so and points at AT&T or the eero instead.

## Everyday use
## Everyday use (running from source)

Run `setup.cmd` once after cloning. `build-exe.cmd` builds `dist\HogWatch.exe` yourself.

| Double-click | To |
|---|---|
Expand Down
Binary file added assets/hogwatch.ico
Binary file not shown.
Binary file added assets/hogwatch.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions build-exe.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@echo off
rem Builds dist\HogWatch.exe: one file, no console window, the dashboard's files packed inside.
rem The GitHub workflows run this same script, so releases never depend on one PC's setup.
cd /d "%~dp0"
set PY=python
if exist ".venv\Scripts\python.exe" set PY=.venv\Scripts\python.exe
%PY% -m pip install --quiet --disable-pip-version-check -r requirements-build.txt || exit /b 1
%PY% -m PyInstaller --noconfirm --clean --onefile --noconsole --name HogWatch ^
--icon assets\hogwatch.ico --add-data "hogwatch\static;hogwatch\static" hogwatch_app.py || exit /b 1
echo.
echo Built dist\HogWatch.exe
2 changes: 1 addition & 1 deletion hogwatch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
web.py serves the local dashboard at http://127.0.0.1:8765
"""

__version__ = "1.0.0"
__version__ = "1.2.0"
104 changes: 84 additions & 20 deletions hogwatch/__main__.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
"""Command line:

python -m hogwatch run [--no-browser] start monitoring + dashboard (default)
python -m hogwatch eero-login connect to the eero (one time)
python -m hogwatch run [--no-browser] start monitoring + dashboard (default from source)
python -m hogwatch launch what double-clicking HogWatch.exe does: ask for admin
rights, start in the background, open the dashboard
python -m hogwatch stop stop a running HogWatch
python -m hogwatch eero-login connect to the eero (one time; also on the dashboard)
python -m hogwatch eero-check show what the eero reports right now
python -m hogwatch selftest 15-second check that everything works
python -m hogwatch send-report email the last 24 hours now (--preview: save it as HTML instead)
python -m hogwatch installer-report technical report for the eero installer (HTML + PDF in data\\)
python -m hogwatch installer-report technical report for the eero installer (HTML + PDF in the data folder)

Options before the command: --data-dir FOLDER, --port N (see config.py).
"""

from __future__ import annotations

import argparse
import ctypes
import json
import logging
import logging.handlers
import subprocess
import sys
import threading
import time
import webbrowser
import urllib.request
from pathlib import Path

from . import config, ping
from .config import DATA_DIR


def _is_admin() -> bool:
"""True when running elevated (needed for the per-program breakdown)."""
try:
return bool(ctypes.windll.shell32.IsUserAnAdmin())
except OSError:
return False
from .config import DATA_DIR, FROZEN, ROOT
from .winutil import is_admin as _is_admin
from .winutil import open_dashboard, run_elevated, server_up


def _setup_logging() -> None:
Expand Down Expand Up @@ -59,11 +59,12 @@ def cmd_run(args) -> int:
db_ref: dict = {}
reporter_ref: dict = {}
try:
server = make_server(cfg["port"], collector_ref, db_ref, on_shutdown=done.set, reporter_ref=reporter_ref)
server = make_server(cfg["port"], collector_ref, db_ref, on_shutdown=done.set, reporter_ref=reporter_ref,
eero_session=DATA_DIR / "eero_session.json")
except OSError:
log.info("HogWatch is already running -- opening the dashboard")
if not args.no_browser:
webbrowser.open(url)
open_dashboard(url)
return 0

db = DB(DATA_DIR / "hogwatch.db")
Expand All @@ -79,7 +80,7 @@ def cmd_run(args) -> int:
reporter_ref["r"] = reporter
threading.Thread(target=reporter.loop, name="report", daemon=True).start()
if not args.no_browser:
webbrowser.open(url)
open_dashboard(url)
try:
while not done.wait(1):
pass
Expand All @@ -91,6 +92,61 @@ def cmd_run(args) -> int:
return 0


def _global_options() -> list[str]:
"""--data-dir / --port as given on this command line, to pass on to a relaunched copy."""
out = []
for name in ("--data-dir", "--port"):
if name in sys.argv and sys.argv.index(name) + 1 < len(sys.argv):
out += [name, sys.argv[sys.argv.index(name) + 1]]
return out


def cmd_launch(args) -> int:
"""Double-click behaviour: make sure HogWatch is running in the background (as admin if
the user allows it), open the dashboard, and exit.

The background copy is started as administrator because Windows only shares
per-program network data with admin tools. This copy stays un-elevated so the
browser it opens isn't running as administrator.
"""
cfg = config.load()
port = int(cfg["port"])
url = f"http://127.0.0.1:{port}/"
if server_up(port):
open_dashboard(url)
return 0
if not _is_admin():
params = _global_options() + ["run", "--no-browser"]
if FROZEN:
exe, argv, cwd = sys.executable, params, str(Path(sys.executable).parent)
else:
pyw = Path(sys.executable).with_name("pythonw.exe")
exe, argv, cwd = str(pyw if pyw.exists() else sys.executable), ["-m", "hogwatch", *params], str(ROOT)
if run_elevated(exe, subprocess.list2cmdline(argv), cwd):
for _ in range(60): # the packaged exe unpacks itself first; allow it 30 seconds
if server_up(port, timeout=0.5):
break
time.sleep(0.5)
open_dashboard(url)
return 0
# The user said No to the admin prompt: run here without per-program detail.
args.no_browser = False
return cmd_run(args)


def cmd_stop(args) -> int:
"""Ask a running HogWatch to shut down cleanly."""
port = int(config.load()["port"])
req = urllib.request.Request(f"http://127.0.0.1:{port}/api/shutdown", data=b"{}", method="POST",
headers={"X-HogWatch": "1", "Content-Type": "application/json"})
try:
urllib.request.urlopen(req, timeout=5).close()
print("HogWatch stopped.")
except OSError:
print("HogWatch wasn't running.")
return 0


def cmd_eero_login(args) -> int:
"""Interactive one-time login. The user types their own login and code; we store only the session token."""
from .eero import Eero, EeroError
Expand Down Expand Up @@ -245,9 +301,14 @@ def cmd_installer_report(args) -> int:
def main() -> int:
"""Parse the command and run it."""
p = argparse.ArgumentParser(prog="hogwatch", description="Find out who is slowing the internet down.")
# Read early by config.py (they decide where config lives); declared here so argparse accepts them.
p.add_argument("--data-dir", help="folder for history, settings and logs")
p.add_argument("--port", type=int, help="dashboard port (default 8765)")
sub = p.add_subparsers(dest="cmd")
r = sub.add_parser("run", help="start monitoring and the dashboard")
r.add_argument("--no-browser", action="store_true", help="don't open the dashboard")
sub.add_parser("launch", help="start in the background as administrator and open the dashboard")
sub.add_parser("stop", help="stop a running HogWatch")
sub.add_parser("eero-login", help="connect to the eero (one time)")
sub.add_parser("eero-check", help="show what the eero reports right now")
st = sub.add_parser("selftest", help="15-second check that everything works")
Expand All @@ -260,10 +321,13 @@ def main() -> int:
ir.add_argument("--note", help="notes from the homeowner to include")
args = p.parse_args()
if args.cmd is None:
args = p.parse_args(["run"])
# Double-clicking the exe gives no command: do the friendly thing. From source,
# plain `python -m hogwatch` keeps its original meaning (run in this window).
args.cmd = "launch" if FROZEN else "run"
args.no_browser = False
DATA_DIR.mkdir(parents=True, exist_ok=True)
return {"run": cmd_run, "eero-login": cmd_eero_login, "eero-check": cmd_eero_check,
"selftest": cmd_selftest, "send-report": cmd_send_report,
return {"run": cmd_run, "launch": cmd_launch, "stop": cmd_stop, "eero-login": cmd_eero_login,
"eero-check": cmd_eero_check, "selftest": cmd_selftest, "send-report": cmd_send_report,
"installer-report": cmd_installer_report}[args.cmd](args)


Expand Down
Loading
Loading