Skip to content

Commit abb53b7

Browse files
tetujinclaude
andcommitted
Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0 parents  commit abb53b7

9 files changed

Lines changed: 1057 additions & 0 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
name: Sensor CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
schedule:
8+
# Every Monday at 02:00 UTC
9+
- cron: '0 2 * * 1'
10+
11+
jobs:
12+
test:
13+
runs-on: macos-15
14+
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Show Xcode and simulator info
20+
run: |
21+
xcodebuild -version
22+
swift --version
23+
xcrun simctl list runtimes
24+
xcrun simctl list devices available
25+
26+
- name: Resolve package dependencies
27+
run: swift package resolve
28+
29+
- name: Detect scheme and simulator
30+
run: |
31+
SCHEME=$(swift package dump-package | python3 -c "import json,sys; print(json.load(sys.stdin)['name'])")
32+
echo "SCHEME=$SCHEME" >> "$GITHUB_ENV"
33+
34+
# Find the latest available iPhone simulator name (name-based, more robust than UDID)
35+
SIM_NAME=$(xcrun simctl list devices available --json | python3 -c "
36+
import json, sys
37+
devs = json.load(sys.stdin)['devices']
38+
iphones = [
39+
(rt, d['name'])
40+
for rt, dl in devs.items()
41+
for d in dl
42+
if 'iOS' in rt and 'iPhone' in d['name'] and d.get('isAvailable', True)
43+
]
44+
iphones.sort(reverse=True)
45+
print(iphones[0][1] if iphones else 'iPhone 16')
46+
")
47+
echo "SIM_NAME=$SIM_NAME" >> "$GITHUB_ENV"
48+
echo "Detected scheme: $SCHEME"
49+
echo "Detected simulator: $SIM_NAME"
50+
51+
- name: Build
52+
id: build
53+
run: |
54+
xcodebuild build \
55+
-scheme "$SCHEME" \
56+
-destination "platform=iOS Simulator,name=$SIM_NAME" \
57+
-sdk iphonesimulator \
58+
2>&1 | tee build_output.txt; exit "${PIPESTATUS[0]}"
59+
continue-on-error: true
60+
61+
- name: Test
62+
id: test
63+
if: steps.build.outcome == 'success'
64+
run: |
65+
xcodebuild test \
66+
-scheme "$SCHEME" \
67+
-destination "platform=iOS Simulator,name=$SIM_NAME" \
68+
-sdk iphonesimulator \
69+
-configuration Debug \
70+
-enableCodeCoverage YES \
71+
2>&1 | tee test_output.txt; exit "${PIPESTATUS[0]}"
72+
continue-on-error: true
73+
74+
- name: Create or update issue on failure
75+
if: steps.build.outcome == 'failure' || steps.test.outcome == 'failure'
76+
uses: actions/github-script@v7
77+
with:
78+
github-token: ${{ secrets.GITHUB_TOKEN }}
79+
script: |
80+
const fs = require('fs');
81+
const failed = '${{ steps.build.outcome }}' === 'failure' ? 'build' : 'test';
82+
const logFile = failed === 'build' ? 'build_output.txt' : 'test_output.txt';
83+
84+
let raw = '';
85+
try { raw = fs.readFileSync(logFile, 'utf8'); } catch (_) {}
86+
87+
const errors = raw.split('\n')
88+
.filter(l => /error:|FAILED|\*\* BUILD FAILED/.test(l))
89+
.slice(0, 60)
90+
.join('\n');
91+
92+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
93+
const title = `[CI] ${failed} failure detected`;
94+
const body = [
95+
`## :x: Automated CI ${failed} failure`,
96+
'',
97+
`| | |`,
98+
`|---|---|`,
99+
`| **Workflow run** | [View logs](${runUrl}) |`,
100+
`| **Trigger** | \`${context.eventName}\` |`,
101+
`| **Ref** | \`${context.ref}\` |`,
102+
`| **Commit** | \`${context.sha.slice(0,7)}\` |`,
103+
'',
104+
`## Error summary`,
105+
'```',
106+
errors || '(no error lines captured — see full logs above)',
107+
'```',
108+
].join('\n');
109+
110+
// Ensure ci-failure label exists
111+
try {
112+
await github.rest.issues.createLabel({
113+
owner: context.repo.owner,
114+
repo: context.repo.repo,
115+
name: 'ci-failure',
116+
color: 'd73a4a',
117+
description: 'Automated CI test failure',
118+
});
119+
} catch (_) { /* label already exists */ }
120+
121+
const { data: issues } = await github.rest.issues.listForRepo({
122+
owner: context.repo.owner,
123+
repo: context.repo.repo,
124+
state: 'open',
125+
labels: 'ci-failure',
126+
});
127+
128+
if (issues.length > 0) {
129+
await github.rest.issues.createComment({
130+
owner: context.repo.owner,
131+
repo: context.repo.repo,
132+
issue_number: issues[0].number,
133+
body: `### :warning: New failure (${new Date().toISOString().split('T')[0]})\n\n${body}`,
134+
});
135+
} else {
136+
await github.rest.issues.create({
137+
owner: context.repo.owner,
138+
repo: context.repo.repo,
139+
title,
140+
body,
141+
labels: ['ci-failure'],
142+
});
143+
}
144+
145+
- name: Close ci-failure issue on success
146+
if: steps.build.outcome == 'success' && steps.test.outcome == 'success'
147+
uses: actions/github-script@v7
148+
with:
149+
github-token: ${{ secrets.GITHUB_TOKEN }}
150+
script: |
151+
const { data: issues } = await github.rest.issues.listForRepo({
152+
owner: context.repo.owner,
153+
repo: context.repo.repo,
154+
state: 'open',
155+
labels: 'ci-failure',
156+
});
157+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
158+
for (const issue of issues) {
159+
await github.rest.issues.createComment({
160+
owner: context.repo.owner,
161+
repo: context.repo.repo,
162+
issue_number: issue.number,
163+
body: `:white_check_mark: All tests are passing again. See [run ${context.runId}](${runUrl}).`,
164+
});
165+
await github.rest.issues.update({
166+
owner: context.repo.owner,
167+
repo: context.repo.repo,
168+
issue_number: issue.number,
169+
state: 'closed',
170+
});
171+
}
172+
173+
- name: Fail job if build or tests failed
174+
if: steps.build.outcome == 'failure' || steps.test.outcome == 'failure'
175+
run: |
176+
echo "Build outcome: ${{ steps.build.outcome }}"
177+
echo "Test outcome: ${{ steps.test.outcome }}"
178+
exit 1
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>SchemeUserState</key>
6+
<dict>
7+
<key>com.awareframework.ios.sensor.network.xcscheme_^#shared#^_</key>
8+
<dict>
9+
<key>orderHint</key>
10+
<integer>9</integer>
11+
</dict>
12+
</dict>
13+
</dict>
14+
</plist>

‎Package.resolved‎

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Package.swift‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// swift-tools-version: 6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "com.awareframework.ios.sensor.network",
7+
platforms: [.iOS(.v13), .macOS(.v10_15)],
8+
products: [
9+
.library(
10+
name: "com.awareframework.ios.sensor.network",
11+
targets: [
12+
"com.awareframework.ios.sensor.network"
13+
]
14+
),
15+
],
16+
dependencies: [
17+
.package(url: "https://github.com/awareframework/com.awareframework.ios.core.git", from: "1.3.0")
18+
],
19+
targets: [
20+
.target(
21+
name: "com.awareframework.ios.sensor.network",
22+
dependencies: [
23+
.product(name: "com.awareframework.ios.core", package: "com.awareframework.ios.core")
24+
],
25+
path: "Sources/com.awareframework.ios.sensor.network"
26+
),
27+
.testTarget(
28+
name: "com.awareframework.ios.sensor.networkTests",
29+
dependencies: ["com.awareframework.ios.core", "com.awareframework.ios.sensor.network"]
30+
)
31+
],
32+
swiftLanguageModes: [.v5]
33+
)

