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
78 changes: 78 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: CI

on:
push:
branches:
- master
pull_request:
workflow_dispatch:

jobs:
test-macos:
name: Test on macOS
runs-on: macos-15
env:
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Show toolchain
run: |
xcodebuild -version
swift --version

- name: Build
run: swift build

- name: Test
run: swift test

- name: Build CLI demo
run: swift build --package-path Examples/ssdp-demo

build-ios:
name: Build for iOS
runs-on: macos-15
env:
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Show toolchain
run: |
xcodebuild -version
swift --version

- name: Build for iOS Simulator
run: |
xcodebuild build \
-scheme SwiftSSDP \
-destination 'generic/platform=iOS Simulator' \
-configuration Debug \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify --renderer github-actions
shell: bash

build-tvos:
name: Build for tvOS
runs-on: macos-15
env:
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Build for tvOS Simulator
run: |
xcodebuild build \
-scheme SwiftSSDP \
-destination 'generic/platform=tvOS Simulator' \
-configuration Debug \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify --renderer github-actions
shell: bash
121 changes: 121 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
name: Release

on:
workflow_dispatch:
inputs:
version:
description: Release version without the v prefix (e.g. 2.0.0)
required: true
type: string
notes:
description: Release notes for GitHub Releases (optional — auto-generated if blank)
required: false
type: string

permissions:
contents: write

jobs:
release:
runs-on: macos-15
env:
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer

steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Validate version format
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
if [[ ! "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]]; then
echo "::error::Version must be SemVer (e.g. 2.0.0 or 2.0.0-beta.1). Got: $INPUT_VERSION"
exit 1
fi
echo "TAG=v${INPUT_VERSION}" >> "$GITHUB_ENV"

- name: Verify tag does not already exist
env:
TAG: ${{ env.TAG }}
run: |
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "::error::Tag $TAG already exists locally."
exit 1
fi
if git ls-remote --tags origin "$TAG" | grep -q "refs/tags/$TAG"; then
echo "::error::Tag $TAG already exists on origin."
exit 1
fi

- name: Show toolchain
run: |
xcodebuild -version
swift --version

- name: Build
run: swift build

- name: Test
run: swift test

- name: Update SwiftSSDP.version constant
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
# Rewrite the version literal in Sources/SwiftSSDP/SwiftSSDP.swift.
# The constant is `public static let version = "X.Y.Z"`; we replace the
# quoted literal in place. Using a delimiter that won't appear in version
# strings keeps sed simple.
sed -i.bak -E \
"s/public static let version = \"[^\"]+\"/public static let version = \"${INPUT_VERSION}\"/" \
Sources/SwiftSSDP/SwiftSSDP.swift
rm -f Sources/SwiftSSDP/SwiftSSDP.swift.bak

# Verify the change actually landed.
if ! grep -q "public static let version = \"${INPUT_VERSION}\"" Sources/SwiftSSDP/SwiftSSDP.swift; then
echo "::error::Failed to update version constant."
cat Sources/SwiftSSDP/SwiftSSDP.swift
exit 1
fi

- name: Re-build with updated version
run: swift build

- name: Commit version bump
env:
TAG: ${{ env.TAG }}
INPUT_VERSION: ${{ inputs.version }}
run: |
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'

if git diff --quiet Sources/SwiftSSDP/SwiftSSDP.swift; then
echo "Version constant already at ${INPUT_VERSION} — nothing to commit."
else
git add Sources/SwiftSSDP/SwiftSSDP.swift
git commit -m "Bump version to ${INPUT_VERSION}"
BRANCH="${GITHUB_REF#refs/heads/}"
git push origin "HEAD:${BRANCH}"
fi

- name: Create and push tag
env:
TAG: ${{ env.TAG }}
run: |
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"

- name: Publish GitHub release
env:
GITHUB_TOKEN: ${{ github.token }}
TAG: ${{ env.TAG }}
NOTES: ${{ inputs.notes }}
run: |
if [[ -n "$NOTES" ]]; then
gh release create "$TAG" --title "$TAG" --notes "$NOTES"
else
gh release create "$TAG" --title "$TAG" --generate-notes
fi
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output

## Swift Package Manager
.swiftpm/
Package.resolved
10 changes: 0 additions & 10 deletions Cartfile

This file was deleted.

3 changes: 0 additions & 3 deletions Cartfile.resolved

This file was deleted.

27 changes: 27 additions & 0 deletions Examples/ssdp-demo/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// swift-tools-version:5.9
//
// Package.swift
// ssdp-demo
//
// CLI demo for SwiftSSDP — `ssdp-demo search <target>` and `ssdp-demo listen`.
//

import PackageDescription

let package = Package(
name: "ssdp-demo",
platforms: [
.macOS(.v14),
],
dependencies: [
// Local path-dep back to the parent SwiftSSDP package.
.package(name: "SwiftSSDP", path: "../.."),
],
targets: [
.executableTarget(
name: "ssdp-demo",
dependencies: ["SwiftSSDP"],
path: "Sources/ssdp-demo"
),
]
)
99 changes: 99 additions & 0 deletions Examples/ssdp-demo/Sources/ssdp-demo/diag.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//
// diag.swift
// ssdp-demo
//
// Low-level network diagnostic: bypasses SwiftSSDP entirely and uses Network.framework
// directly. If `ssdp-demo diag` finds devices but `ssdp-demo search` does not, the bug
// is in SwiftSSDP. If neither finds devices, the bug is in your network or entitlements.
//

import Foundation
import Network

func runDiag() async {
print("Diagnostic: bypassing SwiftSSDP, using NWConnectionGroup directly.\n")

let host = NWEndpoint.Host("239.255.255.250")
let port = NWEndpoint.Port(rawValue: 1900)!

do {
let multicast = try NWMulticastGroup(for: [.hostPort(host: host, port: port)])
let params = NWParameters.udp
params.allowLocalEndpointReuse = true
let group = NWConnectionGroup(with: multicast, using: params)

let receivedCount = Counter()

group.setReceiveHandler(maximumMessageSize: 65_507, rejectOversizedMessages: true)
{ message, content, _ in
guard let data = content, !data.isEmpty else { return }
let source = message.remoteEndpoint?.debugDescription ?? "unknown"
let preview = String(data: data.prefix(120), encoding: .utf8) ?? "<binary>"
let firstLine = preview.split(whereSeparator: \.isNewline).first ?? ""
Task { await receivedCount.bump() }
print("[recv] from \(source): \(firstLine)")
}

group.stateUpdateHandler = { state in
print("[state] group state: \(state)")
}

let queue = DispatchQueue(label: "diag", qos: .utility)
group.start(queue: queue)

// Wait briefly for ready.
try await Task.sleep(for: .milliseconds(500))

// Send an M-SEARCH for ssdp:all.
let msearch = """
M-SEARCH * HTTP/1.1\r
HOST: 239.255.255.250:1900\r
MAN: "ssdp:discover"\r
MX: 2\r
ST: ssdp:all\r
\r

"""
let payload = Data(msearch.utf8)
let endpoint = NWEndpoint.hostPort(host: host, port: port)

print("[send] M-SEARCH ssdp:all → 239.255.255.250:1900\n")
group.send(content: payload, to: endpoint, completion: { error in
if let error {
print("[send] failed: \(error)")
} else {
print("[send] sent successfully")
}
})

// Listen for 8 seconds.
try await Task.sleep(for: .seconds(8))

print("\n--- diag complete: received \(await receivedCount.value) datagrams ---")
if await receivedCount.value == 0 {
print("""

If 0 datagrams were received, possible causes:
1. No SSDP-emitting devices are reachable on this LAN.
2. macOS firewall is blocking inbound UDP/1900.
3. The Wi-Fi network is using AP isolation / client isolation.
4. Devices are on a different VLAN with no multicast forwarding.

Try in another terminal:
sudo tcpdump -i en0 -A -n 'host 239.255.255.250 or (udp and port 1900)'

If tcpdump shows packets but this program shows 0, the bug is in
Network.framework usage.
""")
}
group.cancel()

} catch {
print("Diag failed at setup: \(error)")
}
}

actor Counter {
private(set) var value = 0
func bump() { value += 1 }
}
Loading
Loading