Skip to content
Open
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
43 changes: 43 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Compiled binaries
antipopd
*.o
*.a
*.so
*.dylib

# Build artifacts
build/
dist/
*.dSYM/

# macOS system files
.DS_Store
.AppleDouble
.LSOverride
*.swp
*.swo
*~
.vscode/
.idea/
*.swp

# Xcode
*.xcodeproj/
*.xcworkspace/
xcuserdata/
*.playground

# Temporary files
*.tmp
*.bak
*.log

# IDEs
.vscode/
.idea/
*.sublime-workspace
*.sublime-project

# OS-specific
Thumbs.db

61 changes: 52 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ Build
=====

`antipopd` can be built in a terminal using the following command:
```shell
clang -framework Foundation -framework CoreFoundation -framework AVFoundation -framework IOKit -arch arm64 -arch x86_64 -o antipopd antipopd.m
```

clang -framework AppKit -framework IOKit -arch i386 -arch x86_64 -o antipopd antipopd.m

A built version (`i386` and `x86_64`) of `antipopd` is included in the repository.
A universal (`arm64` and `x86_64`) build will run on Apple Silicon and Intel Macs.

Configuration
=============
Expand All @@ -45,14 +46,56 @@ only be kept alive when on AC power.
The configuration file is only read once when `antipopd` launches. Changing
the configuration file will not take effect until `antipopd` is restarted.

Debug Mode
==========

`antipopd` supports a debug mode that allows you to hear the audio system
being kept active. This is useful for testing and verification purposes.

To run `antipopd` in debug mode, use the `-d` flag:

./antipopd -d

In debug mode, `antipopd` will speak an audible "beep" every 10 seconds instead
of a silent space. This lets you hear the program working and verify that the
audio system is being kept awake as expected.

Command Line Options
====================

-d Enable debug mode (plays audible beep every 10 seconds)
-h Show help message and usage information

Logging
=======

`antipopd` writes occasional log entries to `~/Library/Logs/antipopd.log` so you
can verify that the program is running. Log entries are written every 5 minutes
(every 30 cycles) to keep the log file small and manageable.

Each log entry includes a timestamp and an utterance counter showing how many times
the audio system has been kept active.

**Log Rotation**: When the log file reaches 5MB, it is automatically rotated. The
current log is renamed with a timestamp suffix (e.g., `antipopd.log.20260125_143022`)
and a new `antipopd.log` is created. This prevents the log from consuming excessive
disk space.

You can view the log using:

tail -f ~/Library/Logs/antipopd.log

Installation
============

In order to have `antipopd` run as a daemon (run automatically) it is
necessary to configure `launchctl`. In `Terminal` run the following commands:
To have `antipopd` run automatically for your user account, install it as a
LaunchAgent using the provided script:

./scripts/install.sh

Uninstallation
==============

sudo cp com.blendedcocoa.antipopd.plist /Library/LaunchDaemons
sudo cp antipopd /usr/local/bin
sudo launchctl load -w /Library/LaunchDaemons/com.blendedcocoa.antipopd.plist
To remove `antipopd` from your user account, run:

You will need to provide your password to allow the installation.
./scripts/uninstall.sh
21 changes: 21 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Security Notes

This project is a small, long-running user agent that keeps the audio subsystem
awake by speaking a silent space. The code itself does not require elevated
privileges, but installation and runtime choices can affect security posture.

Considerations and mitigations:

- User-scoped launch: The LaunchAgent runs under your user account instead of
as root, reducing impact if the binary is ever replaced or misused.
- Binary trust: The installer copies `antipopd` to `/usr/local/bin`. Ensure that
path is owned by root or your admin user and not writable by untrusted users.
- Config file integrity: If you use the optional `/usr/local/share/antipop/ac_only`
config file, keep its directory permissions restricted to prevent tampering.
- Log file location: Logs are written to `~/Library/Logs/antipopd.log`. This is
user-writable and may reveal basic runtime info (process start/stop events).
- No network access: The program does not open sockets or accept input, reducing
exposure to remote attacks.

If you need a system-wide install, consider signing the binary and using file
permissions to prevent tampering.
Binary file modified antipopd
Binary file not shown.
104 changes: 95 additions & 9 deletions antipopd.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,70 @@

/*

clang -framework AppKit -framework IOKit -arch i386 -arch x86_64 -o antipopd antipopd.m
clang -framework Foundation -framework CoreFoundation -framework AVFoundation -framework IOKit -arch arm64 -arch x86_64 -o antipopd antipopd.m

*/

#import <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <AVFoundation/AVFoundation.h>
#import <IOKit/ps/IOPowerSources.h>

#import <unistd.h>
#import <string.h>
#import <sys/stat.h>
#import <time.h>

#define ANTIPOPD_CONFIG "/usr/local/share/antipop/ac_only"
#define BATTERY_STATE CFSTR("State:/IOKit/PowerSources/InternalBattery-0")
#define POWER_SOURCE CFSTR("Power Source State")
#define INTERVAL 10 // seconds
#define MAX_LOG_SIZE (5 * 1024 * 1024) // 5MB in bytes

static BOOL runOnACOnly = NO;
static BOOL debugMode = NO;
static char logFilePath[PATH_MAX];
static int logCounter = 0;

// Get the log file path
void setupLogPath() {
const char *homeDir = getenv("HOME");
if (homeDir == NULL) {
homeDir = "/tmp";
}
snprintf(logFilePath, PATH_MAX, "%s/Library/Logs/antipopd.log", homeDir);
}

