Skip to content
Closed
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
125 changes: 125 additions & 0 deletions .github/workflows/ci-linux.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: CI (Linux)

on:
push:
branches: [main]
paths:
- 'platforms/linux/**'
- 'portkiller-core/**'
- '.github/workflows/ci-linux.yml'
tags-ignore:
- 'v*'
pull_request:
branches: [main]
paths:
- 'platforms/linux/**'
- 'portkiller-core/**'
- '.github/workflows/ci-linux.yml'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
rust:
name: Rust Core (Linux)
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt

- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
portkiller-core/target
key: ${{ runner.os }}-cargo-${{ hashFiles('portkiller-core/Cargo.toml') }}
restore-keys: |
${{ runner.os }}-cargo-

- name: Install scan tools
# The scanner shells out to these; without them the integration
# tests fail rather than exercising the real code paths.
run: sudo apt-get update -qq && sudo apt-get install -y -qq procps iproute2 lsof

- name: Build
working-directory: portkiller-core
run: cargo build --verbose

- name: Test
working-directory: portkiller-core
run: cargo test --verbose

- name: Clippy
working-directory: portkiller-core
run: cargo clippy -- -D warnings

- name: Format check
working-directory: portkiller-core
run: cargo fmt --check

python:
name: Linux Tray App
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
python-version: ['3.9', '3.12']

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Syntax check
run: python3 -m compileall -q platforms/linux/src platforms/linux/port-killer.py

- name: Run parser tests
# These cover pure string parsing only, so no GTK stack is needed.
run: python3 -m unittest discover -s platforms/linux/tests -v

- name: Validate installer
run: |
bash -n platforms/linux/install.sh
sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
shellcheck platforms/linux/install.sh

smoke:
name: Import Smoke Test
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install GTK stack
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq \
python3-gi python3-gi-cairo gir1.2-gtk-3.0 \
gir1.2-ayatanaappindicator3-0.1 xvfb

- name: Import all modules
# Catches broken imports and syntax errors that the parser tests,
# which never touch the GTK modules, would miss.
run: |
cd platforms/linux
xvfb-run -a python3 -c "
import src.config, src.scanner
import src.services.clipboard, src.services.cloudflare, src.services.k8s
import src.ui.dialogs, src.ui.window, src.ui.tray
print('All modules import cleanly')
"
120 changes: 120 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -448,3 +448,123 @@ jobs:
$version = "${{ env.RELEASE_VERSION }}"
gh release upload $version "PortKiller-${version}-windows-x64.zip" --clobber
gh release upload $version "PortKiller-${version}-windows-arm64.zip" --clobber

