// This file is part of BOINC. // http://boinc.berkeley.edu // Copyright (C) 2024 University of California // // BOINC is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License // as published by the Free Software Foundation, // either version 3 of the License, or (at your option) any later version. // // BOINC is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // See the GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with BOINC. If not, see . /* PostInstall.cpp */ // Notes on command-line installation to a remote Mac: // // When the installer is run from the Finder, this Postinstall.app will // display up to two dialogs, asking the user whether or not to: // [1] allow non-administrative users to run the BOINC Manager // (asked only if this Mac has any non-administrative users) // [2] set BOINC as the screensaver for all users who can run BOINC // (asked only if BOINC screensaver is not already set for them) // // The installer can also be run from the command line. This is useful // for installation on remote Macs. However, there is no way to respond // to dialogs during a command-line install. // // Apple's command-line installer sets the following environment variable: // COMMAND_LINE_INSTALL=1 // The postinstall script, postupgrade script, and this Postinstall.app // detect this environment variable and do the following: // * Redirect the Postinstall.app log output to a file // /tmp/BOINCInstallLog.txt // * Suppress the 2 dialogs // * test for the existence of a file /tmp/nonadminusersok.txt; if the // file exists, allow non-administrative users to run BOINC Manager // * test for the existence of a file /tmp/setboincsaver.txt; if the // file exists, set BOINC as the screensaver for all BOINC users. // // The BOINC installer package to be used for command line installs can // be found embedded inside the GUI BOINC Installer application at: // "..../BOINC Installer.app/Contents/Resources/BOINC.pkg" // // Example: To install on a remote Mac from the command line, allowing // non-admin users to run the BOINC Manager and setting BOINC as the // screensaver: // * First SCP the "BOINC.pkg" to the remote Mac's /tmp // directory, then SSh into the remote Mac and enter the following // $ touch /tmp/nonadminusersok.txt // $ touch /tmp/setboincsaver.txt // $ sudo installer -pkg /tmp/BOINC.pkg -tgt / // $ sudo reboot // #define VERBOSE_TEST 0 /* for debugging callPosixSpawn */ #if VERBOSE_TEST #define CREATE_LOG 1 /* for debugging */ #else #define CREATE_LOG 0 /* for debugging */ #endif #define USE_OSASCRIPT_FOR_ALL_LOGGED_IN_USERS false #include #include #include // getpwname, getpwuid, getuid #include // getpwname, getpwuid, getuid #include // getgrnam #include // waitpid #include #include // for MAXPATHLEN #include // for chmod #include #include #include #include #include // for time() #include #include #define DLOPEN_NO_WARN #include #include #include "url.h" #include "mac_branding.h" using std::vector; using std::string; #include "mac_util.h" #include "SetupSecurity.h" #include "translate.h" #include "file_names.h" #include "util.h" #define admin_group_name "admin" #define boinc_master_user_name "boinc_master" #define boinc_master_group_name "boinc_master" #define boinc_project_user_name "boinc_project" #define boinc_project_group_name "boinc_project" OSErr Initialize(void); /* function prototypes */ Boolean myFilterProc(DialogRef theDialog, EventRecord *theEvent, DialogItemIndex *itemHit); int DeleteReceipt(void); Boolean IsRestartNeeded(); void CheckUserAndGroupConflicts(); Boolean SetLoginItemOSAScript(long brandID, Boolean deleteLogInItem, char *userName); Boolean SetLoginItemLaunchAgent(long brandID, long oldBrandID, Boolean deleteLogInItem, passwd *pw); OSErr GetCurrentScreenSaverSelection(passwd *pw, char *moduleName, size_t maxLen); OSErr SetScreenSaverSelection(char *moduleName, char *modulePath, int type); static void DeleteScreenSaverLaunchAgent(passwd *pw); void SetSkinInUserPrefs(char *userName, char *nameOfSkin); Boolean CheckDeleteFile(char *name); static void FixLaunchServicesDataBase(uid_t userID, char *pathToKeep, char *theBundleID); void SetEUIDBackToUser (void); static char * PersistentFGets(char *buf, size_t buflen, FILE *f); static void LoadPreferredLanguages(); static Boolean ShowMessage(Boolean askYesNo, const char *format, ...); Boolean IsUserMemberOfGroup(const char *userName, const char *groupName); int CountGroupMembershipEntries(const char *userName, const char *groupName); OSErr UpdateAllVisibleUsers(long brandID, long oldBrandID); static Boolean IsUserLoggedIn(const char *userName); void FindAllVisibleUsers(void); long GetBrandID(char *path); int TestRPCBind(void); #ifdef __arm64__ int check_rosetta2_installed(); int optionally_install_rosetta2(); #endif // __arm64__ pid_t FindProcessPID(char* name, pid_t thePID); static void SleepSeconds(double seconds); static OSErr QuitAppleEventHandler(const AppleEvent *appleEvt, AppleEvent* reply, UInt32 refcon); int callPosixSpawn(const char *cmd); void print_to_log(const char *format, ...); void strip_cr(char *buf); void CopyPreviousErrorsToLog(void); extern int check_security( char *bundlePath, char *dataPath, int use_sandbox, int isManager, char* path_to_error, int len ); /* BEGIN TEMPORARY ITEMS TO ALLOW TRANSLATORS TO START WORK */ void notused() { ShowMessage(true, (char *)_("Yes")); ShowMessage(true, (char *)_("No")); // Future feature ShowMessage(true, (char *)_("Should BOINC run even when no user is logged in?")); } /* END TEMPORARY ITEMS TO ALLOW TRANSLATORS TO START WORK */ #define MAX_LANGUAGES_TO_TRY 5 static char * Catalog_Name = (char *)"BOINC-Setup"; static char * Catalogs_Dir = (char *)"/Library/Application Support/BOINC Data/locale/"; #define REPORT_ERROR(isError) if (isError) print_to_log("BOINC PostInstall error at line %d", __LINE__); /* globals */ static Boolean gCommandLineInstall = false; static Boolean gQuitFlag = false; static Boolean currentUserCanRunBOINC = false; static char loginName[256]; static char tempDirName[MAXPATHLEN]; static time_t waitPermissionsStartTime; static vector human_user_names; static vector human_user_IDs; enum { launchWhenDone, logoutRequired, restartRequired, nothingrequired }; /****************************************************************** *** *** *** NOTE: *** *** *** *** On entry, the postinstall or postupgrade script has set the *** *** current directory to the top level of our installer package *** *** *** ******************************************************************/ int main(int argc, char *argv[]) { long brandID = 0; long oldBrandID = 0; int major = 0; int minor = 0; int i; pid_t installerPID = 0, coreClientPID = 0; OSStatus err; FILE *f; char s[2048]; #ifndef SANDBOX group *grp; #endif // SANDBOX printf("\nStarting PostInstall app %s\n\n", argv[1]); fflush(stdout); if (!check_branding_arrays(s, sizeof(s))) { ShowMessage(false, (char *)_("Branding array has too few entries: %s"), s); return -1; } // getlogin() gives unreliable results under OS 10.6.2, so use environment strncpy(loginName, getenv("USER"), sizeof(loginName)-1); if (loginName[0] == '\0') { ShowMessage(false, (char *)_("Could not get user login name")); return 0; } printf("login name = %s\n", loginName); fflush(stdout); snprintf(tempDirName, sizeof(tempDirName), "InstallBOINC-%s", loginName); CopyPreviousErrorsToLog(); if (getenv("COMMAND_LINE_INSTALL") != NULL) { gCommandLineInstall = true; puts("command-line install\n"); fflush(stdout); } for (i=0; i= RETRY_LIMIT); continue; } // err = SetBOINCAppOwnersGroupsAndPermissions("/Applications/GridRepublic Desktop.app"); err = SetBOINCAppOwnersGroupsAndPermissions(appPath[brandID]); if (err != noErr) { printf("SetBOINCAppOwnersGroupsAndPermissions returned %d (repetition=%d)", err, i); fflush(stdout); REPORT_ERROR(i >= RETRY_LIMIT); continue; } err = SetBOINCDataOwnersGroupsAndPermissions(); if (err != noErr) { printf("SetBOINCDataOwnersGroupsAndPermissions returned %d (repetition=%d)", err, i); fflush(stdout); REPORT_ERROR(i >= RETRY_LIMIT); continue; } err = check_security( appPath[brandID], "/Library/Application Support/BOINC Data", true, false, NULL, 0 ); if (err != noErr) { printf("check_security returned %d (repetition=%d)", err, i); fflush(stdout); REPORT_ERROR(i >= RETRY_LIMIT); } else { break; } } #else // ! defined(SANDBOX) Boolean Success = false; // The BOINC Manager and Core Client have the set-user-ID-on-execution // flag set, so their ownership is important and must match the // ownership of the BOINC Data directory. // Find an appropriate admin user to set as owner of installed files // First, try the user currently logged in grp = getgrnam(admin_group_name); i = 0; while ((p = grp->gr_mem[i]) != NULL) { // Step through all users in group admin if (strcmp(p, loginName) == 0) { Success = true; // Logged in user is a member of group admin break; } ++i; } // If currently logged in user is not admin, use first non-root admin user if (!Success) { i = 0; while ((p = grp->gr_mem[i]) != NULL) { // Step through all users in group admin if (strcmp(p, "root") != 0) break; ++i; } } // Set owner of branded BOINCManager and contents, including core client // "chown -Rf username \"/Applications/GridRepublic Desktop.app\"" sprintf(s, "chown -Rf %s \"%s\"", p, appPath[brandID]); err = callPosixSpawn (s); REPORT_ERROR(err); // Set owner of BOINC Screen Saver // "chown -Rf username \"/Library/Screen Savers/GridRepublic.saver\"" sprintf(s, "chown -Rf %s \"/Library/Screen Savers/%s.saver\"", p, saverName[brandID]); err = callPosixSpawn (s); REPORT_ERROR(err); // We don't customize BOINC Data directory name for branding // "chown -Rf username \"/Library/Application Support/BOINC Data\"" sprintf(s, "chown -Rf %s \"/Library/Application Support/BOINC Data\"", p); err = callPosixSpawn (s); REPORT_ERROR(err); // "chmod -R a+s \"/Applications/GridRepublic Desktop.app\"" sprintf(s, "chmod -R a+s \"%s\"", appPath[brandID]); err = callPosixSpawn (s); REPORT_ERROR(err); #endif // ! defined(SANDBOX) // Remove any branded versions of BOINC other than ours (i.e., old versions) for (i=0; i< NUMBRANDS; i++) { if (i == brandID) continue; // "rm -rf \"/Applications/GridRepublic Desktop.app\"" sprintf(s, "rm -rf \"%s\"", appPath[i]); err = callPosixSpawn (s); REPORT_ERROR(err); // "rm -rf \"/Library/Screen Savers/GridRepublic.saver\"" sprintf(s, "rm -rf \"/Library/Screen Savers/%s.saver\"", saverName[i]); err = callPosixSpawn (s); REPORT_ERROR(err); } if (brandID == 0) { // Installing generic BOINC err = callPosixSpawn ("rm -f \"/Library/Application Support/BOINC Data/Branding\""); REPORT_ERROR(err); } CFStringRef CFAppPath = CFStringCreateWithCString(kCFAllocatorDefault, appPath[brandID], kCFStringEncodingUTF8); if (CFAppPath) { // urlref = CFURLCreateWithFileSystemPath(NULL, "/Applications/GridRepublic Desktop.app", kCFURLPOSIXPathStyle, true); CFURLRef urlref = CFURLCreateWithFileSystemPath(NULL, CFAppPath, kCFURLPOSIXPathStyle, true); CFRelease(CFAppPath); if (urlref) { err = LSRegisterURL(urlref, true); CFRelease(urlref); REPORT_ERROR(err); } } err = UpdateAllVisibleUsers(brandID, oldBrandID); if (err != noErr) { REPORT_ERROR(true); return err; } #if 0 // WaitPermissions is not needed when using wrapper #ifdef SANDBOX pid_t waitPermissionsPID = 0; uid_t saved_euid, saved_uid, b_m_uid; passwd *pw; Boolean restartNeeded; DialogRef theWin; restartNeeded = IsRestartNeeded(); printf("IsRestartNeeded() returned %d\n", (int)restartNeeded); fflush(stdout); if (!restartNeeded) { // Wait for BOINC's RPC socket address to become available to user boinc_master, in // case we are upgrading from a version which did not run as user boinc_master. saved_uid = getuid(); saved_euid = geteuid(); pw = getpwnam(boinc_master_user_name); b_m_uid = pw->pw_uid; seteuid(b_m_uid); for (i=0; i<120; i++) { err = TestRPCBind(); if (err == noErr) break; sleep(1); } seteuid(saved_euid); // When we first create the boinc_master group and add the current user to the // new group, there is a delay before the new group membership is recognized. // If we launch the BOINC Manager too soon, it will fail with a -1037 permissions // error, so we wait until the current user can access the switcher application. // Apparently, in order to get the changed permissions / group membership, we must // launch a new process belonging to the user. It may also need to be in a new // process group or new session. Neither system() nor popen() works, even after // setting the uid and euid back to the logged in user, but LSOpenFSRef() does. // The WaitPermissions application loops until it can access the switcher // application. CFStringRef CFAppPath = CFStringCreateWithCString(kCFAllocatorDefault, "/Library/Application Support/BOINC Data/WaitPermissions.app", kCFStringEncodingUTF8); if (CFAppPath) { // urlref = CFURLCreateWithFileSystemPath(NULL, "/Applications/GridRepublic Desktop.app", kCFURLPOSIXPathStyle, true); CFURLRef urlref = CFURLCreateWithFileSystemPath(NULL, CFAppPath, kCFURLPOSIXPathStyle, true); CFRelease(CFAppPath); if (urlref) { err = LSOpenCFURLRef(urlref, NULL); CFRelease(urlref); } } if (err) { printf("LSOpenCFURLRef(WaitPermissions) returned error %ld\n", err); fflush(stdout); } waitPermissionsStartTime = time(NULL); for (i=0; i<15; i++) { // Show "Please wait..." alert after 15 seconds waitPermissionsPID = FindProcessPID("WaitPermissions", 0); if (waitPermissionsPID == 0) { return 0; } sleep(1); } if (gCommandLineInstall) { printf("Finishing install. Please wait ...\n"); printf("This may take a few more minutes.\n"); fflush(stdout); } else { CreateStandardAlert(kAlertNoteAlert, CFSTR("Finishing install. Please wait ..."), CFSTR("This may take a few more minutes."), NULL, &theWin); HideDialogItem(theWin, kStdOkItemIndex); RemoveDialogItems(theWin, kStdOkItemIndex, 1, false); RunStandardAlert(theWin, &myFilterProc, &itemHit); } } #endif // SANDBOX #endif // WaitPermissions is not needed when using wrapper return 0; } Boolean myFilterProc(DialogRef theDialog, EventRecord *theEvent, DialogItemIndex *itemHit) { static time_t lastCheckTime = 0; time_t now = time(NULL); pid_t waitPermissionsPID = 0; if (now != lastCheckTime) { waitPermissionsPID = FindProcessPID("WaitPermissions", 0); if (waitPermissionsPID == 0) { *itemHit = kStdOkItemIndex; return true; } lastCheckTime = now; // Limit delay to 3 minutes if ((now - waitPermissionsStartTime) > 180) { *itemHit = kStdOkItemIndex; return true; } } return false; } // After installation has completed, delete the installer receipt. // If we don't need to logout the user, also launch BOINC Manager. int DeleteReceipt() { long brandID = 0; int i; pid_t installerPID = 0; OSStatus err; Boolean restartNeeded = true; char s[MAXPATHLEN]; struct stat sbuf; passwd *pw; Boolean launchForThisUser; if (Initialize() != noErr) { REPORT_ERROR(true); return 0; } restartNeeded = IsRestartNeeded(); printf("IsRestartNeeded() returned %d\n", (int)restartNeeded); fflush(stdout); // The new branding (if any) is in the resources of this PostInstall.app getPathToThisApp(s, sizeof(s)); strncat(s, "/Contents/Resources/Branding", sizeof(s)-1); brandID = GetBrandID(s); // Remove installer package receipt so we can run installer again if needed to fix permissions // "rm -rf /Library/Receipts/GridRepublic.pkg" sprintf(s, "rm -rf \"%s\"", receiptName[brandID]); err = callPosixSpawn (s); REPORT_ERROR(err); if (!restartNeeded) { // If system is set up to run BOINC Client as a daemon using launchd, launch it // as a daemon and allow time for client to start before launching BOINC Manager. err = stat("/Library/LaunchDaemons/edu.berkeley.boinc.plist", &sbuf); if (err == noErr) { callPosixSpawn("launchctl unload /Library/LaunchDaemons/edu.berkeley.boinc.plist"); i = callPosixSpawn("launchctl load /Library/LaunchDaemons/edu.berkeley.boinc.plist"); if (i == 0) sleep (2); } #ifdef SANDBOX pw = getpwnam(loginName); REPORT_ERROR(!pw); if (pw) { Boolean isBMGroupMember = IsUserMemberOfGroup(pw->pw_name, boinc_master_group_name); if (!isBMGroupMember){ return 0; // Current user is not authorized to run BOINC Manager } } #endif installerPID = getPidIfRunning("com.apple.installer"); if (installerPID) { // Launch BOINC Manager when user closes installer or after 15 seconds for (i=0; i<15; i++) { // Wait 15 seconds max for installer to quit sleep (1); if (FindProcessPID(NULL, installerPID) == 0) { break; } } } CFStringRef CFAppPath = CFStringCreateWithCString(kCFAllocatorDefault, appPath[brandID], kCFStringEncodingUTF8); if (CFAppPath) { // urlref = CFURLCreateWithFileSystemPath(NULL, "/Applications/GridRepublic Desktop.app", kCFURLPOSIXPathStyle, true); CFURLRef urlref = CFURLCreateWithFileSystemPath(NULL, CFAppPath, kCFURLPOSIXPathStyle, true); if (urlref) { err = LSOpenCFURLRef(urlref, NULL); REPORT_ERROR(err); CFRelease(urlref); CFRelease(CFAppPath); } } boinc_sleep(10); // Allow time for current user's Manager to launch client' FindAllVisibleUsers(); for (i=0; i<(int)human_user_IDs.size(); ++i) { pw = getpwuid(human_user_IDs[i]); if (pw == NULL) { continue; } if (strcmp(loginName, pw->pw_name) == 0) continue; #ifdef SANDBOX launchForThisUser = false; if (IsUserLoggedIn(pw->pw_name)) { launchForThisUser = (IsUserMemberOfGroup(pw->pw_name, admin_group_name) || IsUserMemberOfGroup(pw->pw_name, boinc_master_group_name)); } #else // SANDBOX launchForThisUser = true; #endif // SANDBOX if (launchForThisUser) { // Launch Manager hidden (in background, without opening windows) sprintf(s, "su -l \"%s\" -c 'open -jg \"%s\" --args -s'", pw->pw_name, appPath[brandID]); err = callPosixSpawn(s); printf("command: %s returned error %d\n", s, err); fflush(stdout); } } } return 0; } // BOINC Installer.app wrote a file to tell us whether a restart is required Boolean IsRestartNeeded() { char s[MAXPATHLEN]; FILE *restartNeededFile; int value; snprintf(s, sizeof(s), "/tmp/%s/BOINC_restart_flag", tempDirName); restartNeededFile = fopen(s, "r"); if (restartNeededFile) { fscanf(restartNeededFile,"%d", &value); fclose(restartNeededFile); return (value != 0); } return true; } // Some newer versions of the OS define users and groups which may conflict with // our previously created boinc_master or boinc_project user or group. This could // also happen when the user installs new software. So we must check for such // duplicate UserIDs and groupIDs; if found, we delete our user or group so that // the PostInstall application will create a new one that does not conflict. // // Older versions of the installer created our users and groups at the first // unused IDs at or above 25. Apple now recommends using IDs at or above 501, // to reduce the likelihood of conflicts with future UserIDs and groupIDs. // If we have previously created UserIDs and / or groupIDs below 501, this code // now removes them so we can create new ones above 500. void CheckUserAndGroupConflicts() { #ifdef SANDBOX passwd *pw = NULL; group *grp = NULL; gid_t boinc_master_gid = 0, boinc_project_gid = 0; uid_t boinc_master_uid = 0, boinc_project_uid = 0; FILE *f; char cmd[256], buf[256]; int entryCount; OSErr err = noErr; if (compareOSVersionTo(10, 5) < 0) { // This fails under OS 10.4, but should not be needed under OS 10.4 return; } printf("Checking user and group conflicts\n"); fflush(stdout); entryCount = 0; grp = getgrnam(boinc_master_group_name); if (grp) { boinc_master_gid = grp->gr_gid; printf("boinc_master group ID = %d\n", (int)boinc_master_gid); fflush(stdout); if (boinc_master_gid > 500) { sprintf(cmd, "dscl . -search /Groups PrimaryGroupID %d", boinc_master_gid); f = popen(cmd, "r"); REPORT_ERROR(!f); if (f) { while (PersistentFGets(buf, sizeof(buf), f)) { if (strstr(buf, "PrimaryGroupID")) { ++entryCount; } } pclose(f); } } } if ((boinc_master_gid < 501) || (entryCount > 1)) { err = callPosixSpawn ("dscl . -delete /groups/boinc_master"); // User boinc_master must have group boinc_master as its primary group. // Since this group no longer exists, delete the user as well. if (err) { fprintf(stdout, "dscl . -delete /groups/boinc_master returned %d\n", err); fflush(stdout); } err = callPosixSpawn ("dscl . -delete /users/boinc_master"); if (err) { fprintf(stdout, "dscl . -delete /users/boinc_master returned %d\n", err); fflush(stdout); } ResynchDSSystem(); } entryCount = 0; grp = getgrnam(boinc_project_group_name); if (grp) { boinc_project_gid = grp->gr_gid; printf("boinc_project group ID = %d\n", (int)boinc_project_gid); fflush(stdout); if (boinc_project_gid > 500) { sprintf(cmd, "dscl . -search /Groups PrimaryGroupID %d", boinc_project_gid); f = popen(cmd, "r"); REPORT_ERROR(!f); if (f) { while (PersistentFGets(buf, sizeof(buf), f)) { if (strstr(buf, "PrimaryGroupID")) { ++entryCount; } } pclose(f); } } } if ((boinc_project_gid < 501) || (entryCount > 1)) { err = callPosixSpawn ("dscl . -delete /groups/boinc_project"); if (err) { fprintf(stdout, "dscl . -delete /groups/boinc_project returned %d\n", err); fflush(stdout); } // User boinc_project must have group boinc_project as its primary group. // Since this group no longer exists, delete the user as well. err = callPosixSpawn ("dscl . -delete /users/boinc_project"); if (err) { fprintf(stdout, "dscl . -delete /users/boinc_project returned %d\n", err); fflush(stdout); } ResynchDSSystem(); } if ((boinc_master_gid < 500) && (boinc_project_gid < 500)) { return; } entryCount = 0; pw = getpwnam(boinc_master_user_name); REPORT_ERROR(!pw); if (pw) { boinc_master_uid = pw->pw_uid; printf("boinc_master user ID = %d\n", (int)boinc_master_uid); fflush(stdout); sprintf(cmd, "dscl . -search /Users UniqueID %d", boinc_master_uid); f = popen(cmd, "r"); REPORT_ERROR(!f); if (f) { while (PersistentFGets(buf, sizeof(buf), f)) { if (strstr(buf, "UniqueID")) { ++entryCount; } } pclose(f); } } if (entryCount > 1) { err = callPosixSpawn ("dscl . -delete /users/boinc_master"); if (err) { REPORT_ERROR(true); fprintf(stdout, "dscl . -delete /users/boinc_master returned %d\n", err); fflush(stdout); } ResynchDSSystem(); } entryCount = 0; pw = getpwnam(boinc_project_user_name); REPORT_ERROR(!pw); if (pw) { boinc_project_uid = pw->pw_uid; printf("boinc_project user ID = %d\n", (int)boinc_project_uid); fflush(stdout); sprintf(cmd, "dscl . -search /Users UniqueID %d", boinc_project_uid); f = popen(cmd, "r"); REPORT_ERROR(!f); if (f) { while (PersistentFGets(buf, sizeof(buf), f)) { if (strstr(buf, "UniqueID")) { ++entryCount; } } pclose(f); } } if (entryCount > 1) { err = callPosixSpawn ("dscl . -delete /users/boinc_project"); if (err) { REPORT_ERROR(true); fprintf(stdout, "dscl . -delete /users/boinc_project returned %d\n", err); fflush(stdout); } ResynchDSSystem(); } #endif // SANDBOX } enum { kSystemEventsCreator = 'sevs' }; CFStringRef kSystemEventsBundleID = CFSTR("com.apple.systemevents"); char *systemEventsAppName = "System Events"; Boolean SetLoginItemOSAScript(long brandID, Boolean deleteLogInItem, char *userName) { int i, j; char cmd[2048]; char systemEventsPath[1024]; pid_t systemEventsPID; OSErr err, err2; #if USE_OSASCRIPT_FOR_ALL_LOGGED_IN_USERS // NOTE: It may not be necessary to kill and relaunch the // System Events application for each logged in user under High Sierra Boolean isHighSierraOrLater = (compareOSVersionTo(10, 13) >= 0); #endif fprintf(stdout, "Adjusting login items for user %s\n", userName); fflush(stdout); // We must launch the System Events application for the target user err = noErr; systemEventsPath[0] = '\0'; err = GetPathToAppFromID(kSystemEventsCreator, kSystemEventsBundleID, systemEventsPath, sizeof(systemEventsPath)); REPORT_ERROR(err); #if CREATE_LOG if (err == noErr) { print_to_log("SystemEvents is at %s\n", systemEventsPath); } else { print_to_log("GetPathToAppFromID(kSystemEventsCreator, kSystemEventsBundleID) returned error %d ", (int) err); } #endif if (err == noErr) { // Find SystemEvents process. If found, quit it in case // it is running under a different user. fprintf(stdout, "Telling System Events to quit (at start of SetLoginItemOSAScript)\n"); fflush(stdout); systemEventsPID = FindProcessPID(systemEventsAppName, 0); if (systemEventsPID != 0) { err = kill(systemEventsPID, SIGKILL); } if (err != noErr) { REPORT_ERROR(true); fprintf(stdout, "(systemEventsPID, SIGKILL) returned error %d \n", (int) err); fflush(stdout); } // Wait for the process to be gone for (i=0; i<50; ++i) { // 5 seconds max delay SleepSeconds(0.1); // 1/10 second systemEventsPID = FindProcessPID(systemEventsAppName, 0); if (systemEventsPID == 0) break; } if (i >= 50) { REPORT_ERROR(true); fprintf(stdout, "Failed to make System Events quit\n"); fflush(stdout); err = noErr; goto cleanupSystemEvents; } sleep(4); } if (systemEventsPath[0] != '\0') { fprintf(stdout, "Launching SystemEvents for user %s\n", userName); fflush(stdout); for (j=0; j<5; ++j) { sprintf(cmd, "sudo -u \"%s\" open \"%s\"", userName, systemEventsPath); err = callPosixSpawn(cmd); if (err) { REPORT_ERROR(true); fprintf(stdout, "[2] Command: %s returned error %d (try %d of 5)\n", cmd, (int) err, j); fflush(stdout); } // Wait for the process to start for (i=0; i<50; ++i) { // 5 seconds max delay SleepSeconds(0.1); // 1/10 second systemEventsPID = FindProcessPID(systemEventsAppName, 0); if (systemEventsPID != 0) break; } if (i < 50) break; // Exit j loop on success } if (j >= 5) { fprintf(stdout, "Failed to launch System Events for user %s\n", userName); REPORT_ERROR(true); fflush(stdout); err = noErr; goto cleanupSystemEvents; } } sleep(2); for (i=0; i= 50) { REPORT_ERROR(true); fprintf(stdout, "Failed to make System Events quit\n"); fflush(stdout); } sleep(4); return (err == noErr); } // Under OS 10.13 High Sierra, telling System Events to modify Login Items for // users who are not currently logged in no longer works, even when System Events // is running as that user. // So we create a LaunchAgent for that user. The next time that user logs in, the // LaunchAgent will make the desired changes to that user's Login Items, launch // BOINC Manager if appropriate, and delete itself. // // While we could just use a LaunchAgent to launch BOINC Manager on every login // instead of using it to create a Login Item, we still need to remove any branded // Login Items kept from an earlier installation (perhaps before the user upgraded // the OS to High Sierra.) Also, I prefer Login Items because: // * they are more readily visible to a less technically aware user through // System Preferences, and // * they are more easily added or removed through System Preferences, and // * continuing to use them is consistent with older versions of BOINC Manager. // Boolean SetLoginItemLaunchAgent(long brandID, long oldBrandID, Boolean deleteLogInItem, passwd *pw) { struct stat sbuf; char s[2048]; // Create a LaunchAgent to finish installation for the specified user, replacing any LaunchAgent // created previously (such as by Uninstaller or by installing a differently branded BOINC.) // Create LaunchAgents directory for this user if it does not yet exist snprintf(s, sizeof(s), "/Users/%s/Library/LaunchAgents", pw->pw_name); if (stat(s, &sbuf) != 0) { mkdir(s, 0755); chown(s, pw->pw_uid, pw->pw_gid); } snprintf(s, sizeof(s), "/Users/%s/Library/LaunchAgents/edu.berkeley.boinc.plist", pw->pw_name); FILE* f = fopen(s, "w"); if (!f) return false; fprintf(f, "\n"); fprintf(f, "\n"); fprintf(f, "\n"); fprintf(f, "\n"); fprintf(f, "\tLabel\n"); fprintf(f, "\tedu.berkeley.fix_login_items\n"); fprintf(f, "\tProgramArguments\n"); fprintf(f, "\t\n"); fprintf(f, "\t\t/Users/%s/Library/Application Support/BOINC/%s_Finish_Install.app/Contents/MacOS/%s_Finish_Install\n", pw->pw_name, brandName[brandID], brandName[brandID]); if (deleteLogInItem) { fprintf(f, "\t\t-d\n"); fprintf(f, "\t\t%d\n", (int)oldBrandID); } else { fprintf(f, "\t\t-a\n"); fprintf(f, "\t\t%d\n", (int)brandID); } fprintf(f, "\t\n"); if (compareOSVersionTo(13, 0) >= 0) { fprintf(f, "\tAssociatedBundleIdentifiers\n"); fprintf(f, "\tedu.berkeley.boinc.finish-install\n"); } fprintf(f, "\tRunAtLoad\n"); fprintf(f, "\t\n"); fprintf(f, "\n"); fprintf(f, "\n"); fclose(f); chmod(s, 0644); chown(s, pw->pw_uid, pw->pw_gid); if (IsUserLoggedIn(pw->pw_name)) { sprintf(s, "su -l \"%s\" -c 'launchctl unload /Users/%s/Library/LaunchAgents/edu.berkeley.boinc.plist'", pw->pw_name, pw->pw_name); callPosixSpawn(s); sprintf(s, "su -l \"%s\" -c 'launchctl load /Users/%s/Library/LaunchAgents/edu.berkeley.boinc.plist'", pw->pw_name, pw->pw_name); callPosixSpawn(s); } return true; } void DeleteScreenSaverLaunchAgent(passwd *pw) { char cmd[MAXPATHLEN]; sprintf(cmd, "/Users/%s/Library/LaunchAgents/edu.berkeley.boinc-sshelper.plist", pw->pw_name); if (boinc_file_exists(cmd)) { sprintf(cmd, "su -l \"%s\" -c 'launchctl unload /Users/%s/Library/LaunchAgents/edu.berkeley.boinc-sshelper.plist'", pw->pw_name, pw->pw_name); callPosixSpawn(cmd); snprintf(cmd, sizeof(cmd), "/Users/%s/Library/LaunchAgents/edu.berkeley.boinc-sshelper.plist", pw->pw_name); boinc_delete_file(cmd); } } // Sets the skin selection in the specified user's preferences to the specified skin void SetSkinInUserPrefs(char *userName, char *nameOfSkin) { passwd *pw; FILE *oldPrefs, *newPrefs; char oldFileName[MAXPATHLEN], tempFilename[MAXPATHLEN]; char buf[1024]; int wroteSkinName; struct stat sbuf; group *grp; OSStatus statErr; if (nameOfSkin[0]) { sprintf(oldFileName, "/Users/%s/Library/Preferences/BOINC Manager Preferences", userName); sprintf(tempFilename, "/Users/%s/Library/Preferences/BOINC Manager NewPrefs", userName); newPrefs = fopen(tempFilename, "w"); REPORT_ERROR(!newPrefs); if (newPrefs) { wroteSkinName = 0; statErr = stat(oldFileName, &sbuf); oldPrefs = fopen(oldFileName, "r"); if (oldPrefs) { while (fgets(buf, sizeof(buf), oldPrefs)) { if (strstr(buf, "Skin=")) { fprintf(newPrefs, "Skin=%s\n", nameOfSkin); wroteSkinName = 1; } else { fputs(buf, newPrefs); } } fclose(oldPrefs); } if (! wroteSkinName) fprintf(newPrefs, "Skin=%s\n", nameOfSkin); fclose(newPrefs); rename(tempFilename, oldFileName); // Deletes old file if (! statErr) { chown(oldFileName, sbuf.st_uid, sbuf.st_gid); chmod(oldFileName, sbuf.st_mode); } else { chmod(oldFileName, 0664); pw = getpwnam(userName); grp = getgrnam(userName); if (pw && grp) chown(oldFileName, pw->pw_uid, grp->gr_gid); } } } } // Returns true if the user name is in the nologinitems.txt, else false Boolean CheckDeleteFile(char *name) { FILE *f; char buf[64]; size_t len; f = fopen("/Library/Application Support/BOINC Data/nologinitems.txt", "r"); if (!f) return false; while (true) { *buf = '\0'; len = sizeof(buf); fgets(buf, len, f); if (feof(f)) break; strip_cr(buf); if (strcmp(buf, name) == 0) { fclose(f); return true; } } fclose(f); return false; } // FixLaunchServicesDataBase is used in two ways: // // [1] To delete references to old copies of BOINC_Finish_Install, whose // presence, for reasons I don't understand, causes its signing entity // to be shown instead of its application name in the Login Items System // Settings under MacOS 13 Ventura. // NOTE: The new copy of BOINC_Finish_Install must then be registered. // // [2] If there are other copies of BOINC Manager with different branding // on the system, Notifications may display the icon for the wrong // branding, due to the Launch Services database having one of the // other copies of BOINC Manager as the first entry. // This probably will happen only on BOINC development systems where // Xcode has generated copies of BOINC Manager. // // Each user has their own copy of the Launch Services database, so these // must be done for each user. // static void FixLaunchServicesDataBase(uid_t userID, char *pathToKeep, char *theBundleID) { uid_t saved_uid; char foundPath[MAXPATHLEN]; char cmd[MAXPATHLEN+250]; long i, n; CFArrayRef appRefs = NULL; OSStatus err; if (compareOSVersionTo(10, 8) < 0) { return; // Notifications before OS 10.8 just bounce our Dock icon } saved_uid = geteuid(); CFStringRef bundleID = CFStringCreateWithCString(NULL, theBundleID, kCFStringEncodingUTF8); if (LSCopyApplicationURLsForBundleIdentifier) { // Weak linked; not available before OS 10.10 seteuid(userID); // Temporarily set effective uid to this user appRefs = LSCopyApplicationURLsForBundleIdentifier(bundleID, NULL); seteuid(saved_uid); // Set effective uid back to privileged user if (appRefs == NULL) { printf("Call to LSCopyApplicationURLsForBundleIdentifier(%d, %s, %s) returned NULL\n", userID, pathToKeep ? pathToKeep : "NULL", theBundleID); fflush(stdout); goto registerOurApp; } n = CFArrayGetCount(appRefs); // Returns all results at once, in database order printf("LSCopyApplicationURLsForBundleIdentifier(%d, %s, %s) returned %ld results\n", userID, pathToKeep ? pathToKeep : "NULL", theBundleID, n); fflush(stdout); } else { n = 500; // Prevent infinite loop } for (i=0; ipw_uid; setuid(login_uid); seteuid(login_uid); } static char * PersistentFGets(char *buf, size_t buflen, FILE *f) { char *p = buf; size_t len = buflen; size_t datalen = 0; memset(buf, 0, buflen); while (datalen < (buflen - 1)) { fgets(p, len, f); if (feof(f)) break; if (ferror(f) && (errno != EINTR)) break; if (strchr(buf, '\n')) break; datalen = strlen(buf); p = buf + datalen; len -= datalen; } return (buf[0] ? buf : NULL); } // Because language preferences are set on a per-user basis, we // must get the preferred languages while set to the current // user, before the Apple Installer switches us to root. // So we get the preferred languages in our BOINC Installer.app // which writes them to a temporary file which we retrieve here. // We must do it this way because, for unknown reasons, the // CFBundleCopyLocalizationsForPreferences() API does not work // correctly if we seteuid and setuid to the logged in user by // calling SetEUIDBackToUser() after running as root. // static void LoadPreferredLanguages(){ char s[MAXPATHLEN]; FILE *f; int i; char *p; char language[32]; BOINCTranslationInit(); // BOINC Installer.app wrote a list of our preferred languages to a temp file snprintf(s, sizeof(s), "/tmp/%s/BOINC_preferred_languages", tempDirName); f = fopen(s, "r"); if (!f) return; for (i=0; igr_mem[i]) != NULL) { // Step through all users in group groupName if (strcmp(p, userName) == 0) { return true; } ++i; } return false; } // OS 10.7 dscl merge command has a bug such that the command: // dscl . -merge /Groups/GROUPNAME users USERNAME // adds the user to the group even if it was already a member, resulting in // duplicate (multiple) entries. Earlier BOINC versions used this command // but did not check for this, so we remove duplicate entries if present. // Note: We now avoid this problem by instead using the command: // dscl . -merge /Groups/GROUPNAME GroupMembership USERNAME // which correctly avoids duplication. int CountGroupMembershipEntries(const char *userName, const char *groupName) { int count = 0; char cmd[512], buf[2048], escapedUserName[1024]; FILE *f; char *p, *q; // getgrnam(groupName)->gr_mem[] only returns one entry, so we must use dscl escape_url(userName, escapedUserName, sizeof(escapedUserName)); // Avoid confusion if name has embedded spaces sprintf(cmd, "dscl -url . -read /Groups/%s GroupMembership", groupName); f = popen(cmd, "r"); if (f == NULL) { REPORT_ERROR(true); return 0; } while (PersistentFGets(buf, sizeof(buf), f)) { p = buf; while (p) { p = strstr(p, escapedUserName); if (p) { q = p-1; p += strlen(escapedUserName); // Count only whole words (preceded and followed by white space) so // that if we have both 'jon' and 'jones' we don't count 'jon' twice if (isspace(*q) && isspace(*p)) { ++ count; } } } } pclose(f); return count; } // Find all visible users. // If user is a member of group admin, add user to groups boinc_master and boinc_project. // Optionally add non-admin users to group boinc_master but not to group boinc_project. // Set login item for all members of group boinc_master to launch BOINC Manager. // If our install package included a skin, set those user's preferences to use that skin. // Optionally set BOINC as screensaver for all users running BOINC. OSErr UpdateAllVisibleUsers(long brandID, long oldBrandID) { passwd *pw; uid_t saved_uid; Boolean deleteLoginItem; char human_user_name[256]; char s[2*MAXPATHLEN]; Boolean saverAlreadySetForAll = true; Boolean setSaverForAllUsers = false; Boolean allNonAdminUsersAreSet = true; Boolean allowNonAdminUsersToRunBOINC = false; int err; Boolean isAdminGroupMember, isBMGroupMember; struct stat sbuf; char cmd[2*MAXPATHLEN]; #ifdef SANDBOX int BMGroupMembershipCount, BPGroupMembershipCount; #endif int i; int userIndex; char path[MAXPATHLEN]; // char nameOfSkin[256]; // FindSkinName(nameOfSkin, sizeof(nameOfSkin)); // Step through all users puts("Beginning first pass through all users\n"); fflush(stdout); saved_uid = geteuid(); FindAllVisibleUsers(); for (userIndex=0; userIndex< (int)human_user_names.size(); ++userIndex) { strlcpy(human_user_name, human_user_names[userIndex].c_str(), sizeof(human_user_name)); printf("[1] Checking user %s\n", human_user_name); fflush(stdout); // getpwnam works with either the full / login name (pw->pw_gecos) // or the short / Posix name (pw->pw_name) pw = getpwnam(human_user_name); if (pw == NULL) { printf("[1] %s not in getpwnam data base\n", human_user_name); fflush(stdout); continue; } printf("[1] User %s: Posix name=%s, Full name=%s\n", human_user_name, pw->pw_name, pw->pw_gecos); fflush(stdout); #ifdef SANDBOX isAdminGroupMember = false; isBMGroupMember = false; isAdminGroupMember = IsUserMemberOfGroup(pw->pw_name, admin_group_name); if (isAdminGroupMember) { // User is a member of group admin, so add user to groups boinc_master and boinc_project printf("[1] User %s is a member of group admin\n", pw->pw_name); fflush(stdout); } else { isBMGroupMember = IsUserMemberOfGroup(pw->pw_name, boinc_master_group_name); if (isBMGroupMember) { // User is a member of group boinc_master printf("[1] Non-admin user %s is a member of group boinc_master\n", pw->pw_name); fflush(stdout); } else { allNonAdminUsersAreSet = false; } } #else // SANDBOX isGroupMember = true; #endif // SANDBOX if (isAdminGroupMember || isBMGroupMember) { if ((strcmp(loginName, human_user_name) == 0) || (strcmp(loginName, pw->pw_name) == 0) || (strcmp(loginName, pw->pw_gecos) == 0)) { currentUserCanRunBOINC = true; } err = GetCurrentScreenSaverSelection(pw, s, sizeof(s) -1); if (err == noErr) { if (strcmp(s, saverName[brandID])) { saverAlreadySetForAll = false; } } printf("[1] Current Screensaver Selection for user %s is: \"%s\"\n", pw->pw_name, s); fflush(stdout); } // End if (isGroupMember) } // End for (userIndex=0; userIndex< human_user_names.size(); ++userIndex) ResynchDSSystem(); if (allNonAdminUsersAreSet) { puts("[2] All non-admin users are already members of group boinc_master\n"); fflush(stdout); } else { if (gCommandLineInstall) { err = stat("/tmp/nonadminusersok.txt", &sbuf); if (err == noErr) { puts("nonadminusersok.txt file detected\n"); fflush(stdout); unlink("/tmp/nonadminusersok.txt"); allowNonAdminUsersToRunBOINC = true; currentUserCanRunBOINC = true; saverAlreadySetForAll = false; } } else { if (ShowMessage(true, (char *)_("Users who are permitted to administer this computer will automatically be allowed to " "run and control %s.\n\n" "Do you also want non-administrative users to be able to run and control %s on this Mac?"), brandName[brandID], brandName[brandID]) ) { allowNonAdminUsersToRunBOINC = true; currentUserCanRunBOINC = true; saverAlreadySetForAll = false; printf("[2] User answered Yes to allowing non-admin users to run %s\n", brandName[brandID]); fflush(stdout); } else { printf("[2] User answered No to allowing non-admin users to run %s\n", brandName[brandID]); fflush(stdout); } } } // As of MacOS 14.0 Sonoma, we can't set the screensaver //automatically. I have filed bug report FB13270885 about this. // The response to my bug report is that it will be fixed in a // future rlease of MacOS. // See also the comment at top of SetScreenSaverSelection(). if (compareOSVersionTo(14, 0) < 0) { if (! saverAlreadySetForAll) { if (gCommandLineInstall) { err = stat("/tmp/setboincsaver.txt", &sbuf); if (err == noErr) { puts("setboincsaver.txt file detected\n"); fflush(stdout); unlink("/tmp/setboincsaver.txt"); setSaverForAllUsers = true; } } else { setSaverForAllUsers = ShowMessage(true, (char *)_("Do you want to set %s as the screensaver for all %s users on this Mac?"), brandName[brandID], brandName[brandID]); } } } // Step through all users a second time, setting non-admin users and / or our screensaver puts("Beginning second pass through all users\n"); fflush(stdout); for (userIndex=0; userIndex<(int)human_user_names.size(); ++userIndex) { strlcpy(human_user_name, human_user_names[userIndex].c_str(), sizeof(human_user_name)); printf("[2] Checking user %s\n", human_user_name); fflush(stdout); pw = getpwnam(human_user_name); if (pw == NULL) { // "Deleted Users", "Shared", etc. printf("[2] %s not in getpwnam data base\n", human_user_name); fflush(stdout); continue; } printf("[2] User %s: Posix name=%s, Full name=%s\n", human_user_name, pw->pw_name, pw->pw_gecos); fflush(stdout); #ifdef SANDBOX isAdminGroupMember = false; isBMGroupMember = false; isAdminGroupMember = IsUserMemberOfGroup(pw->pw_name, admin_group_name); if (isAdminGroupMember) { // User is a member of group admin, so add user to groups boinc_master and boinc_project printf("[2] User %s is a member of group admin\n", pw->pw_name); fflush(stdout); } // If allNonAdminUsersAreSet, some older BOINC versions added non-admin users only to group // boinc_master; ensure all permitted BOINC users are also members of group boinc_project if (isAdminGroupMember || allowNonAdminUsersToRunBOINC || allNonAdminUsersAreSet) { // OS 10.7 dscl merge command has a bug that it adds the user to the group even if // it was already a member, resulting in duplicate (multiple) entries. Earlier BOINC // versions did not check for this, so we remove duplicate entries if present. BMGroupMembershipCount = CountGroupMembershipEntries(pw->pw_name, boinc_master_group_name); printf("[2] User %s found in group %s member list %d times\n", pw->pw_name, boinc_master_group_name, BMGroupMembershipCount); fflush(stdout); if (BMGroupMembershipCount == 0) { sprintf(cmd, "dscl . -merge /groups/%s GroupMembership \"%s\"", boinc_master_group_name, pw->pw_name); err = callPosixSpawn(cmd); REPORT_ERROR(err); printf("[2] %s returned %d\n", cmd, err); fflush(stdout); isBMGroupMember = true; } else { isBMGroupMember = true; for (i=1; ipw_name); err = callPosixSpawn(cmd); REPORT_ERROR(err); printf("[2] %s returned %d\n", cmd, err); fflush(stdout); } } BPGroupMembershipCount = CountGroupMembershipEntries(pw->pw_name, boinc_project_group_name); printf("[2] User %s found in group %s member list %d times\n", pw->pw_name, boinc_project_group_name, BPGroupMembershipCount); fflush(stdout); if (BPGroupMembershipCount == 0) { sprintf(cmd, "dscl . -merge /groups/%s GroupMembership \"%s\"", boinc_project_group_name, pw->pw_name); err = callPosixSpawn(cmd); REPORT_ERROR(err); printf("[2] %s returned %d\n", cmd, err); fflush(stdout); } else { for (i=1; ipw_name); err = callPosixSpawn(cmd); REPORT_ERROR(err); printf("[2] %s returned %d\n", cmd, err); fflush(stdout); } } } #else // SANDBOX isBMGroupMember = true; #endif // SANDBOX saved_uid = geteuid(); deleteLoginItem = CheckDeleteFile(human_user_name); if (CheckDeleteFile(pw->pw_name)) { deleteLoginItem = true; } if (CheckDeleteFile(pw->pw_gecos)) { deleteLoginItem = true; } if (!isBMGroupMember) { deleteLoginItem = true; } // Set login item for this user bool useOSASript = false; if ((compareOSVersionTo(10, 13) < 0) || (strcmp(loginName, human_user_name) == 0) || (strcmp(loginName, pw->pw_name) == 0) || (strcmp(loginName, pw->pw_gecos) == 0)) { useOSASript = true; } #if USE_OSASCRIPT_FOR_ALL_LOGGED_IN_USERS if (! useOSASript) { useOSASript = IsUserLoggedIn(pw->pw_name); } #endif if (useOSASript) { snprintf(s, sizeof(s), "/Users/%s/Library/LaunchAgents/edu.berkeley.boinc.plist", pw->pw_name); boinc_delete_file(s); for (i=0; i< NUMBRANDS; i++) { snprintf(s, sizeof(s), "rm -fR \"/Users/%s/Library/Application Support/BOINC/%s_Finish_Install.app\"", pw->pw_name, brandName[i]); err = callPosixSpawn(s); REPORT_ERROR(err); if (err) { printf("Command %s returned error %d\n", s, err); fflush(stdout); } snprintf(s, sizeof(s), "rm -fR \"/Users/%s/Library/Application Support/BOINC/%s_Finish_Uninstall.app\"", pw->pw_name, brandName[i]); err = callPosixSpawn(s); REPORT_ERROR(err); if (err) { printf("Command %s returned error %d\n", s, err); fflush(stdout); } } if (compareOSVersionTo(13, 0) >= 0) { deleteLoginItem = true; // Use LaunchAgent to autostart BOINC Manager snprintf(s, sizeof(s), "open \"/Library/Application Support/BOINC Data/%s_Finish_Install.app\"", brandName[brandID]); err = callPosixSpawn(s); REPORT_ERROR(err); if (err) { printf("Command %s returned error %d\n", s, err); fflush(stdout); } } printf("[2] calling SetLoginItemOSAScript for user %s, euid = %d, deleteLoginItem = %d\n", pw->pw_name, geteuid(), deleteLoginItem); fflush(stdout); SetLoginItemOSAScript(brandID, deleteLoginItem, pw->pw_name); } else { printf("[2] calling FixLaunchServicesDataBase for Finish_Install for user %s\n", pw->pw_name); fflush(stdout); FixLaunchServicesDataBase(pw->pw_uid, NULL, "edu.berkeley.boinc.finish-install"); snprintf(s, sizeof(s), "mkdir -p \"/Users/%s/Library/Application Support/BOINC/\"", pw->pw_name); err = callPosixSpawn(s); REPORT_ERROR(err); if (err) { printf("Command %s returned error %d\n", s, err); fflush(stdout); } snprintf(s, sizeof(s), "/Users/%s/Library/Application Support/BOINC/", pw->pw_name); chmod(s, 0771); chown(s, pw->pw_uid, pw->pw_gid); for (i=0; i< NUMBRANDS; i++) { // If we previously ran the installer for any brand but did not log in to // this user, remove the user's unused BOINC_Manager_Finish_Install file. snprintf(s, sizeof(s), "rm -fR \"/Users/%s/Library/Application Support/BOINC/%s_Finish_Install.app\"", pw->pw_name, brandName[i]); err = callPosixSpawn(s); REPORT_ERROR(err); if (err) { printf("Command %s returned error %d\n", s, err); fflush(stdout); } // If we previously ran the installer for any brand but did not log in to // this user, remove the user's unused BOINC_Manager_Finish_Uninstall file. snprintf(s, sizeof(s), "rm -fR \"/Users/%s/Library/Application Support/BOINC/%s_Finish_Uninstall.app\"", pw->pw_name, brandName[i]); err = callPosixSpawn(s); REPORT_ERROR(err); if (err) { printf("Command %s returned error %d\n", s, err); fflush(stdout); } } getPathToThisApp(path, sizeof(path)); snprintf(s, sizeof(s), "cp -fR \"%s/Contents/Resources/%s_Finish_Install.app\" \"/Users/%s/Library/Application Support/BOINC/\"", path, brandName[brandID], pw->pw_name); err = callPosixSpawn(s); REPORT_ERROR(err); if (err) { printf("Command %s returned error %d\n", s, err); fflush(stdout); } snprintf(s, sizeof(s), "chown -fR %s \"/Users/%s/Library/Application Support/BOINC/%s_Finish_Install.app\"", pw->pw_name, pw->pw_name, brandName[brandID]); err = callPosixSpawn(s); REPORT_ERROR(err); if (err) { printf("Command %s returned error %d\n", s, err); fflush(stdout); } // Register this copy of BOINCFinish_Install.app. See comments on FixLaunchServicesDataBase. sprintf(s, "sudo -u #%d /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Versions/Current/Support/lsregister \"/Users/%s/Library/Application Support/BOINC/%s_Finish_Install.app\"", pw->pw_uid, pw->pw_name, brandName[brandID]); err = callPosixSpawn(s); if (err) { printf("*** user %s: lsregister call returned error %d for %s_Finish_Install.app\n", pw->pw_name, err, brandName[brandID]); fflush(stdout); } printf("[2] calling SetLoginItemLaunchAgent for user %s, euid = %d, deleteLoginItem = %d\n", pw->pw_name, geteuid(), deleteLoginItem); fflush(stdout); // SetLoginItemLaunchAgent will run helper app which will call FixLaunchServicesDataBase() SetLoginItemLaunchAgent(brandID, oldBrandID, deleteLoginItem, pw); } printf("[2] calling FixLaunchServicesDataBase for BOINC Manager for user %s\n", pw->pw_name); fflush(stdout); FixLaunchServicesDataBase(pw->pw_uid, appPath[brandID], "edu.berkeley.boinc"); if (isBMGroupMember) { // For some reason we need to call getpwnam again on OS 10.5 pw = getpwnam(human_user_name); if (pw == NULL) { // "Deleted Users", "Shared", etc. printf("[2] ERROR: %s was in getpwnam data base but now is not!\n", human_user_name); fflush(stdout); continue; } SetSkinInUserPrefs(pw->pw_name, skinName[brandID]); if (setSaverForAllUsers) { seteuid(pw->pw_uid); // Temporarily set effective uid to this user sprintf(s, "/Library/Screen Savers/%s.saver", saverName[brandID]); err = SetScreenSaverSelection(saverName[brandID], s, 0); seteuid(saved_uid); // Set effective uid back to privileged user // This seems to work also: // sprintf(s, "su -l \"%s\" -c 'defaults -currentHost write com.apple.screensaver moduleDict -dict moduleName \"%s\" path \"/Library/Screen Savers/%s.saver\" type 0'", pw->pw_name, saverName[brandID], s); // callPosixSpawn(s); } if (compareOSVersionTo(10, 15) >= 0) { // Under Catalina, Screensaver output files are put in the user's Containers // directory. Create the directory if it doesn't exist and create a symbolic // link to it in the normal per-user BOINC directory snprintf(s, sizeof(s), "/Users/%s/Library/Application Support/BOINC", pw->pw_name); if (stat(s, &sbuf) != 0) { snprintf(cmd, sizeof(cmd), "sudo -u \"%s\" mkdir -p -m 0775 \"/Users/%s/Library/Application Support/BOINC\"", pw->pw_name, pw->pw_name); err = callPosixSpawn(cmd); REPORT_ERROR(err); printf("[2] %s returned %d\n", cmd, err); fflush(stdout); } snprintf(s, sizeof(s), "/Users/%s/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Application Support/BOINC", pw->pw_name); if (stat(s, &sbuf) != 0) { // mkdir -p creates intermediate directories as required snprintf(cmd, sizeof(cmd), "sudo -u \"%s\" mkdir -p -m 0700 \"/Users/%s/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Application Support\"", pw->pw_name, pw->pw_name); err = callPosixSpawn(cmd); REPORT_ERROR(err); printf("[2] %s returned %d\n", cmd, err); fflush(stdout); snprintf(cmd, sizeof(cmd), "sudo -u \"%s\" mkdir -m 0775 \"/Users/%s/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Application Support/BOINC\"", pw->pw_name, pw->pw_name); err = callPosixSpawn(cmd); REPORT_ERROR(err); printf("[2] %s returned %d\n", cmd, err); fflush(stdout); } snprintf(s, sizeof(s), "/Users/%s/Library/Application Support/BOINC/ScreenSaver Logs", pw->pw_name); if (lstat(s, &sbuf) != 0) { snprintf(cmd, sizeof(cmd), "sudo -u \"%s\" ln -s \"/Users/%s/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Application Support/BOINC\" \"/Users/%s/Library/Application Support/BOINC/ScreenSaver Logs\"", pw->pw_name, pw->pw_name, pw->pw_name); err = callPosixSpawn(cmd); REPORT_ERROR(err); printf("[2] %s returned %d\n", cmd, err); fflush(stdout); } } } // We no longer use a ScreenSaver LaunchAgent. // Delete it if it was installed by an older version of BOINC DeleteScreenSaverLaunchAgent(pw); // Delete the BOINC Manager's wxSingleInstanceChecker lock file, in case // it was not deleted (such as due to a crash.) // Lock file name always has "BOINC Manager" even if the application is // branded, due to SetAppName(wxT("BOINC Manager")) in CBOINCGUIApp::OnInit(). // This path must match that in CBOINCGUIApp::DetectDuplicateInstance() sprintf(cmd, "sudo -u \"%s\" rm -f \"/Users/%s/Library/Application Support/BOINC/BOINC Manager-%s\"", pw->pw_name, pw->pw_name, pw->pw_name); err = callPosixSpawn(cmd); REPORT_ERROR(err); printf("[2] %s returned %d\n", cmd, err); fflush(stdout); } // End for (userIndex=0; userIndex< human_user_names.size(); ++userIndex) ResynchDSSystem(); BOINCTranslationCleanup(); return noErr; } // As of MacOS 14.0 Sonoma, this code no longer will detect the current screensaver, // and will need to be rewritten. See the comment at top of SetScreenSaverSelection(). // It is unclear whether this will be fixed in a uture rlease of MacOS. // This Applescript stoll works: // tell application "System Events" to set mysaver to name of current screen saver OSErr GetCurrentScreenSaverSelection(passwd *pw, char *moduleName, size_t maxLen) { char buf[1024]; FILE *f; char *p, *q; int i; *moduleName = '\0'; sprintf(buf, "su -l \"%s\" -c 'defaults -currentHost read com.apple.screensaver moduleDict'", pw->pw_name); f = popen(buf, "r"); if (f == NULL) { REPORT_ERROR(true); return 0; } while (PersistentFGets(buf, sizeof(buf), f)) { p = strstr(buf, "moduleName = "); if (p) { p += 13; // Point past "moduleName = " q = moduleName; for (i=0; i buf) { if (*p != ' ') break; --p; } *(p+1) = '\0'; human_user_names.push_back(string(buf)); *(p+1) = ' '; } } pclose(f); } for (userIndex=human_user_names.size(); userIndex>0; --userIndex) { flag = 0; strlcpy(human_user_name, human_user_names[userIndex-1].c_str(), sizeof(human_user_name)); sprintf(cmd, "dscl . -read \"/Users/%s\" NFSHomeDirectory", human_user_name); f = popen(cmd, "r"); REPORT_ERROR(!f); if (f) { while (PersistentFGets(buf, sizeof(buf), f)) { p = strrchr(buf, ' '); if (p) { if (strstr(p, "/var/empty") != NULL) { flag = 1; break; } } } pclose(f); } if (flag) { sprintf(cmd, "dscl . -read \"/Users/%s\" UserShell", human_user_name); f = popen(cmd, "r"); REPORT_ERROR(!f); if (f) { while (PersistentFGets(buf, sizeof(buf), f)) { p = strrchr(buf, ' '); if (p) { if (strstr(p, "/usr/bin/false") != NULL) { flag |= 2; break; } } } pclose(f); } } if (flag == 3) { // if (Home Directory == "/var/empty") && (UserShell == "/usr/bin/false") human_user_names.erase(human_user_names.begin()+userIndex-1); human_user_IDs.erase(human_user_IDs.begin()+userIndex-1); } } } long GetBrandID(char *path) { long iBrandId; iBrandId = 0; // Default value FILE *f = fopen(path, "r"); if (f) { fscanf(f, "BrandId=%ld\n", &iBrandId); fclose(f); } if ((iBrandId < 0) || (iBrandId > (NUMBRANDS-1))) { iBrandId = 0; } return iBrandId; } int TestRPCBind() { sockaddr_in addr; int lsock; int retval; lsock = (int)socket(AF_INET, SOCK_STREAM, 0); if (lsock < 0) return -153; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(31416); addr.sin_addr.s_addr = htonl(INADDR_ANY); int one = 1; retval = setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, (char*)&one, 4); if (! retval) retval = bind(lsock, (const sockaddr*)(&addr), (socklen_t)sizeof(addr)); if (! retval) retval = listen(lsock, 999); close(lsock); return retval; } #ifdef __arm64__ int check_rosetta2_installed() { int prog; const char * data_dir = "/Library/Application Support/BOINC Data"; char execpath[MAXPATHLEN]; int retval = 0; // write the EMULATED_CPU_INFO into the BOINC data dir // the executable should be in BOINC data dir strncpy(execpath, data_dir, sizeof(execpath)); strncat(execpath, "/" EMULATED_CPU_INFO_EXECUTABLE, sizeof(execpath) - strlen(execpath) - 1); int argc = 1; char* const argv[2] = { const_cast(execpath), NULL }; retval = run_program( data_dir, execpath, argc, argv, prog ); if (retval) { return retval; } int status; retval = get_exit_status(prog, status, -1); if (retval) { if (WIFEXITED(retval)) { return (WEXITSTATUS(retval)); } else if (WIFSIGNALED(retval)) { return (WTERMSIG(retval)); } else { return -1; } } return 0; } int optionally_install_rosetta2() { int err = 0; int i = 0; const char *cmd = "/usr/sbin/softwareupdate --install-rosetta --agree-to-license"; Boolean answer = ShowMessage(true, (char *)_("BOINC can run project applications written for intel Macs if Rosetta 2 is installed.\n\n" "Do you want to install Rosetta 2 now?" )); printf("User answered %s to installing Rosetta 2\n", answer? "yes" : "no"); fflush(stdout); if (answer) { err = callPosixSpawn(cmd); REPORT_ERROR(err); printf("%s returned %d\n", cmd, err); fflush(stdout); if (err) return err; // Wait up to 20 seconds for system to install Rosetta 2. // My tests seem to show that callPosixSpawn() does not return until after // installaton of Rosetta 2 is complete, but do this anyway to be sure. for (;;) { err = check_rosetta2_installed(); if (err == 0) break; if (++i > 20) break; boinc_sleep(1); } printf("check_rosetta2_installed() returned %d after %d seconds.\n", err, i); fflush(stdout); } return err; } #endif // __arm64__ pid_t FindProcessPID(char* name, pid_t thePID) { FILE *f; char buf[1024]; size_t n = 0; pid_t aPID; if (name != NULL) // Search ny name n = strlen(name); f = popen("ps -a -x -c -o command,pid", "r"); if (f == NULL) { REPORT_ERROR(true); return 0; } while (PersistentFGets(buf, sizeof(buf), f)) { if (name != NULL) { // Search by name if (strncmp(buf, name, n) == 0) { aPID = atol(buf+16); pclose(f); return aPID; } } else { // Search by PID aPID = atol(buf+16); if (aPID == thePID) { pclose(f); return aPID; } } } pclose(f); return 0; } // Uses usleep to sleep for full duration even if a signal is received static void SleepSeconds(double seconds) { double end_time = dtime() + seconds - 0.01; // sleep() and usleep() can be interrupted by SIGALRM, // so we may need multiple calls // while (1) { if (seconds >= 1) { sleep((unsigned int) seconds); } else { usleep((int)fmod(seconds*1000000, 1000000)); } seconds = end_time - dtime(); if (seconds <= 0) break; } } static OSErr QuitAppleEventHandler( const AppleEvent *appleEvt, AppleEvent* reply, UInt32 refcon ) { gQuitFlag = true; return noErr; } #define NOT_IN_TOKEN 0 #define IN_SINGLE_QUOTED_TOKEN 1 #define IN_DOUBLE_QUOTED_TOKEN 2 #define IN_UNQUOTED_TOKEN 3 static int parse_posix_spawn_command_line(char* p, char** argv) { int state = NOT_IN_TOKEN; int argc=0; while (*p) { switch(state) { case NOT_IN_TOKEN: if (isspace(*p)) { } else if (*p == '\'') { p++; argv[argc++] = p; state = IN_SINGLE_QUOTED_TOKEN; break; } else if (*p == '\"') { p++; argv[argc++] = p; state = IN_DOUBLE_QUOTED_TOKEN; break; } else { argv[argc++] = p; state = IN_UNQUOTED_TOKEN; } break; case IN_SINGLE_QUOTED_TOKEN: if (*p == '\'') { if (*(p-1) == '\\') break; *p = 0; state = NOT_IN_TOKEN; } break; case IN_DOUBLE_QUOTED_TOKEN: if (*p == '\"') { if (*(p-1) == '\\') break; *p = 0; state = NOT_IN_TOKEN; } break; case IN_UNQUOTED_TOKEN: if (isspace(*p)) { *p = 0; state = NOT_IN_TOKEN; } break; } p++; } argv[argc] = 0; return argc; } #include int callPosixSpawn(const char *cmdline) { char command[1024]; char progName[1024]; char progPath[MAXPATHLEN]; char* argv[100]; int argc __attribute__((unused)) = 0; char *p; pid_t thePid = 0; int result = 0; int status = 0; extern char **environ; // Make a copy of cmdline because parse_posix_spawn_command_line modifies it strlcpy(command, cmdline, sizeof(command)); argc = parse_posix_spawn_command_line(const_cast(command), argv); strlcpy(progPath, argv[0], sizeof(progPath)); strlcpy(progName, argv[0], sizeof(progName)); p = strrchr(progName, '/'); if (p) { argv[0] = p+1; } else { argv[0] = progName; } #if VERBOSE_TEST print_to_log("***********"); for (int i=0; i