/** * @file mac_updater.cpp * @brief * * $LicenseInfo:firstyear=2006&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2010, Linden Research, Inc. * * This library 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; * version 2.1 of the License only. * * This library 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 this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "linden_common.h" #include #include #include #include #include #include #include #include #include "llerror.h" #include "lltimer.h" #include "lldir.h" #include "llfile.h" #include "llstring.h" #include "llerrorcontrol.h" #include "mac_updater.h" #include pthread_t updatethread; LLMacUpdater* LLMacUpdater::sInstance = NULL; LLMacUpdater::LLMacUpdater(): mUpdateURL (NULL), mProductName (NULL), mBundleID (NULL), mDmgFile (NULL), mMarkerPath (NULL) { sInstance = this; } void LLMacUpdater::doUpdate() { // We assume that all the logs we're looking for reside on the current drive gDirUtilp->initAppDirs("SecondLife"); LLError::initForApplication( gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, "")); // Rename current log file to ".old" std::string old_log_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "updater.log.old"); std::string log_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "updater.log"); LLFile::rename(log_file.c_str(), old_log_file.c_str()); // Set the log file to updater.log LLError::logToFile(log_file); if ((mUpdateURL == NULL) && (mDmgFile == NULL)) { llinfos << "Usage: mac_updater -url | -dmg [-name ] [-program ]" << llendl; exit(1); } else { llinfos << "Update url is: " << mUpdateURL << llendl; if (mProductName) { llinfos << "Product name is: " << *mProductName << llendl; } else { mProductName = new std::string("Second Life"); } if (mBundleID) { llinfos << "Bundle ID is: " << *mBundleID << llendl; } else { mBundleID = new std::string("com.secondlife.indra.viewer"); } } llinfos << "Starting " << *mProductName << " Updater" << llendl; pthread_create(&updatethread, NULL, &sUpdatethreadproc, NULL); void *threadresult; pthread_join(updatethread, &threadresult); if(gCancelled || gFailure) { sendStopAlert(); if(mMarkerPath != 0) { // Create a install fail marker that can be used by the viewer to // detect install problems. std::ofstream stream(mMarkerPath->c_str()); if(stream) stream << -1; } exit(-1); } else { exit(0); } return; } //SPATTERS TODO this should be moved to lldir_mac.cpp const std::string LLMacUpdater::walkParents( signed int depth, const std::string& childpath ) { boost::filesystem::path fullpath(childpath.c_str()); while (depth > 0 && fullpath.has_parent_path()) { fullpath = boost::filesystem::path(fullpath.parent_path()); --depth; } return fullpath.string(); } //#if 0 //size_t curl_download_callback(void *data, size_t size, size_t nmemb, // void *user_data) //{ // S32 bytes = size * nmemb; // char *cdata = (char *) data; // for (int i =0; i < bytes; i += 1) // { // gServerResponse.append(cdata[i]); // } // return bytes; //} //#endif int curl_progress_callback_func(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow) { int max = (int)(dltotal / 1024.0); int cur = (int)(dlnow / 1024.0); setProgress(cur, max); if(gCancelled) return(1); return(0); } bool LLMacUpdater::isApplication(const std::string& app_str) { return !(bool) app_str.compare( app_str.length()-4, 4, ".app"); } // Search through the directory specified by 'parent' for an item that appears to be a Second Life viewer. bool LLMacUpdater::findAppBundleOnDiskImage(const boost::filesystem::path& dir_path, boost::filesystem::path& path_found) { if ( !boost::filesystem::exists( dir_path ) ) return false; boost::filesystem::directory_iterator end_itr; for ( boost::filesystem::directory_iterator itr( dir_path ); itr != end_itr; ++itr ) { if ( boost::filesystem::is_directory(itr->status()) ) { std::string dir_name = itr->path().string(); if ( isApplication(dir_name) ) { if(isFSRefViewerBundle(dir_name)) { llinfos << dir_name << " is the one" << llendl; path_found = itr->path(); return true; } } } } return false; } bool LLMacUpdater::verifyDirectory(const boost::filesystem::path* directory, bool isParent) { bool replacingTarget; std::string app_str = directory->string(); if (boost::filesystem::is_directory(*directory)) { // This is fine, just means we're not replacing anything. replacingTarget = true; } else { replacingTarget = isParent; } //Check that the directory is writeable. if(!isDirWritable(app_str)) { // Parent directory isn't writable. llinfos << "Target directory not writable." << llendl; replacingTarget = false; } return replacingTarget; } bool LLMacUpdater::getViewerDir(boost::filesystem::path &app_dir) { std::string app_dir_str; //Walk up 6 levels from the App Updater's installation point. app_dir_str = walkParents( 6, *mApplicationPath ); app_dir = boost::filesystem::path(app_dir_str); //Check to see that the directory's name ends in .app Lame but it's the best thing we have to go on. //If it's not there, we're going to default to /Applications/VIEWERNAME if (!isApplication(app_dir_str)) { llinfos << "Target search failed, defaulting to /Applications/" << *mProductName << ".app." << llendl; std::string newpath = std::string("/Applications/") + mProductName->c_str(); app_dir = boost::filesystem::path(newpath); } return verifyDirectory(&app_dir); } bool LLMacUpdater::downloadDMG(const std::string& dmgName, boost::filesystem::path* temp_dir) { LLFILE *downloadFile = NULL; char temp[PATH_MAX] = ""; /* Flawfinder: ignore */ chdir(temp_dir->string().c_str()); snprintf(temp, sizeof(temp), "SecondLife.dmg"); downloadFile = LLFile::fopen(temp, "wb"); /* Flawfinder: ignore */ if(downloadFile == NULL) { return false; } bool success = false; CURL *curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); // curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &curl_download_callback); curl_easy_setopt(curl, CURLOPT_FILE, downloadFile); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, &curl_progress_callback_func); curl_easy_setopt(curl, CURLOPT_URL, mUpdateURL); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); sendProgress(0, 1, std::string("Downloading...")); CURLcode result = curl_easy_perform(curl); curl_easy_cleanup(curl); if(gCancelled) { llinfos << "User cancel, bailing out."<< llendl; goto close_file; } if(result != CURLE_OK) { llinfos << "Error " << result << " while downloading disk image."<< llendl; goto close_file; } fclose(downloadFile); downloadFile = NULL; success = true; close_file: // Close disk image file if necessary if(downloadFile != NULL) { llinfos << "Closing download file." << llendl; fclose(downloadFile); downloadFile = NULL; } return success; } bool LLMacUpdater::doMount(const std::string& dmgName, char* deviceNode, const boost::filesystem::path& temp_dir) { char temp[PATH_MAX] = ""; /* Flawfinder: ignore */ sendProgress(0, 0, std::string("Mounting image...")); chdir(temp_dir.string().c_str()); std::string mnt_dir = temp_dir.string() + std::string("/mnt"); LLFile::mkdir(mnt_dir.c_str(), 0700); // NOTE: we could add -private at the end of this command line to keep the image from showing up in the Finder, // but if our cleanup fails, this makes it much harder for the user to unmount the image. std::string mountOutput; boost::format cmdFormat("hdiutil attach %s -mountpoint mnt"); cmdFormat % dmgName; FILE* mounter = popen(cmdFormat.str().c_str(), "r"); /* Flawfinder: ignore */ if(mounter == NULL) { llinfos << "Failed to mount disk image, exiting."<< llendl; return false; } // We need to scan the output from hdiutil to find the device node it uses to attach the disk image. // If we don't have this information, we can't detach it later. while(mounter != NULL) { size_t len = fread(temp, 1, sizeof(temp)-1, mounter); temp[len] = 0; mountOutput.append(temp); if(len < sizeof(temp)-1) { // End of file or error. int result = pclose(mounter); if(result != 0) { // NOTE: We used to abort here, but pclose() started returning // -1, possibly when the size of the DMG passed a certain point llinfos << "Unexpected result closing pipe: " << result << llendl; } mounter = NULL; } } if(!mountOutput.empty()) { const char *s = mountOutput.c_str(); const char *prefix = "/dev/"; char *sub = strstr(s, prefix); if(sub != NULL) { sub += strlen(prefix); /* Flawfinder: ignore */ sscanf(sub, "%1023s", deviceNode); /* Flawfinder: ignore */ } } if(deviceNode[0] != 0) { llinfos << "Disk image attached on /dev/" << deviceNode << llendl; } else { llinfos << "Disk image device node not found!" << llendl; return false; } return true; } bool LLMacUpdater::moveApplication (const boost::filesystem::path& app_dir, const boost::filesystem::path& temp_dir, boost::filesystem::path& aside_dir) { try { //Grab filename from installdir append to tempdir move set aside_dir to moved path. std::string install_str = app_dir.parent_path().string(); std::string temp_str = temp_dir.string(); std::string app_str = app_dir.filename().string(); aside_dir = boost::filesystem::path( boost::filesystem::operator/(temp_dir,app_str) ); std::cout << "Attempting to move " << app_dir.string() << " to " << aside_dir.string() << std::endl; boost::filesystem::rename(app_dir, aside_dir); } catch(boost::filesystem::filesystem_error e) { llinfos << "Application move failed." << llendl; return false; } return true; } bool LLMacUpdater::doInstall(const boost::filesystem::path& app_dir, const boost::filesystem::path& temp_dir, boost::filesystem::path& mount_dir, bool replacingTarget) { std::string temp_name = temp_dir.string() + std::string("/mnt"); llinfos << "Disk image mount point is: " << temp_name << llendl; mount_dir = boost::filesystem::path(temp_name.c_str()); if (! boost::filesystem::exists ( mount_dir ) ) { llinfos << "Couldn't make FSRef to disk image mount point." << llendl; return false; } sendProgress(0, 0, std::string("Searching for the app bundle...")); boost::filesystem::path source_dir; if ( !findAppBundleOnDiskImage(mount_dir, source_dir) ) { llinfos << "Couldn't find application bundle on mounted disk image." << llendl; return false; } else { llinfos << "found the bundle." << llendl; } sendProgress(0, 0, std::string("Preparing to copy files...")); // this will hold the name of the destination target boost::filesystem::path aside_dir; if(replacingTarget) { if (! moveApplication (app_dir, temp_dir, aside_dir) ) { llwarns << "failed to move aside old version." << llendl; return false; } } sendProgress(0, 0, std::string("Copying files...")); llinfos << "Starting copy..." << llendl; // If we were replacingTarget, we've moved the app to a temp directory. // Otherwise the destination should be empty. // We have mounted the DMG as a volume so we should be able to just // move the app from the volume to the destination and everything will just work. // Copy the new version from the disk image to the target location. //The installer volume is mounted read-only so we can't move. Instead copy and then unmount. if (! copyDir(source_dir.string(), app_dir.string()) ) { llwarns << "Failed to copy " << source_dir.string() << " to " << app_dir.string() << llendl; // Something went wrong during the copy. Attempt to put the old version back and bail. boost::filesystem::rename(app_dir, aside_dir); return false; } // The update has succeeded. Clear the cache directory. sendProgress(0, 0, std::string("Clearing cache...")); llinfos << "Clearing cache..." << llendl; gDirUtilp->deleteFilesInDir(gDirUtilp->getExpandedFilename(LL_PATH_CACHE,""), "*.*"); llinfos << "Clear complete." << llendl; return true; } bool mkTempDir(boost::filesystem::path& temp_dir) { char temp_str[PATH_MAX] = "/tmp/SecondLifeUpdate_XXXXXX"; if(mkdtemp(temp_str) == NULL) { return false; } temp_dir = boost::filesystem::path(temp_str); return true; } void* LLMacUpdater::updatethreadproc(void*) { char tempDir[PATH_MAX] = ""; /* Flawfinder: ignore */ char temp[PATH_MAX] = ""; /* Flawfinder: ignore */ // *NOTE: This buffer length is used in a scanf() below. char deviceNode[1024] = ""; /* Flawfinder: ignore */ bool replacingTarget = false; boost::filesystem::path app_dir; boost::filesystem::path temp_dir; boost::filesystem::path mount_dir; // Attempt to get a reference to the Second Life application bundle containing this updater. // Any failures during this process will cause us to default to updating /Applications/Second Life.app try { replacingTarget = getViewerDir( app_dir ); if (!mkTempDir(temp_dir)) { throw 0; } //In case the dir doesn't exist, try to create it. If create fails, verify it exists. if (! boost::filesystem::create_directory(app_dir)) { if(isFSRefViewerBundle(app_dir.string())) { // This is the bundle we're looking for. replacingTarget = true; } else { throw 0; } } if ( !verifyDirectory(&app_dir, true) ) { // We're so hosed. llinfos << "Applications directory not found, giving up." << llendl; throw 0; } // Skip downloading the file if the dmg was passed on the command line. std::string dmgName; if(mDmgFile != NULL) { //Create a string from the mDmgFile then a dir reference to that. //change to that directory and begin install. boost::filesystem::path dmg_path(*mDmgFile); dmgName = dmg_path.string(); std::string* dmgPath = new std::string(dmg_path.parent_path().string()); if ( !boost::filesystem::exists( dmg_path.parent_path() ) ) { llinfos << "Path " << *dmgPath << " is not writeable. Aborting." << llendl; throw 0; } chdir(dmgPath->c_str()); } else { // Continue on to download file. dmgName = "SecondLife.dmg"; if (!downloadDMG(dmgName, &temp_dir)) { throw 0; } } if (!doMount(dmgName, deviceNode, temp_dir)) { throw 0; } if (!doInstall( app_dir, temp_dir, mount_dir, replacingTarget )) { throw 0; } } catch(...) { if(!gCancelled) gFailure = true; } // Failures from here on out are all non-fatal and not reported. sendProgress(0, 3, std::string("Cleaning up...")); setProgress(1, 3); // Unmount image if(deviceNode[0] != 0) { llinfos << "Detaching disk image." << llendl; snprintf(temp, sizeof(temp), "hdiutil detach '%s'", deviceNode); system(temp); /* Flawfinder: ignore */ } setProgress(2, 3); std::string *trash_str=getUserTrashFolder(); // Move work directory to the trash if(tempDir[0] != 0) { llinfos << "Moving work directory to the trash." << llendl; try { boost::filesystem::path trash_dir(*trash_str); boost::filesystem::rename(mount_dir, trash_dir); } catch(boost::filesystem::filesystem_error e) { llwarns << "Failed to move " << mount_dir.string() << " to " << *trash_str << llendl; return (NULL); } } std::string app_name_str = app_dir.string(); if(!gCancelled && !gFailure && !app_name_str.empty()) { //SPATTERS todo is there no better way to do this than system calls? llinfos << "Touching application bundle." << llendl; std::stringstream touch_str; touch_str << "touch '" << app_name_str << "'"; system(touch_str.str().c_str()); /* Flawfinder: ignore */ llinfos << "Launching updated application." << llendl; std::stringstream open_str; open_str << "open '" << app_name_str << "'"; system(open_str.str().c_str()); /* Flawfinder: ignore */ } sendDone(); return (NULL); } //static void* LLMacUpdater::sUpdatethreadproc(void* vptr) { if (!sInstance) { llerrs << "LLMacUpdater not instantiated before use. Aborting." << llendl; return (NULL); } return sInstance->updatethreadproc(vptr); }