build-linux:
name: Build Linux
needs: create-release
# Skip for PR builds (macOS only)
if: ${{ always() && !inputs.is_pr_build && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') }}
runs-on: ubuntu-22.04

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install build dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq \
python3 python3-gi python3-gi-cairo \
gir1.2-gtk-3.0 gir1.2-ayatanaappindicator3-0.1 \
librsvg2-bin desktop-file-utils file

- name: Build AppImage
run: |
VERSION="${{ env.RELEASE_VERSION }}"
VERSION="${VERSION#v}"
APPDIR=AppDir

# Lay out the AppDir
mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/share/port-killer"
mkdir -p "$APPDIR/usr/share/applications"
mkdir -p "$APPDIR/usr/share/icons/hicolor/scalable/apps"

cp -r platforms/linux/src "$APPDIR/usr/share/port-killer/"
cp platforms/linux/port-killer.py "$APPDIR/usr/share/port-killer/"
chmod +x "$APPDIR/usr/share/port-killer/port-killer.py"

# Icon: AppImage needs it at the AppDir root as well as in the theme
cp platforms/macos/Resources/AppIcon.svg "$APPDIR/usr/share/icons/hicolor/scalable/apps/port-killer.svg"
cp platforms/macos/Resources/AppIcon.svg "$APPDIR/port-killer.svg"
# Some desktop environments will not read an SVG-only AppImage icon
rsvg-convert -w 256 -h 256 platforms/macos/Resources/AppIcon.svg -o "$APPDIR/port-killer.png"

cat > "$APPDIR/usr/share/applications/port-killer.desktop" <<'DESKTOP'
[Desktop Entry]
Type=Application
Name=PortKiller
Comment=Monitor listening ports and terminate processes from system tray
Exec=port-killer
Icon=port-killer
Terminal=false
Categories=Development;
StartupNotify=false
StartupWMClass=port-killer
DESKTOP
cp "$APPDIR/usr/share/applications/port-killer.desktop" "$APPDIR/port-killer.desktop"
desktop-file-validate "$APPDIR/port-killer.desktop"

# Launcher. The GTK stack is taken from the host rather than bundled:
# PyGObject binds tightly to the system GLib/GTK, and bundling them
# breaks on distros whose versions differ from the build image.
cat > "$APPDIR/usr/bin/port-killer" <<'LAUNCH'
#!/bin/bash
HERE="$(dirname "$(readlink -f "${0}")")"
APPROOT="$(dirname "$(dirname "$HERE")")"

for dep in "gi:python3-gi" "gi.repository.Gtk:gir1.2-gtk-3.0"; do
mod="${dep%%:*}"
if ! python3 -c "import ${mod%%.*}" 2>/dev/null; then
echo "PortKiller: missing ${dep##*:}." >&2
echo "Install the GTK Python bindings, e.g.:" >&2
echo " Debian/Ubuntu: sudo apt install python3-gi gir1.2-ayatanaappindicator3-0.1" >&2
echo " Fedora: sudo dnf install python3-gobject libayatana-appindicator-gtk3" >&2
echo " Arch: sudo pacman -S python-gobject libayatana-appindicator" >&2
exit 1
fi
done

exec python3 "$APPROOT/usr/share/port-killer/port-killer.py" "$@"
LAUNCH
chmod +x "$APPDIR/usr/bin/port-killer"

# AppRun -> launcher
ln -s usr/bin/port-killer "$APPDIR/AppRun"

# Package
curl -fsSL -o appimagetool \
"https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x appimagetool

ARCH=x86_64 ./appimagetool --appimage-extract-and-run \
"$APPDIR" "PortKiller-${{ env.RELEASE_VERSION }}-linux-x86_64.AppImage"

- name: Verify AppImage
run: |
FILE="PortKiller-${{ env.RELEASE_VERSION }}-linux-x86_64.AppImage"
[ -f "$FILE" ] || { echo "AppImage not produced"; exit 1; }
chmod +x "$FILE"
file "$FILE"

# Confirm the payload is actually in there
./"$FILE" --appimage-extract >/dev/null
[ -f squashfs-root/usr/share/port-killer/port-killer.py ] || { echo "Missing entry point"; exit 1; }
[ -f squashfs-root/usr/share/port-killer/src/ui/tray.py ] || { echo "Missing src/"; exit 1; }
[ -x squashfs-root/AppRun ] || { echo "AppRun not executable"; exit 1; }
rm -rf squashfs-root
echo "AppImage verified"

- name: Upload to Release
if: ${{ !inputs.dry_run }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "${{ env.RELEASE_VERSION }}" \
"PortKiller-${{ env.RELEASE_VERSION }}-linux-x86_64.AppImage" --clobber

- name: Upload artifact (dry run)
if: ${{ inputs.dry_run }}
uses: actions/upload-artifact@v4
with:
name: linux-appimage
path: PortKiller-*-linux-x86_64.AppImage
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,12 @@ bun.lockb

# Rust
**/target/

# Python (Linux tray app)
__pycache__/
*.py[cod]

# Linux packaging
AppDir/
*.AppImage
appimagetool
79 changes: 76 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

## Requirements

- **macOS 15.0+** / **Windows 10+**
- **macOS 15.0+** / **Windows 10+** / **Linux**
- **Xcode 16+** with Swift 6.0 (for macOS)
- **.NET 9 SDK** (for Windows)
- **Python 3.9+**, PyGObject (GTK 3) and libayatana-appindicator (for Linux)
- **Rust stable** (for `portkiller-core`, used by the Linux app only)

## Setup

Expand Down Expand Up @@ -37,6 +39,32 @@ cd platforms/windows/PortKiller
dotnet run
```

### Linux

A native system tray app built with Python, GTK 3 and AppIndicator.

```bash
# Run directly
./platforms/linux/port-killer.py &

# Or install it (registers a launcher and autostart on login)
./platforms/linux/install.sh
```

Install the dependencies first — `install.sh` checks for them and stops with
hints if any are missing:

```bash
# Debian/Ubuntu
sudo apt install python3 python3-gi gir1.2-ayatanaappindicator3-0.1

# Fedora
sudo dnf install python3 python3-gobject libayatana-appindicator-gtk3

# Arch
sudo pacman -S python python-gobject libayatana-appindicator
```

## Building

### macOS
Expand All @@ -56,6 +84,33 @@ dotnet build # Debug
dotnet publish -c Release -r win-x64 # Release
```

### Linux

The tray app is plain Python, so there is no build step. The Rust core is
built separately:

```bash
cd portkiller-core
cargo build # Debug
cargo build --release # Release
```

Releases ship an AppImage, produced by the `build-linux` job in
`.github/workflows/release.yml`.

## Tests

```bash
# macOS
cd platforms/macos && swift test

# Linux tray app (parser tests, no GTK needed)
python3 -m unittest discover -s platforms/linux/tests

# Rust core (Linux only)
cd portkiller-core && cargo test
```

## Pull Requests

1. Fork the repo
Expand All @@ -76,6 +131,11 @@ dotnet publish -c Release -r win-x64 # Release
- C# with WPF
- MVVM pattern

### Linux
- Python 3 with GTK 3 (PyGObject)
- Scans run on a worker thread; UI updates go back through `GLib.idle_add`
- Parsers are pure functions, kept testable without a GTK stack

## Project Structure

```
Expand All @@ -88,6 +148,19 @@ platforms/
│ │ └── Views/ # SwiftUI views
│ ├── Resources/ # Assets, Info.plist
│ └── scripts/ # Build scripts
└── windows/
└── PortKiller/ # .NET WPF project
├── windows/
│ └── PortKiller/ # .NET WPF project
└── linux/
├── port-killer.py # Entry point
├── install.sh # Launcher + autostart installer
├── src/
│ ├── scanner.py # ss/lsof parsing, process killing
│ ├── config.py # Persisted preferences
│ ├── services/ # Clipboard, Cloudflare, k8s
│ └── ui/ # Tray, window, dialogs
└── tests/ # Parser tests

portkiller-core/ # Rust core (Linux only)
├── src/scanner/ # Port scanning
└── src/process/ # Process termination
```
Loading
Loading