/** * @file lllogininstance.cpp * @brief Viewer's host for a login connection. * * $LicenseInfo:firstyear=2009&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 "llviewerprecompiledheaders.h" #include "lllogininstance.h" // llcommon #include "llevents.h" #include "stringize.h" // llmessage (!) #include "llfiltersd2xmlrpc.h" // for xml_escape_string() // login #include "lllogin.h" // newview #include "llhasheduniqueid.h" #include "llviewernetwork.h" #include "llviewercontrol.h" #include "llversioninfo.h" #include "llslurl.h" #include "llstartup.h" #include "llfloaterreg.h" #include "llnotifications.h" #include "llwindow.h" #include "llviewerwindow.h" #include "llprogressview.h" #if LL_LINUX || LL_SOLARIS #include "lltrans.h" #endif #include "llsecapi.h" #include "llstartup.h" #include "llmachineid.h" #include "llupdaterservice.h" #include "llevents.h" #include "llappviewer.h" #include #include const S32 LOGIN_MAX_RETRIES = 3; class LLLoginInstance::Disposable { public: virtual ~Disposable() {} }; namespace { class MandatoryUpdateMachine: public LLLoginInstance::Disposable { public: MandatoryUpdateMachine(LLLoginInstance & loginInstance, LLUpdaterService & updaterService); void start(void); LLNotificationsInterface& getNotificationsInterface() const { return mLoginInstance.getNotificationsInterface(); } private: class State; class CheckingForUpdate; class Error; class ReadyToInstall; class StartingUpdaterService; class WaitingForDownload; boost::scoped_ptr mState; LLLoginInstance & mLoginInstance; LLUpdaterService & mUpdaterService; void setCurrentState(State * newState); }; class MandatoryUpdateMachine::State { public: virtual ~State() {} virtual void enter(void) {} virtual void exit(void) {} }; class MandatoryUpdateMachine::CheckingForUpdate: public MandatoryUpdateMachine::State { public: CheckingForUpdate(MandatoryUpdateMachine & machine); virtual void enter(void); virtual void exit(void); private: LLTempBoundListener mConnection; MandatoryUpdateMachine & mMachine; LLProgressView * mProgressView; bool onEvent(LLSD const & event); }; class MandatoryUpdateMachine::Error: public MandatoryUpdateMachine::State { public: Error(MandatoryUpdateMachine & machine); virtual void enter(void); virtual void exit(void); void onButtonClicked(const LLSD &, const LLSD &); private: MandatoryUpdateMachine & mMachine; }; class MandatoryUpdateMachine::ReadyToInstall: public MandatoryUpdateMachine::State { public: ReadyToInstall(MandatoryUpdateMachine & machine); virtual void enter(void); virtual void exit(void); private: //MandatoryUpdateMachine & mMachine; }; class MandatoryUpdateMachine::StartingUpdaterService: public MandatoryUpdateMachine::State { public: StartingUpdaterService(MandatoryUpdateMachine & machine); virtual void enter(void); virtual void exit(void); void onButtonClicked(const LLSD & uiform, const LLSD & result); private: MandatoryUpdateMachine & mMachine; }; class MandatoryUpdateMachine::WaitingForDownload: public MandatoryUpdateMachine::State { public: WaitingForDownload(MandatoryUpdateMachine & machine); virtual void enter(void); virtual void exit(void); private: LLTempBoundListener mConnection; MandatoryUpdateMachine & mMachine; LLProgressView * mProgressView; bool onEvent(LLSD const & event); }; } static const char * const TOS_REPLY_PUMP = "lllogininstance_tos_callback"; static const char * const TOS_LISTENER_NAME = "lllogininstance_tos"; std::string construct_start_string(); // MandatoryUpdateMachine //----------------------------------------------------------------------------- MandatoryUpdateMachine::MandatoryUpdateMachine(LLLoginInstance & loginInstance, LLUpdaterService & updaterService): mLoginInstance(loginInstance), mUpdaterService(updaterService) { ; // No op. } void MandatoryUpdateMachine::start(void) { LL_INFOS() << "starting mandatory update machine" << LL_ENDL; if(mUpdaterService.isChecking()) { switch(mUpdaterService.getState()) { case LLUpdaterService::UP_TO_DATE: mUpdaterService.stopChecking(); mUpdaterService.startChecking(); // Fall through. case LLUpdaterService::INITIAL: case LLUpdaterService::CHECKING_FOR_UPDATE: setCurrentState(new CheckingForUpdate(*this)); break; case LLUpdaterService::TEMPORARY_ERROR: setCurrentState(new Error(*this)); break; case LLUpdaterService::DOWNLOADING: setCurrentState(new WaitingForDownload(*this)); break; case LLUpdaterService::TERMINAL: if(LLUpdaterService::updateReadyToInstall()) { setCurrentState(new ReadyToInstall(*this)); } else { setCurrentState(new Error(*this)); } break; case LLUpdaterService::FAILURE: setCurrentState(new Error(*this)); break; default: llassert(!"unpossible case"); break; } } else { setCurrentState(new StartingUpdaterService(*this)); } } void MandatoryUpdateMachine::setCurrentState(State * newStatePointer) { { boost::scoped_ptr newState(newStatePointer); if(mState != 0) mState->exit(); mState.swap(newState); // Old state will be deleted on exit from this block before the new state // is entered. } if(mState != 0) mState->enter(); } // MandatoryUpdateMachine::CheckingForUpdate //----------------------------------------------------------------------------- MandatoryUpdateMachine::CheckingForUpdate::CheckingForUpdate(MandatoryUpdateMachine & machine): mMachine(machine) { ; // No op. } void MandatoryUpdateMachine::CheckingForUpdate::enter(void) { LL_INFOS() << "entering checking for update" << LL_ENDL; mProgressView = gViewerWindow->getProgressView(); mProgressView->setMessage("Looking for update..."); mProgressView->setText("There is a required update for your Second Life installation."); mProgressView->setPercent(0); mProgressView->setVisible(true); mConnection = LLEventPumps::instance().obtain(LLUpdaterService::pumpName()). listen("MandatoryUpdateMachine::CheckingForUpdate", boost::bind(&MandatoryUpdateMachine::CheckingForUpdate::onEvent, this, _1)); } void MandatoryUpdateMachine::CheckingForUpdate::exit(void) { } bool MandatoryUpdateMachine::CheckingForUpdate::onEvent(LLSD const & event) { if(event["type"].asInteger() == LLUpdaterService::STATE_CHANGE) { switch(event["state"].asInteger()) { case LLUpdaterService::DOWNLOADING: mMachine.setCurrentState(new WaitingForDownload(mMachine)); break; case LLUpdaterService::TEMPORARY_ERROR: case LLUpdaterService::UP_TO_DATE: case LLUpdaterService::TERMINAL: case LLUpdaterService::FAILURE: mProgressView->setVisible(false); mMachine.setCurrentState(new Error(mMachine)); break; case LLUpdaterService::INSTALLING: llassert(!"can't possibly be installing"); break; default: break; } } else { ; // Ignore. } return false; } // MandatoryUpdateMachine::Error //----------------------------------------------------------------------------- MandatoryUpdateMachine::Error::Error(MandatoryUpdateMachine & machine): mMachine(machine) { ; // No op. } void MandatoryUpdateMachine::Error::enter(void) { LL_INFOS() << "entering error" << LL_ENDL; mMachine.getNotificationsInterface().add("FailedRequiredUpdateInstall", LLSD(), LLSD(), boost::bind(&MandatoryUpdateMachine::Error::onButtonClicked, this, _1, _2)); } void MandatoryUpdateMachine::Error::exit(void) { LLAppViewer::instance()->forceQuit(); } void MandatoryUpdateMachine::Error::onButtonClicked(const LLSD &, const LLSD &) { mMachine.setCurrentState(0); } // MandatoryUpdateMachine::ReadyToInstall //----------------------------------------------------------------------------- MandatoryUpdateMachine::ReadyToInstall::ReadyToInstall(MandatoryUpdateMachine & machine) //: //mMachine(machine) { ; // No op. } void MandatoryUpdateMachine::ReadyToInstall::enter(void) { LL_INFOS() << "entering ready to install" << LL_ENDL; // Open update ready dialog. } void MandatoryUpdateMachine::ReadyToInstall::exit(void) { // Restart viewer. } // MandatoryUpdateMachine::StartingUpdaterService //----------------------------------------------------------------------------- MandatoryUpdateMachine::StartingUpdaterService::StartingUpdaterService(MandatoryUpdateMachine & machine): mMachine(machine) { ; // No op. } void MandatoryUpdateMachine::StartingUpdaterService::enter(void) { LL_INFOS() << "entering start update service" << LL_ENDL; mMachine.getNotificationsInterface().add("UpdaterServiceNotRunning", LLSD(), LLSD(), boost::bind(&MandatoryUpdateMachine::StartingUpdaterService::onButtonClicked, this, _1, _2)); } void MandatoryUpdateMachine::StartingUpdaterService::exit(void) { ; // No op. } void MandatoryUpdateMachine::StartingUpdaterService::onButtonClicked(const LLSD & uiform, const LLSD & result) { if(result["OK_okcancelbuttons"].asBoolean()) { mMachine.mUpdaterService.startChecking(false); mMachine.setCurrentState(new CheckingForUpdate(mMachine)); } else { LLAppViewer::instance()->forceQuit(); } } // MandatoryUpdateMachine::WaitingForDownload //----------------------------------------------------------------------------- MandatoryUpdateMachine::WaitingForDownload::WaitingForDownload(MandatoryUpdateMachine & machine): mMachine(machine), mProgressView(0) { ; // No op. } void MandatoryUpdateMachine::WaitingForDownload::enter(void) { LL_INFOS() << "entering waiting for download" << LL_ENDL; mProgressView = gViewerWindow->getProgressView(); mProgressView->setMessage("Downloading update..."); std::ostringstream stream; stream << "There is a required update for your Second Life installation." << std::endl << "Version " << mMachine.mUpdaterService.updatedVersion(); mProgressView->setText(stream.str()); mProgressView->setPercent(0); mProgressView->setVisible(true); mConnection = LLEventPumps::instance().obtain(LLUpdaterService::pumpName()). listen("MandatoryUpdateMachine::CheckingForUpdate", boost::bind(&MandatoryUpdateMachine::WaitingForDownload::onEvent, this, _1)); } void MandatoryUpdateMachine::WaitingForDownload::exit(void) { mProgressView->setVisible(false); } bool MandatoryUpdateMachine::WaitingForDownload::onEvent(LLSD const & event) { switch(event["type"].asInteger()) { case LLUpdaterService::DOWNLOAD_COMPLETE: mMachine.setCurrentState(new ReadyToInstall(mMachine)); break; case LLUpdaterService::DOWNLOAD_ERROR: mMachine.setCurrentState(new Error(mMachine)); break; case LLUpdaterService::PROGRESS: { double downloadSize = event["download_size"].asReal(); double bytesDownloaded = event["bytes_downloaded"].asReal(); mProgressView->setPercent(100. * bytesDownloaded / downloadSize); break; } default: break; } return false; } // LLLoginInstance //----------------------------------------------------------------------------- LLLoginInstance::LLLoginInstance() : mLoginModule(new LLLogin()), mNotifications(NULL), mLoginState("offline"), mSkipOptionalUpdate(false), mAttemptComplete(false), mTransferRate(0.0f), mDispatcher("LLLoginInstance", "change"), mUpdaterService(0) { mLoginModule->getEventPump().listen("lllogininstance", boost::bind(&LLLoginInstance::handleLoginEvent, this, _1)); // This internal use of LLEventDispatcher doesn't really need // per-function descriptions. mDispatcher.add("fail.login", "", boost::bind(&LLLoginInstance::handleLoginFailure, this, _1)); mDispatcher.add("connect", "", boost::bind(&LLLoginInstance::handleLoginSuccess, this, _1)); mDispatcher.add("disconnect", "", boost::bind(&LLLoginInstance::handleDisconnect, this, _1)); mDispatcher.add("indeterminate", "", boost::bind(&LLLoginInstance::handleIndeterminate, this, _1)); } void LLLoginInstance::setPlatformInfo(const std::string platform, const std::string platform_version) { mPlatform = platform; mPlatformVersion = platform_version; } LLLoginInstance::~LLLoginInstance() { } void LLLoginInstance::connect(LLPointer credentials) { std::vector uris; LLGridManager::getInstance()->getLoginURIs(uris); connect(uris.front(), credentials); } void LLLoginInstance::connect(const std::string& uri, LLPointer credentials) { mAttemptComplete = false; // Reset attempt complete at this point! constructAuthParams(credentials); mLoginModule->connect(uri, mRequestData); } void LLLoginInstance::reconnect() { // Sort of like connect, only using the pre-existing // request params. std::vector uris; LLGridManager::getInstance()->getLoginURIs(uris); mLoginModule->connect(uris.front(), mRequestData); gViewerWindow->setShowProgress(true); } void LLLoginInstance::disconnect() { mAttemptComplete = false; // Reset attempt complete at this point! mRequestData.clear(); mLoginModule->disconnect(); } LLSD LLLoginInstance::getResponse() { return mResponseData; } void LLLoginInstance::constructAuthParams(LLPointer user_credential) { // Set up auth request options. //#define LL_MINIMIAL_REQUESTED_OPTIONS LLSD requested_options; // *Note: this is where gUserAuth used to be created. requested_options.append("inventory-root"); requested_options.append("inventory-skeleton"); //requested_options.append("inventory-meat"); //requested_options.append("inventory-skel-targets"); #if (!defined LL_MINIMIAL_REQUESTED_OPTIONS) if(FALSE == gSavedSettings.getBOOL("NoInventoryLibrary")) { requested_options.append("inventory-lib-root"); requested_options.append("inventory-lib-owner"); requested_options.append("inventory-skel-lib"); // requested_options.append("inventory-meat-lib"); } requested_options.append("initial-outfit"); requested_options.append("gestures"); requested_options.append("display_names"); requested_options.append("event_categories"); requested_options.append("event_notifications"); requested_options.append("classified_categories"); requested_options.append("adult_compliant"); //requested_options.append("inventory-targets"); requested_options.append("buddy-list"); requested_options.append("newuser-config"); requested_options.append("ui-config"); //send this info to login.cgi for stats gathering //since viewerstats isn't reliable enough requested_options.append("advanced-mode"); #endif requested_options.append("max-agent-groups"); requested_options.append("map-server-url"); requested_options.append("voice-config"); requested_options.append("tutorial_setting"); requested_options.append("login-flags"); requested_options.append("global-textures"); if(gSavedSettings.getBOOL("ConnectAsGod")) { gSavedSettings.setBOOL("UseDebugMenus", TRUE); requested_options.append("god-connect"); } // (re)initialize the request params with creds. LLSD request_params = user_credential->getLoginParams(); unsigned char hashed_unique_id_string[MD5HEX_STR_SIZE]; if ( ! llHashedUniqueID(hashed_unique_id_string) ) { LL_WARNS() << "Not providing a unique id in request params" << LL_ENDL; } request_params["start"] = construct_start_string(); request_params["skipoptional"] = mSkipOptionalUpdate; request_params["agree_to_tos"] = false; // Always false here. Set true in request_params["read_critical"] = false; // handleTOSResponse request_params["last_exec_event"] = mLastExecEvent; request_params["last_exec_duration"] = mLastExecDuration; request_params["mac"] = (char*)hashed_unique_id_string; request_params["version"] = LLVersionInfo::getVersion(); request_params["channel"] = LLVersionInfo::getChannel(); request_params["platform"] = mPlatform; request_params["platform_version"] = mPlatformVersion; request_params["id0"] = mSerialNumber; request_params["host_id"] = gSavedSettings.getString("HostID"); request_params["extended_errors"] = true; // request message_id and message_args // Specify desired timeout/retry options LLSD http_params; http_params["timeout"] = gSavedSettings.getF32("LoginSRVTimeout"); http_params["retries"] = LOGIN_MAX_RETRIES; mRequestData.clear(); mRequestData["method"] = "login_to_simulator"; mRequestData["params"] = request_params; mRequestData["options"] = requested_options; mRequestData["http_params"] = http_params; } bool LLLoginInstance::handleLoginEvent(const LLSD& event) { LL_DEBUGS("LLLogin") << "LoginListener called!: \n" << event << LL_ENDL; if(!(event.has("state") && event.has("change") && event.has("progress"))) { LL_ERRS() << "Unknown message from LLLogin: " << event << LL_ENDL; } mLoginState = event["state"].asString(); mResponseData = event["data"]; if(event.has("transfer_rate")) { mTransferRate = event["transfer_rate"].asReal(); } // Call the method registered in constructor, if any, for more specific // handling mDispatcher.try_call(event); return false; } void LLLoginInstance::handleLoginFailure(const LLSD& event) { // Login has failed. // Figure out why and respond... LLSD response = event["data"]; std::string reason_response = response["reason"].asString(); std::string message_response = response["message"].asString(); // For the cases of critical message or TOS agreement, // start the TOS dialog. The dialog response will be handled // by the LLLoginInstance::handleTOSResponse() callback. // The callback intiates the login attempt next step, either // to reconnect or to end the attempt in failure. if(reason_response == "tos") { LL_INFOS() << "LLLoginInstance::handleLoginFailure ToS" << LL_ENDL; LLSD data(LLSD::emptyMap()); data["message"] = message_response; data["reply_pump"] = TOS_REPLY_PUMP; if (gViewerWindow) gViewerWindow->setShowProgress(FALSE); LLFloaterReg::showInstance("message_tos", data); LLEventPumps::instance().obtain(TOS_REPLY_PUMP) .listen(TOS_LISTENER_NAME, boost::bind(&LLLoginInstance::handleTOSResponse, this, _1, "agree_to_tos")); } else if(reason_response == "critical") { LL_INFOS() << "LLLoginInstance::handleLoginFailure Crit" << LL_ENDL; LLSD data(LLSD::emptyMap()); data["message"] = message_response; data["reply_pump"] = TOS_REPLY_PUMP; if(response.has("error_code")) { data["error_code"] = response["error_code"]; } if(response.has("certificate")) { data["certificate"] = response["certificate"]; } if (gViewerWindow) gViewerWindow->setShowProgress(FALSE); LLFloaterReg::showInstance("message_critical", data); LLEventPumps::instance().obtain(TOS_REPLY_PUMP) .listen(TOS_LISTENER_NAME, boost::bind(&LLLoginInstance::handleTOSResponse, this, _1, "read_critical")); } else if(reason_response == "update" || gSavedSettings.getBOOL("ForceMandatoryUpdate")) { LL_INFOS() << "LLLoginInstance::handleLoginFailure update" << LL_ENDL; gSavedSettings.setBOOL("ForceMandatoryUpdate", FALSE); updateApp(true, message_response); } else if(reason_response == "optional") { LL_INFOS() << "LLLoginInstance::handleLoginFailure optional" << LL_ENDL; updateApp(false, message_response); } else { LL_INFOS() << "LLLoginInstance::handleLoginFailure attemptComplete" << LL_ENDL; attemptComplete(); } } void LLLoginInstance::handleLoginSuccess(const LLSD& event) { LL_INFOS() << "LLLoginInstance::handleLoginSuccess" << LL_ENDL; if(gSavedSettings.getBOOL("ForceMandatoryUpdate")) { LLSD response = event["data"]; std::string message_response = response["message"].asString(); // Testing update... gSavedSettings.setBOOL("ForceMandatoryUpdate", FALSE); // Don't confuse startup by leaving login "online". mLoginModule->disconnect(); updateApp(true, message_response); } else { attemptComplete(); } } void LLLoginInstance::handleDisconnect(const LLSD& event) { // placeholder LL_INFOS() << "LLLoginInstance::handleDisconnect placeholder " << LL_ENDL; } void LLLoginInstance::handleIndeterminate(const LLSD& event) { // The indeterminate response means that the server // gave the viewer a new url and params to try. // The login module handles the retry, but it gives us the // server response so that we may show // the user some status. LLSD message = event.get("data").get("message"); if(message.isDefined()) { LL_INFOS() << "LLLoginInstance::handleIndeterminate " << message.asString() << LL_ENDL; LLSD progress_update; progress_update["desc"] = message; LLEventPumps::getInstance()->obtain("LLProgressView").post(progress_update); } } bool LLLoginInstance::handleTOSResponse(bool accepted, const std::string& key) { if(accepted) { LL_INFOS() << "LLLoginInstance::handleTOSResponse: accepted" << LL_ENDL; // Set the request data to true and retry login. mRequestData["params"][key] = true; reconnect(); } else { LL_INFOS() << "LLLoginInstance::handleTOSResponse: attemptComplete" << LL_ENDL; attemptComplete(); } LLEventPumps::instance().obtain(TOS_REPLY_PUMP).stopListening(TOS_LISTENER_NAME); return true; } void LLLoginInstance::updateApp(bool mandatory, const std::string& auth_msg) { if(mandatory) { gViewerWindow->setShowProgress(false); MandatoryUpdateMachine * machine = new MandatoryUpdateMachine(*this, *mUpdaterService); mUpdateStateMachine.reset(machine); machine->start(); return; } // store off config state, as we might quit soon gSavedSettings.saveToFile(gSavedSettings.getString("ClientSettingsFile"), TRUE); LLUIColorTable::instance().saveUserSettings(); std::ostringstream message; std::string msg; if (!auth_msg.empty()) { msg = "(" + auth_msg + ") \n"; } LLSD args; args["MESSAGE"] = msg; LLSD payload; payload["mandatory"] = mandatory; /* * We're constructing one of the following 9 strings here: * "DownloadWindowsMandatory" * "DownloadWindowsReleaseForDownload" * "DownloadWindows" * "DownloadMacMandatory" * "DownloadMacReleaseForDownload" * "DownloadMac" * "DownloadLinuxMandatory" * "DownloadLinuxReleaseForDownload" * "DownloadLinux" * * I've called them out explicitly in this comment so that they can be grepped for. */ std::string notification_name = "Download"; #if LL_WINDOWS notification_name += "Windows"; #elif LL_DARWIN notification_name += "Mac"; #else notification_name += "Linux"; #endif if (mandatory) { notification_name += "Mandatory"; } else { #if LL_RELEASE_FOR_DOWNLOAD notification_name += "ReleaseForDownload"; #endif } if(mNotifications) { mNotifications->add(notification_name, args, payload, boost::bind(&LLLoginInstance::updateDialogCallback, this, _1, _2)); gViewerWindow->setShowProgress(false); } } bool LLLoginInstance::updateDialogCallback(const LLSD& notification, const LLSD& response) { S32 option = LLNotification::getSelectedOption(notification, response); std::string update_exe_path; bool mandatory = notification["payload"]["mandatory"].asBoolean(); #if !LL_RELEASE_FOR_DOWNLOAD if (option == 2) { // This condition attempts to skip the // update if using a dev build. // The relog probably won't work if the // update is mandatory. :) // *REMOVE:Mani - Saving for reference... //LLStartUp::setStartupState( STATE_LOGIN_AUTH_INIT ); mSkipOptionalUpdate = true; reconnect(); return false; } #endif if (option == 1) { // ...user doesn't want to do it if (mandatory) { // Mandatory update, user chose to not to update... // The login attemp is complete, startup should // quit when detecting this. attemptComplete(); // *REMOVE:Mani - Saving for reference... //LLAppViewer::instance()->forceQuit(); // // Bump them back to the login screen. // //reset_login(); } else { // Optional update, user chose to skip mSkipOptionalUpdate = true; reconnect(); } return false; } if(mUpdaterLauncher) { mUpdaterLauncher(); } attemptComplete(); return false; } std::string construct_start_string() { std::string start; LLSLURL start_slurl = LLStartUp::getStartSLURL(); switch(start_slurl.getType()) { case LLSLURL::LOCATION: { // a startup URL was specified LLVector3 position = start_slurl.getPosition(); std::string unescaped_start = STRINGIZE( "uri:" << start_slurl.getRegion() << "&" << position[VX] << "&" << position[VY] << "&" << position[VZ]); start = xml_escape_string(unescaped_start); break; } case LLSLURL::HOME_LOCATION: { start = "home"; break; } default: { start = "last"; } } return start; }