diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..886d0b5 --- /dev/null +++ b/.gitignore @@ -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 + diff --git a/README.md b/README.md index e0ef3a5..e10c10c 100755 --- a/README.md +++ b/README.md @@ -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 ============= @@ -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 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..aba7c61 --- /dev/null +++ b/SECURITY.md @@ -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. diff --git a/antipopd b/antipopd index 79c86c4..39bfdc4 100755 Binary files a/antipopd and b/antipopd differ diff --git a/antipopd.m b/antipopd.m index d92417f..1f405a5 100755 --- a/antipopd.m +++ b/antipopd.m @@ -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 #import -#import +#import #import #import +#import +#import +#import #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"); @@ -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 @@ -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 @@ -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, }; diff --git a/com.blendedcocoa.antipopd.plist b/com.blendedcocoa.antipopd.plist index 62df59e..6a95e16 100644 --- a/com.blendedcocoa.antipopd.plist +++ b/com.blendedcocoa.antipopd.plist @@ -2,16 +2,18 @@ - Disabled - KeepAlive Label com.blendedcocoa.antipopd - OnDemand - - Program - /usr/local/bin/antipopd + ProgramArguments + + /usr/local/bin/antipopd + + StandardErrorPath + __HOME__/Library/Logs/antipopd.log + StandardOutPath + __HOME__/Library/Logs/antipopd.log RunAtLoad diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..5860e64 --- /dev/null +++ b/scripts/install.sh @@ -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" diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh new file mode 100644 index 0000000..a34aa76 --- /dev/null +++ b/scripts/uninstall.sh @@ -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"