‎README.md‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# AWARE Network Sensor
2+
3+
This package implements the iOS network sensor for AWARE-compatible clients.
4+
5+
It records Wi-Fi/mobile connectivity state in `ios_network` and interface traffic counters in
6+
`ios_network_traffic` under the `aware_network.sqlite` database.
7+
8+
## Installation
9+
10+
Add the package to your Swift package dependencies:
11+
12+
```swift
13+
.package(url: "https://github.com/awareframework/com.awareframework.ios.sensor.network.git", from: "1.0.0")
14+
```
15+
16+
Then add the product to your target:
17+
18+
```swift
19+
.product(name: "com.awareframework.ios.sensor.network", package: "com.awareframework.ios.sensor.network")
20+
```
21+
22+
## Usage
23+
24+
```swift
25+
import com_awareframework_ios_sensor_network
26+
27+
let sensor = NetworkSensor(NetworkSensor.Config().apply { config in
28+
config.dbType = .sqlite
29+
config.dbPath = "aware_network"
30+
config.dbTableName = NetworkData.databaseTableName
31+
config.interval = 60
32+
config.statusEventsEnabled = true
33+
config.trafficEnabled = true
34+
})
35+
36+
sensor.start()
37+
```
38+
39+
`NetworkSensor.Config` extends the common AWARE `SensorConfig`.
40+
41+
| Property | Type | Default | Description |
42+
| --- | --- | --- | --- |
43+
| `interval` | `Int` | `60` | Traffic sampling interval in seconds. |
44+
| `statusEventsEnabled` | `Bool` | `true` | Enables Wi-Fi/mobile/internet availability events. |
45+
| `trafficEnabled` | `Bool` | `true` | Enables interface traffic sampling. |
46+
| `includeLoopback` | `Bool` | `false` | Includes loopback interfaces when sampling. |
47+
| `sensorObserver` | `NetworkObserver?` | `nil` | Callback for live status and traffic samples. |
48+
| `dbPath` | `String?` | `aware_network` | SQLite database path. |
49+
| `dbTableName` | `String?` | `ios_network` in AwareClient | Active status table name. |
50+
51+
## Data
52+
53+
`NetworkData` stores connectivity state:
54+
55+
| Field | Type | Description |
56+
| --- | --- | --- |
57+
| `networkType` | `Int` | AWARE type. `1` Wi-Fi, `4` mobile. |
58+
| `networkSubtype` | `String` | `WIFI` or `MOBILE`. |
59+
| `networkState` | `Int` | `1` on, `0` off. |
60+
61+
`NetworkTrafficData` stores interface counters:
62+
63+
| Field | Type | Description |
64+
| --- | --- | --- |
65+
| `networkType` | `Int` | Traffic type. `1` mobile, `2` Wi-Fi. |
66+
| `networkSubtype` | `String` | `WIFI` or `MOBILE`. |
67+
| `interfaceName` | `String` | Interface name, for example `en0` or `pdp_ip0`. |
68+
| `receivedBytes` / `sentBytes` | `Double` | Cumulative byte counters from the OS. |
69+
| `receivedPackets` / `sentPackets` | `Double` | Cumulative packet counters from the OS. |
70+
| `receivedBytesDelta` / `sentBytesDelta` | `Double` | Difference from the previous sample. |
71+
| `receivedPacketsDelta` / `sentPacketsDelta` | `Double` | Packet difference from the previous sample. |
72+
| `totalBytesDelta` | `Double` | Received plus sent byte delta. |
73+
74+
The first traffic sample has zero deltas because there is no previous counter snapshot.
75+
76+
## Notifications
77+
78+
| Notification | Description |
79+
| --- | --- |
80+
| `actionAwareNetworkStart` | Posted when the sensor starts. |
81+
| `actionAwareNetworkStop` | Posted when the sensor stops. |
82+
| `actionAwareNetwork` | Posted after each connectivity state record. |
83+
| `actionAwareNetworkWifiOn` / `actionAwareNetworkWifiOff` | Posted when Wi-Fi state changes. |
84+
| `actionAwareNetworkMobileOn` / `actionAwareNetworkMobileOff` | Posted when mobile state changes. |
85+
| `actionAwareInternetAvailable` / `actionAwareInternetUnavailable` | Posted when internet availability changes. |
86+
| `actionAwareNetworkTraffic` | Posted after each traffic sample. |
87+
| `actionAwareNetworkSync` | Posted when sync starts. |
88+
| `actionAwareNetworkSyncCompletion` | Posted when a table sync completes. |
89+
| `actionAwareNetworkSetLabel` | Posted when the label changes. |
90+
91+
## Observer
92+
93+
```swift
94+
final class Observer: NetworkObserver {
95+
func onNetworkChanged(data: NetworkData) {}
96+
func onTrafficChanged(data: [NetworkTrafficData]) {}
97+
func onInternetAvailable(access: Int) {}
98+
func onInternetUnavailable() {}
99+
}
100+
```
101+
102+
## Notes
103+
104+
The traffic implementation uses iOS/macOS interface counters exposed through `getifaddrs`.
105+
It monitors Wi-Fi as `en0` and cellular interfaces as `pdp_ip*`. iOS does not expose per-app
106+
network byte counts through public APIs, so these values represent interface-level counters.

0 commit comments

Comments
 (0)