// Rotate log file if it exceeds MAX_LOG_SIZE
void rotateLogFile() {
struct stat st;
if (stat(logFilePath, &st) == 0 && st.st_size >= MAX_LOG_SIZE) {
// Create backup filename with timestamp
char backupPath[PATH_MAX];
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
char timestamp[32];
strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm_info);
snprintf(backupPath, PATH_MAX, "%s.%s", logFilePath, timestamp);

// Rename current log to backup
rename(logFilePath, backupPath);
}
}

// Write to log file
void writeLog(const char *message) {
FILE *logFile = fopen(logFilePath, "a");
if (logFile) {
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
char timestamp[32];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);

fprintf(logFile, "[%s] %s\n", timestamp, message);
fclose(logFile);
}
}

void banner() {
printf("antipopd\n\n");
Expand All @@ -51,17 +98,31 @@ void banner() {
printf("a Creative Commons Attribution Noncommercial Share Alike License 3.0,\n");
printf("http://creativecommons.org/licenses/by-nc-sa/3.0/us\n\n");

printf("Usage: antipopd [options]\n");
printf(" -d Enable debug mode (plays audible beep every 10 seconds)\n");
printf(" -h Show this help message\n\n");

}


NSSpeechSynthesizer *speech = nil;
AVSpeechSynthesizer *speech = nil; // Use AVFoundation speech to keep audio subsystem awake.

// Timer callback that actually speaks the space
void speak(CFRunLoopTimerRef timer, void *info) {
if (!speech) {
speech = [[NSSpeechSynthesizer alloc] initWithVoice:nil];
// Lazily initialize the synthesizer once to keep overhead low.
speech = [[AVSpeechSynthesizer alloc] init];
}

// Increment log counter and log periodically (every 30 calls = 5 minutes)
logCounter++;
if (logCounter % 30 == 0) {
rotateLogFile();
char message[256];
snprintf(message, sizeof(message), "Spoke utterance (count: %d)", logCounter);
writeLog(message);
}

// If we are only supposed to run on AC power
if (runOnACOnly) {
// and we don't have unlimited power remaining
Expand All @@ -71,7 +132,21 @@ void speak(CFRunLoopTimerRef timer, void *info) {
}
}

[speech startSpeakingString:@" "];
if (!speech.isSpeaking) {
AVSpeechUtterance *utterance;

if (debugMode) {
// In debug mode, speak "beep" audibly so we can hear it
utterance = [AVSpeechUtterance speechUtteranceWithString:@"beep"];
utterance.volume = 1.0f;
} else {
// Speak a silent space so the audio system stays active without audible output.
utterance = [AVSpeechUtterance speechUtteranceWithString:@" "];
utterance.volume = 0.0f;
}

[speech speakUtterance:utterance];
}
}

// Check for the existance of the ac_only file, check the contents
Expand All @@ -96,15 +171,26 @@ void loadACOnlyConfig() {
}

int main(int argc, char *argv[]) {
setupLogPath();
loadACOnlyConfig();

// Put an AutoreleasePool in place in case NSSpeechSynthesizer expects it
@autoreleasepool {
if (argc >= 2) { // if we have any parameter show the banner
banner();
exit(EXIT_SUCCESS);
// Parse command line arguments
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-d") == 0) {
debugMode = YES;
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
banner();
exit(EXIT_SUCCESS);
}
}


// If debug mode is enabled, show banner for confirmation
if (debugMode) {
printf("antipopd running in DEBUG MODE - audible beeps every 10 seconds\n\n");
}

CFRunLoopTimerContext context = {
0, NULL, NULL, NULL, NULL,
};
Expand Down
14 changes: 8 additions & 6 deletions com.blendedcocoa.antipopd.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Disabled</key>
<false/>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>com.blendedcocoa.antipopd</string>
<key>OnDemand</key>
<false/>
<key>Program</key>
<string>/usr/local/bin/antipopd</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/antipopd</string>
</array>
<key>StandardErrorPath</key>
<string>__HOME__/Library/Logs/antipopd.log</string>
<key>StandardOutPath</key>
<string>__HOME__/Library/Logs/antipopd.log</string>
<key>RunAtLoad</key>
<true/>
</dict>
Expand Down
26 changes: 26 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/sh
set -eu

PLIST_SRC="com.blendedcocoa.antipopd.plist"
PLIST_DST="$HOME/Library/LaunchAgents/com.blendedcocoa.antipopd.plist"
BIN_DST="/usr/local/bin/antipopd"
LOG_PATH="$HOME/Library/Logs/antipopd.log"

mkdir -p "$HOME/Library/LaunchAgents"
mkdir -p "/usr/local/bin"
mkdir -p "$HOME/Library/Logs"

# Compile the antipopd binary
echo "Compiling antipopd..."
clang -framework Foundation -framework CoreFoundation -framework AVFoundation -framework IOKit -arch arm64 -arch x86_64 -o antipopd antipopd.m

cp antipopd "$BIN_DST"
cp "$PLIST_SRC" "$PLIST_DST"

# launchd does not expand "~" in plist paths, so inject the absolute $HOME.
sed -i '' "s|__HOME__|$HOME|g" "$PLIST_DST"

launchctl unload -w "$PLIST_DST" >/dev/null 2>&1 || true
launchctl load -w "$PLIST_DST"

echo "Installed LaunchAgent and logging to $LOG_PATH"
14 changes: 14 additions & 0 deletions scripts/uninstall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/sh
set -eu

PLIST_DST="$HOME/Library/LaunchAgents/com.blendedcocoa.antipopd.plist"
BIN_DST="/usr/local/bin/antipopd"
LOG_PATH="$HOME/Library/Logs/antipopd.log"

launchctl unload -w "$PLIST_DST" >/dev/null 2>&1 || true

rm -f "$PLIST_DST"
rm -f "$BIN_DST"
rm -f "$LOG_PATH"

echo "Uninstalled LaunchAgent and removed $LOG_PATH"