674 lines
19 KiB
C++
674 lines
19 KiB
C++
/**
|
|
* @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 <boost/format.hpp>
|
|
#include <boost/filesystem/operations.hpp>
|
|
#include <boost/filesystem.hpp>
|
|
|
|
#include <libgen.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include <curl/curl.h>
|
|
|
|
#include "llerror.h"
|
|
#include "lltimer.h"
|
|
#include "lldir.h"
|
|
#include "llfile.h"
|
|
|
|
#include "llstring.h"
|
|
|
|
#include "llerrorcontrol.h"
|
|
#include "mac_updater.h"
|
|
#include <sstream>
|
|
|
|
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 <url> | -dmg <dmg file> [-name <product_name>] [-program <program_name>]" << 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);
|
|
}
|
|
|