/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "VolumeManager.h" #include "Volume.h" #include "VolumeCommand.h" #include "VolumeManagerLog.h" #include "VolumeServiceTest.h" #include "nsWhitespaceTokenizer.h" #include "nsXULAppAPI.h" #include "base/message_loop.h" #include "base/task.h" #include "mozilla/Scoped.h" #include "mozilla/StaticPtr.h" #include #include #include #include namespace mozilla { namespace system { static StaticRefPtr sVolumeManager; VolumeManager::STATE VolumeManager::mState = VolumeManager::UNINITIALIZED; VolumeManager::StateObserverList VolumeManager::mStateObserverList; /***************************************************************************/ VolumeManager::VolumeManager() : LineWatcher('\0', kRcvBufSize), mSocket(-1), mCommandPending(false) { DBG("VolumeManager constructor called"); } VolumeManager::~VolumeManager() { } //static void VolumeManager::Dump(const char* aLabel) { if (!sVolumeManager) { LOG("%s: sVolumeManager == null", aLabel); return; } VolumeArray::size_type numVolumes = NumVolumes(); VolumeArray::index_type volIndex; for (volIndex = 0; volIndex < numVolumes; volIndex++) { RefPtr vol = GetVolume(volIndex); vol->Dump(aLabel); } } //static size_t VolumeManager::NumVolumes() { if (!sVolumeManager) { return 0; } return sVolumeManager->mVolumeArray.Length(); } //static already_AddRefed VolumeManager::GetVolume(size_t aIndex) { MOZ_ASSERT(aIndex < NumVolumes()); RefPtr vol = sVolumeManager->mVolumeArray[aIndex]; return vol.forget(); } //static VolumeManager::STATE VolumeManager::State() { return mState; } //static const char * VolumeManager::StateStr(VolumeManager::STATE aState) { switch (aState) { case UNINITIALIZED: return "Uninitialized"; case STARTING: return "Starting"; case VOLUMES_READY: return "Volumes Ready"; } return "???"; } //static void VolumeManager::SetState(STATE aNewState) { if (mState != aNewState) { LOG("changing state from '%s' to '%s'", StateStr(mState), StateStr(aNewState)); mState = aNewState; mStateObserverList.Broadcast(StateChangedEvent()); } } //static void VolumeManager::RegisterStateObserver(StateObserver* aObserver) { mStateObserverList.AddObserver(aObserver); } //static void VolumeManager::UnregisterStateObserver(StateObserver* aObserver) { mStateObserverList.RemoveObserver(aObserver); } //static already_AddRefed VolumeManager::FindVolumeByName(const nsCSubstring& aName) { if (!sVolumeManager) { return nullptr; } VolumeArray::size_type numVolumes = NumVolumes(); VolumeArray::index_type volIndex; for (volIndex = 0; volIndex < numVolumes; volIndex++) { RefPtr vol = GetVolume(volIndex); if (vol->Name().Equals(aName)) { return vol.forget(); } } return nullptr; } //static already_AddRefed VolumeManager::FindAddVolumeByName(const nsCSubstring& aName) { RefPtr vol = FindVolumeByName(aName); if (vol) { return vol.forget(); } // No volume found, create and add a new one. vol = new Volume(aName); sVolumeManager->mVolumeArray.AppendElement(vol); return vol.forget(); } //static bool VolumeManager::RemoveVolumeByName(const nsCSubstring& aName) { if (!sVolumeManager) { return false; } VolumeArray::size_type numVolumes = NumVolumes(); VolumeArray::index_type volIndex; for (volIndex = 0; volIndex < numVolumes; volIndex++) { RefPtr vol = GetVolume(volIndex); if (vol->Name().Equals(aName)) { sVolumeManager->mVolumeArray.RemoveElementAt(volIndex); return true; } } // No volume found. Return false to indicate this. return false; } //static void VolumeManager::InitConfig() { MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); // This function uses /system/etc/volume.cfg to add additional volumes // to the Volume Manager. // // This is useful on devices like the Nexus 4, which have no physical sd card // or dedicated partition. // // The format of the volume.cfg file is as follows: // create volume-name mount-point // configure volume-name preference preference-value // Blank lines and lines starting with the hash character "#" will be ignored. ScopedCloseFile fp; int n = 0; char line[255]; const char *filename = "/system/etc/volume.cfg"; if (!(fp = fopen(filename, "r"))) { LOG("Unable to open volume configuration file '%s' - ignoring", filename); return; } while(fgets(line, sizeof(line), fp)) { n++; if (line[0] == '#') continue; nsCString commandline(line); nsCWhitespaceTokenizer tokenizer(commandline); if (!tokenizer.hasMoreTokens()) { // Blank line - ignore continue; } nsCString command(tokenizer.nextToken()); if (command.EqualsLiteral("create")) { if (!tokenizer.hasMoreTokens()) { ERR("No vol_name in %s line %d", filename, n); continue; } nsCString volName(tokenizer.nextToken()); if (!tokenizer.hasMoreTokens()) { ERR("No mount point for volume '%s'. %s line %d", volName.get(), filename, n); continue; } nsCString mountPoint(tokenizer.nextToken()); RefPtr vol = FindAddVolumeByName(volName); vol->SetFakeVolume(mountPoint); continue; } if (command.EqualsLiteral("configure")) { if (!tokenizer.hasMoreTokens()) { ERR("No vol_name in %s line %d", filename, n); continue; } nsCString volName(tokenizer.nextToken()); if (!tokenizer.hasMoreTokens()) { ERR("No configuration name specified for volume '%s'. %s line %d", volName.get(), filename, n); continue; } nsCString configName(tokenizer.nextToken()); if (!tokenizer.hasMoreTokens()) { ERR("No value for configuration name '%s'. %s line %d", configName.get(), filename, n); continue; } nsCString configValue(tokenizer.nextToken()); RefPtr vol = FindVolumeByName(volName); if (vol) { vol->SetConfig(configName, configValue); } else { ERR("Invalid volume name '%s'.", volName.get()); } continue; } if (command.EqualsLiteral("ignore")) { // This command is useful to remove volumes which are being tracked by // vold, but for which we have no interest. if (!tokenizer.hasMoreTokens()) { ERR("No vol_name in %s line %d", filename, n); continue; } nsCString volName(tokenizer.nextToken()); RemoveVolumeByName(volName); continue; } ERR("Unrecognized command: '%s'", command.get()); } } void VolumeManager::DefaultConfig() { VolumeManager::VolumeArray::size_type numVolumes = VolumeManager::NumVolumes(); if (numVolumes == 0) { return; } if (numVolumes == 1) { // This is to cover early shipping phones like the Buri, // which had no internal storage, and only external sdcard. // // Phones line the nexus-4 which only have an internal // storage area will need to have a volume.cfg file with // removable set to false. RefPtr vol = VolumeManager::GetVolume(0); vol->SetIsRemovable(true); vol->SetIsHotSwappable(true); return; } VolumeManager::VolumeArray::index_type volIndex; for (volIndex = 0; volIndex < numVolumes; volIndex++) { RefPtr vol = VolumeManager::GetVolume(volIndex); if (!vol->Name().EqualsLiteral("sdcard")) { vol->SetIsRemovable(true); vol->SetIsHotSwappable(true); } } } class VolumeListCallback : public VolumeResponseCallback { virtual void ResponseReceived(const VolumeCommand* aCommand) { switch (ResponseCode()) { case ::ResponseCode::VolumeListResult: { // Each line will look something like: // // sdcard /mnt/sdcard 1 // // So for each volume that we get back, we update any volumes that // we have of the same name, or add new ones if they don't exist. nsCWhitespaceTokenizer tokenizer(ResponseStr()); nsDependentCSubstring volName(tokenizer.nextToken()); RefPtr vol = VolumeManager::FindAddVolumeByName(volName); vol->HandleVoldResponse(ResponseCode(), tokenizer); break; } case ::ResponseCode::CommandOkay: { // We've received the list of volumes. Now read the Volume.cfg // file to perform customizations, and then tell everybody // that we're ready for business. VolumeManager::DefaultConfig(); VolumeManager::InitConfig(); VolumeManager::Dump("READY"); VolumeManager::SetState(VolumeManager::VOLUMES_READY); break; } } } }; bool VolumeManager::OpenSocket() { SetState(STARTING); if ((mSocket.rwget() = socket_local_client("vold", ANDROID_SOCKET_NAMESPACE_RESERVED, SOCK_STREAM)) < 0) { ERR("Error connecting to vold: (%s) - will retry", strerror(errno)); return false; } // add FD_CLOEXEC flag int flags = fcntl(mSocket.get(), F_GETFD); if (flags == -1) { return false; } flags |= FD_CLOEXEC; if (fcntl(mSocket.get(), F_SETFD, flags) == -1) { return false; } // set non-blocking if (fcntl(mSocket.get(), F_SETFL, O_NONBLOCK) == -1) { return false; } if (!MessageLoopForIO::current()-> WatchFileDescriptor(mSocket.get(), true, MessageLoopForIO::WATCH_READ, &mReadWatcher, this)) { return false; } LOG("Connected to vold"); PostCommand(new VolumeListCommand(new VolumeListCallback)); return true; } //static void VolumeManager::PostCommand(VolumeCommand* aCommand) { if (!sVolumeManager) { ERR("VolumeManager not initialized. Dropping command '%s'", aCommand->Data()); return; } aCommand->SetPending(true); DBG("Sending command '%s'", aCommand->Data()); // vold can only process one command at a time, so add our command // to the end of the command queue. sVolumeManager->mCommands.push(aCommand); if (!sVolumeManager->mCommandPending) { // There aren't any commands currently being processed, so go // ahead and kick this one off. sVolumeManager->mCommandPending = true; sVolumeManager->WriteCommandData(); } } /*************************************************************************** * The WriteCommandData initiates sending of a command to vold. Since * we're running on the IOThread and not allowed to block, WriteCommandData * will write as much data as it can, and if not all of the data can be * written then it will setup a file descriptor watcher and * OnFileCanWriteWithoutBlocking will call WriteCommandData to write out * more of the command data. */ void VolumeManager::WriteCommandData() { if (mCommands.size() == 0) { return; } VolumeCommand* cmd = mCommands.front(); if (cmd->BytesRemaining() == 0) { // All bytes have been written. We're waiting for a response. return; } // There are more bytes left to write. Try to write them all. ssize_t bytesWritten = write(mSocket.get(), cmd->Data(), cmd->BytesRemaining()); if (bytesWritten < 0) { ERR("Failed to write %d bytes to vold socket", cmd->BytesRemaining()); Restart(); return; } DBG("Wrote %d bytes (of %d)", bytesWritten, cmd->BytesRemaining()); cmd->ConsumeBytes(bytesWritten); if (cmd->BytesRemaining() == 0) { return; } // We were unable to write all of the command bytes. Setup a watcher // so we'll get called again when we can write without blocking. if (!MessageLoopForIO::current()-> WatchFileDescriptor(mSocket.get(), false, // one-shot MessageLoopForIO::WATCH_WRITE, &mWriteWatcher, this)) { ERR("Failed to setup write watcher for vold socket"); Restart(); } } void VolumeManager::OnLineRead(int aFd, nsDependentCSubstring& aMessage) { MOZ_ASSERT(aFd == mSocket.get()); char* endPtr; int responseCode = strtol(aMessage.Data(), &endPtr, 10); if (*endPtr == ' ') { endPtr++; } // Now fish out the rest of the line after the response code nsDependentCString responseLine(endPtr, aMessage.Length() - (endPtr - aMessage.Data())); DBG("Rcvd: %d '%s'", responseCode, responseLine.Data()); if (responseCode >= ::ResponseCode::UnsolicitedInformational) { // These are unsolicited broadcasts. We intercept these and process // them ourselves HandleBroadcast(responseCode, responseLine); } else { // Everything else is considered to be part of the command response. if (mCommands.size() > 0) { VolumeCommand* cmd = mCommands.front(); cmd->HandleResponse(responseCode, responseLine); if (responseCode >= ::ResponseCode::CommandOkay) { // That's a terminating response. We can remove the command. mCommands.pop(); mCommandPending = false; // Start the next command, if there is one. WriteCommandData(); } } else { ERR("Response with no command"); } } } void VolumeManager::OnFileCanWriteWithoutBlocking(int aFd) { MOZ_ASSERT(aFd == mSocket.get()); WriteCommandData(); } void VolumeManager::HandleBroadcast(int aResponseCode, nsCString& aResponseLine) { // Format of the line is something like: // // Volume sdcard /mnt/sdcard state changed from 7 (Shared-Unmounted) to 1 (Idle-Unmounted) // // So we parse out the volume name and the state after the string " to " nsCWhitespaceTokenizer tokenizer(aResponseLine); tokenizer.nextToken(); // The word "Volume" nsDependentCSubstring volName(tokenizer.nextToken()); RefPtr vol = FindVolumeByName(volName); if (!vol) { return; } vol->HandleVoldResponse(aResponseCode, tokenizer); } void VolumeManager::Restart() { mReadWatcher.StopWatchingFileDescriptor(); mWriteWatcher.StopWatchingFileDescriptor(); while (!mCommands.empty()) { mCommands.pop(); } mCommandPending = false; mSocket.dispose(); Start(); } //static void VolumeManager::Start() { MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); if (!sVolumeManager) { return; } SetState(STARTING); if (!sVolumeManager->OpenSocket()) { // Socket open failed, try again in a second. MessageLoopForIO::current()-> PostDelayedTask(NewRunnableFunction(VolumeManager::Start), 1000); } } void VolumeManager::OnError() { Restart(); } /***************************************************************************/ static void InitVolumeManagerIOThread() { MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); MOZ_ASSERT(!sVolumeManager); sVolumeManager = new VolumeManager(); VolumeManager::Start(); InitVolumeServiceTestIOThread(); } static void ShutdownVolumeManagerIOThread() { MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); sVolumeManager = nullptr; } /************************************************************************** * * Public API * * Since the VolumeManager runs in IO Thread context, we need to switch * to IOThread context before we can do anything. * **************************************************************************/ void InitVolumeManager() { XRE_GetIOMessageLoop()->PostTask( NewRunnableFunction(InitVolumeManagerIOThread)); } void ShutdownVolumeManager() { ShutdownVolumeServiceTest(); XRE_GetIOMessageLoop()->PostTask( NewRunnableFunction(ShutdownVolumeManagerIOThread)); } } // system } // mozilla