michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "CacheLog.h" michael@0: #include "CacheFileContextEvictor.h" michael@0: #include "CacheFileIOManager.h" michael@0: #include "CacheIndex.h" michael@0: #include "CacheIndexIterator.h" michael@0: #include "CacheFileUtils.h" michael@0: #include "nsIFile.h" michael@0: #include "LoadContextInfo.h" michael@0: #include "nsThreadUtils.h" michael@0: #include "nsString.h" michael@0: #include "nsISimpleEnumerator.h" michael@0: #include "nsIDirectoryEnumerator.h" michael@0: #include "mozilla/Base64.h" michael@0: michael@0: michael@0: namespace mozilla { michael@0: namespace net { michael@0: michael@0: const char kContextEvictionPrefix[] = "ce_"; michael@0: const uint32_t kContextEvictionPrefixLength = michael@0: sizeof(kContextEvictionPrefix) - 1; michael@0: michael@0: bool CacheFileContextEvictor::sDiskAlreadySearched = false; michael@0: michael@0: CacheFileContextEvictor::CacheFileContextEvictor() michael@0: : mEvicting(false) michael@0: , mIndexIsUpToDate(false) michael@0: { michael@0: LOG(("CacheFileContextEvictor::CacheFileContextEvictor() [this=%p]", this)); michael@0: } michael@0: michael@0: CacheFileContextEvictor::~CacheFileContextEvictor() michael@0: { michael@0: LOG(("CacheFileContextEvictor::~CacheFileContextEvictor() [this=%p]", this)); michael@0: } michael@0: michael@0: nsresult michael@0: CacheFileContextEvictor::Init(nsIFile *aCacheDirectory) michael@0: { michael@0: LOG(("CacheFileContextEvictor::Init()")); michael@0: michael@0: nsresult rv; michael@0: michael@0: MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); michael@0: michael@0: CacheIndex::IsUpToDate(&mIndexIsUpToDate); michael@0: michael@0: mCacheDirectory = aCacheDirectory; michael@0: michael@0: rv = aCacheDirectory->Clone(getter_AddRefs(mEntriesDir)); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) { michael@0: return rv; michael@0: } michael@0: michael@0: rv = mEntriesDir->AppendNative(NS_LITERAL_CSTRING(kEntriesDir)); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) { michael@0: return rv; michael@0: } michael@0: michael@0: if (!sDiskAlreadySearched) { michael@0: LoadEvictInfoFromDisk(); michael@0: if ((mEntries.Length() != 0) && mIndexIsUpToDate) { michael@0: CreateIterators(); michael@0: StartEvicting(); michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: uint32_t michael@0: CacheFileContextEvictor::ContextsCount() michael@0: { michael@0: MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); michael@0: michael@0: return mEntries.Length(); michael@0: } michael@0: michael@0: nsresult michael@0: CacheFileContextEvictor::AddContext(nsILoadContextInfo *aLoadContextInfo) michael@0: { michael@0: LOG(("CacheFileContextEvictor::AddContext() [this=%p, loadContextInfo=%p]", michael@0: this, aLoadContextInfo)); michael@0: michael@0: nsresult rv; michael@0: michael@0: MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); michael@0: michael@0: CacheFileContextEvictorEntry *entry = nullptr; michael@0: for (uint32_t i = 0; i < mEntries.Length(); ++i) { michael@0: if (mEntries[i]->mInfo->Equals(aLoadContextInfo)) { michael@0: entry = mEntries[i]; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (!entry) { michael@0: entry = new CacheFileContextEvictorEntry(); michael@0: entry->mInfo = aLoadContextInfo; michael@0: mEntries.AppendElement(entry); michael@0: } michael@0: michael@0: entry->mTimeStamp = PR_Now() / PR_USEC_PER_MSEC; michael@0: michael@0: PersistEvictionInfoToDisk(aLoadContextInfo); michael@0: michael@0: if (mIndexIsUpToDate) { michael@0: // Already existing context could be added again, in this case the iterator michael@0: // would be recreated. Close the old iterator explicitely. michael@0: if (entry->mIterator) { michael@0: entry->mIterator->Close(); michael@0: entry->mIterator = nullptr; michael@0: } michael@0: michael@0: rv = CacheIndex::GetIterator(aLoadContextInfo, false, michael@0: getter_AddRefs(entry->mIterator)); michael@0: if (NS_FAILED(rv)) { michael@0: // This could probably happen during shutdown. Remove the entry from michael@0: // the array, but leave the info on the disk. No entry can be opened michael@0: // during shutdown and we'll load the eviction info on next start. michael@0: LOG(("CacheFileContextEvictor::AddContext() - Cannot get an iterator. " michael@0: "[rv=0x%08x]", rv)); michael@0: mEntries.RemoveElement(entry); michael@0: return rv; michael@0: } michael@0: michael@0: StartEvicting(); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: CacheFileContextEvictor::CacheIndexStateChanged() michael@0: { michael@0: LOG(("CacheFileContextEvictor::CacheIndexStateChanged() [this=%p]", this)); michael@0: michael@0: MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); michael@0: michael@0: bool isUpToDate = false; michael@0: CacheIndex::IsUpToDate(&isUpToDate); michael@0: if (mEntries.Length() == 0) { michael@0: // Just save the state and exit, since there is nothing to do michael@0: mIndexIsUpToDate = isUpToDate; michael@0: return NS_OK; michael@0: } michael@0: michael@0: if (!isUpToDate && !mIndexIsUpToDate) { michael@0: // Index is outdated and status has not changed, nothing to do. michael@0: return NS_OK; michael@0: } michael@0: michael@0: if (isUpToDate && mIndexIsUpToDate) { michael@0: // Status has not changed, but make sure the eviction is running. michael@0: if (mEvicting) { michael@0: return NS_OK; michael@0: } michael@0: michael@0: // We're not evicting, but we should be evicting?! michael@0: LOG(("CacheFileContextEvictor::CacheIndexStateChanged() - Index is up to " michael@0: "date, we have some context to evict but eviction is not running! " michael@0: "Starting now.")); michael@0: } michael@0: michael@0: mIndexIsUpToDate = isUpToDate; michael@0: michael@0: if (mIndexIsUpToDate) { michael@0: CreateIterators(); michael@0: StartEvicting(); michael@0: } else { michael@0: CloseIterators(); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: CacheFileContextEvictor::WasEvicted(const nsACString &aKey, nsIFile *aFile, michael@0: bool *_retval) michael@0: { michael@0: LOG(("CacheFileContextEvictor::WasEvicted() [key=%s]", michael@0: PromiseFlatCString(aKey).get())); michael@0: michael@0: nsresult rv; michael@0: michael@0: MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); michael@0: michael@0: nsCOMPtr info = CacheFileUtils::ParseKey(aKey); michael@0: MOZ_ASSERT(info); michael@0: if (!info) { michael@0: LOG(("CacheFileContextEvictor::WasEvicted() - Cannot parse key!")); michael@0: *_retval = false; michael@0: return NS_OK; michael@0: } michael@0: michael@0: CacheFileContextEvictorEntry *entry = nullptr; michael@0: for (uint32_t i = 0; i < mEntries.Length(); ++i) { michael@0: if (info->Equals(mEntries[i]->mInfo)) { michael@0: entry = mEntries[i]; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (!entry) { michael@0: LOG(("CacheFileContextEvictor::WasEvicted() - Didn't find equal context, " michael@0: "returning false.")); michael@0: *_retval = false; michael@0: return NS_OK; michael@0: } michael@0: michael@0: PRTime lastModifiedTime; michael@0: rv = aFile->GetLastModifiedTime(&lastModifiedTime); michael@0: if (NS_FAILED(rv)) { michael@0: LOG(("CacheFileContextEvictor::WasEvicted() - Cannot get last modified time" michael@0: ", returning false.")); michael@0: *_retval = false; michael@0: return NS_OK; michael@0: } michael@0: michael@0: *_retval = !(lastModifiedTime > entry->mTimeStamp); michael@0: LOG(("CacheFileContextEvictor::WasEvicted() - returning %s. [mTimeStamp=%lld," michael@0: " lastModifiedTime=%lld]", *_retval ? "true" : "false", michael@0: mEntries[0]->mTimeStamp, lastModifiedTime)); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: CacheFileContextEvictor::PersistEvictionInfoToDisk( michael@0: nsILoadContextInfo *aLoadContextInfo) michael@0: { michael@0: LOG(("CacheFileContextEvictor::PersistEvictionInfoToDisk() [this=%p, " michael@0: "loadContextInfo=%p]", this, aLoadContextInfo)); michael@0: michael@0: nsresult rv; michael@0: michael@0: MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); michael@0: michael@0: nsCOMPtr file; michael@0: rv = GetContextFile(aLoadContextInfo, getter_AddRefs(file)); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) { michael@0: return rv; michael@0: } michael@0: michael@0: #ifdef PR_LOGGING michael@0: nsAutoCString path; michael@0: file->GetNativePath(path); michael@0: #endif michael@0: michael@0: PRFileDesc *fd; michael@0: rv = file->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, 0600, michael@0: &fd); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) { michael@0: LOG(("CacheFileContextEvictor::PersistEvictionInfoToDisk() - Creating file " michael@0: "failed! [path=%s, rv=0x%08x]", path.get(), rv)); michael@0: return rv; michael@0: } michael@0: michael@0: PR_Close(fd); michael@0: michael@0: LOG(("CacheFileContextEvictor::PersistEvictionInfoToDisk() - Successfully " michael@0: "created file. [path=%s]", path.get())); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: CacheFileContextEvictor::RemoveEvictInfoFromDisk( michael@0: nsILoadContextInfo *aLoadContextInfo) michael@0: { michael@0: LOG(("CacheFileContextEvictor::RemoveEvictInfoFromDisk() [this=%p, " michael@0: "loadContextInfo=%p]", this, aLoadContextInfo)); michael@0: michael@0: nsresult rv; michael@0: michael@0: MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); michael@0: michael@0: nsCOMPtr file; michael@0: rv = GetContextFile(aLoadContextInfo, getter_AddRefs(file)); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) { michael@0: return rv; michael@0: } michael@0: michael@0: #ifdef PR_LOGGING michael@0: nsAutoCString path; michael@0: file->GetNativePath(path); michael@0: #endif michael@0: michael@0: rv = file->Remove(false); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) { michael@0: LOG(("CacheFileContextEvictor::RemoveEvictionInfoFromDisk() - Removing file" michael@0: " failed! [path=%s, rv=0x%08x]", path.get(), rv)); michael@0: return rv; michael@0: } michael@0: michael@0: LOG(("CacheFileContextEvictor::RemoveEvictionInfoFromDisk() - Successfully " michael@0: "removed file. [path=%s]", path.get())); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: CacheFileContextEvictor::LoadEvictInfoFromDisk() michael@0: { michael@0: LOG(("CacheFileContextEvictor::LoadEvictInfoFromDisk() [this=%p]", this)); michael@0: michael@0: nsresult rv; michael@0: michael@0: MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); michael@0: michael@0: sDiskAlreadySearched = true; michael@0: michael@0: nsCOMPtr enumerator; michael@0: rv = mCacheDirectory->GetDirectoryEntries(getter_AddRefs(enumerator)); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) { michael@0: return rv; michael@0: } michael@0: michael@0: nsCOMPtr dirEnum = do_QueryInterface(enumerator, &rv); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) { michael@0: return rv; michael@0: } michael@0: michael@0: while (true) { michael@0: nsCOMPtr file; michael@0: rv = dirEnum->GetNextFile(getter_AddRefs(file)); michael@0: if (!file) { michael@0: break; michael@0: } michael@0: michael@0: bool isDir = false; michael@0: file->IsDirectory(&isDir); michael@0: if (isDir) { michael@0: continue; michael@0: } michael@0: michael@0: nsAutoCString leaf; michael@0: rv = file->GetNativeLeafName(leaf); michael@0: if (NS_FAILED(rv)) { michael@0: LOG(("CacheFileContextEvictor::LoadEvictInfoFromDisk() - " michael@0: "GetNativeLeafName() failed! Skipping file.")); michael@0: continue; michael@0: } michael@0: michael@0: if (leaf.Length() < kContextEvictionPrefixLength) { michael@0: continue; michael@0: } michael@0: michael@0: if (!StringBeginsWith(leaf, NS_LITERAL_CSTRING(kContextEvictionPrefix))) { michael@0: continue; michael@0: } michael@0: michael@0: nsAutoCString encoded; michael@0: encoded = Substring(leaf, kContextEvictionPrefixLength); michael@0: encoded.ReplaceChar('-', '/'); michael@0: michael@0: nsAutoCString decoded; michael@0: rv = Base64Decode(encoded, decoded); michael@0: if (NS_FAILED(rv)) { michael@0: LOG(("CacheFileContextEvictor::LoadEvictInfoFromDisk() - Base64 decoding " michael@0: "failed. Removing the file. [file=%s]", leaf.get())); michael@0: file->Remove(false); michael@0: continue; michael@0: } michael@0: michael@0: nsCOMPtr info = CacheFileUtils::ParseKey(decoded); michael@0: michael@0: if (!info) { michael@0: LOG(("CacheFileContextEvictor::LoadEvictInfoFromDisk() - Cannot parse " michael@0: "context key, removing file. [contextKey=%s, file=%s]", michael@0: decoded.get(), leaf.get())); michael@0: file->Remove(false); michael@0: continue; michael@0: } michael@0: michael@0: PRTime lastModifiedTime; michael@0: rv = file->GetLastModifiedTime(&lastModifiedTime); michael@0: if (NS_FAILED(rv)) { michael@0: continue; michael@0: } michael@0: michael@0: CacheFileContextEvictorEntry *entry = new CacheFileContextEvictorEntry(); michael@0: entry->mInfo = info; michael@0: entry->mTimeStamp = lastModifiedTime; michael@0: mEntries.AppendElement(entry); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: CacheFileContextEvictor::GetContextFile(nsILoadContextInfo *aLoadContextInfo, michael@0: nsIFile **_retval) michael@0: { michael@0: nsresult rv; michael@0: michael@0: nsAutoCString leafName; michael@0: leafName.Assign(NS_LITERAL_CSTRING(kContextEvictionPrefix)); michael@0: michael@0: nsAutoCString keyPrefix; michael@0: CacheFileUtils::AppendKeyPrefix(aLoadContextInfo, keyPrefix); michael@0: michael@0: // TODO: This hack is needed because current CacheFileUtils::ParseKey() can michael@0: // parse only the whole key and not just the key prefix generated by michael@0: // CacheFileUtils::CreateKeyPrefix(). This should be removed once bug #968593 michael@0: // is fixed. michael@0: keyPrefix.Append(":foo"); michael@0: michael@0: nsAutoCString data64; michael@0: rv = Base64Encode(keyPrefix, data64); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) { michael@0: return rv; michael@0: } michael@0: michael@0: // Replace '/' with '-' since '/' cannot be part of the filename. michael@0: data64.ReplaceChar('/', '-'); michael@0: michael@0: leafName.Append(data64); michael@0: michael@0: nsCOMPtr file; michael@0: rv = mCacheDirectory->Clone(getter_AddRefs(file)); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) { michael@0: return rv; michael@0: } michael@0: michael@0: rv = file->AppendNative(leafName); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) { michael@0: return rv; michael@0: } michael@0: michael@0: file.swap(*_retval); michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: CacheFileContextEvictor::CreateIterators() michael@0: { michael@0: LOG(("CacheFileContextEvictor::CreateIterators() [this=%p]", this)); michael@0: michael@0: CloseIterators(); michael@0: michael@0: nsresult rv; michael@0: michael@0: for (uint32_t i = 0; i < mEntries.Length(); ) { michael@0: rv = CacheIndex::GetIterator(mEntries[i]->mInfo, false, michael@0: getter_AddRefs(mEntries[i]->mIterator)); michael@0: if (NS_FAILED(rv)) { michael@0: LOG(("CacheFileContextEvictor::CreateIterators() - Cannot get an iterator" michael@0: ". [rv=0x%08x]", rv)); michael@0: mEntries.RemoveElementAt(i); michael@0: continue; michael@0: } michael@0: michael@0: ++i; michael@0: } michael@0: } michael@0: michael@0: void michael@0: CacheFileContextEvictor::CloseIterators() michael@0: { michael@0: LOG(("CacheFileContextEvictor::CloseIterators() [this=%p]", this)); michael@0: michael@0: for (uint32_t i = 0; i < mEntries.Length(); ++i) { michael@0: if (mEntries[i]->mIterator) { michael@0: mEntries[i]->mIterator->Close(); michael@0: mEntries[i]->mIterator = nullptr; michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: CacheFileContextEvictor::StartEvicting() michael@0: { michael@0: LOG(("CacheFileContextEvictor::StartEvicting() [this=%p]", this)); michael@0: michael@0: MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); michael@0: michael@0: if (mEvicting) { michael@0: LOG(("CacheFileContextEvictor::StartEvicting() - already evicintg.")); michael@0: return; michael@0: } michael@0: michael@0: if (mEntries.Length() == 0) { michael@0: LOG(("CacheFileContextEvictor::StartEvicting() - no context to evict.")); michael@0: return; michael@0: } michael@0: michael@0: nsCOMPtr ev; michael@0: ev = NS_NewRunnableMethod(this, &CacheFileContextEvictor::EvictEntries); michael@0: michael@0: nsRefPtr ioThread = CacheFileIOManager::IOThread(); michael@0: michael@0: nsresult rv = ioThread->Dispatch(ev, CacheIOThread::EVICT); michael@0: if (NS_FAILED(rv)) { michael@0: LOG(("CacheFileContextEvictor::StartEvicting() - Cannot dispatch event to " michael@0: "IO thread. [rv=0x%08x]", rv)); michael@0: } michael@0: michael@0: mEvicting = true; michael@0: } michael@0: michael@0: nsresult michael@0: CacheFileContextEvictor::EvictEntries() michael@0: { michael@0: LOG(("CacheFileContextEvictor::EvictEntries()")); michael@0: michael@0: nsresult rv; michael@0: michael@0: MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); michael@0: michael@0: mEvicting = false; michael@0: michael@0: if (!mIndexIsUpToDate) { michael@0: LOG(("CacheFileContextEvictor::EvictEntries() - Stopping evicting due to " michael@0: "outdated index.")); michael@0: return NS_OK; michael@0: } michael@0: michael@0: while (true) { michael@0: if (CacheIOThread::YieldAndRerun()) { michael@0: LOG(("CacheFileContextEvictor::EvictEntries() - Breaking loop for higher " michael@0: "level events.")); michael@0: mEvicting = true; michael@0: return NS_OK; michael@0: } michael@0: michael@0: if (mEntries.Length() == 0) { michael@0: LOG(("CacheFileContextEvictor::EvictEntries() - Stopping evicting, there " michael@0: "is no context to evict.")); michael@0: return NS_OK; michael@0: } michael@0: michael@0: SHA1Sum::Hash hash; michael@0: rv = mEntries[0]->mIterator->GetNextHash(&hash); michael@0: if (rv == NS_ERROR_NOT_AVAILABLE) { michael@0: LOG(("CacheFileContextEvictor::EvictEntries() - No more entries left in " michael@0: "iterator. [iterator=%p, info=%p]", mEntries[0]->mIterator.get(), michael@0: mEntries[0]->mInfo.get())); michael@0: RemoveEvictInfoFromDisk(mEntries[0]->mInfo); michael@0: mEntries.RemoveElementAt(0); michael@0: continue; michael@0: } else if (NS_FAILED(rv)) { michael@0: LOG(("CacheFileContextEvictor::EvictEntries() - Iterator failed to " michael@0: "provide next hash (shutdown?), keeping eviction info on disk." michael@0: " [iterator=%p, info=%p]", mEntries[0]->mIterator.get(), michael@0: mEntries[0]->mInfo.get())); michael@0: mEntries.RemoveElementAt(0); michael@0: continue; michael@0: } michael@0: michael@0: LOG(("CacheFileContextEvictor::EvictEntries() - Processing hash. " michael@0: "[hash=%08x%08x%08x%08x%08x, iterator=%p, info=%p]", LOGSHA1(&hash), michael@0: mEntries[0]->mIterator.get(), mEntries[0]->mInfo.get())); michael@0: michael@0: nsRefPtr handle; michael@0: CacheFileIOManager::gInstance->mHandles.GetHandle(&hash, false, michael@0: getter_AddRefs(handle)); michael@0: if (handle) { michael@0: // We doom any active handle in CacheFileIOManager::EvictByContext(), so michael@0: // this must be a new one. Skip it. michael@0: LOG(("CacheFileContextEvictor::EvictEntries() - Skipping entry since we " michael@0: "found an active handle. [handle=%p]", handle.get())); michael@0: continue; michael@0: } michael@0: michael@0: nsAutoCString leafName; michael@0: CacheFileIOManager::HashToStr(&hash, leafName); michael@0: michael@0: PRTime lastModifiedTime; michael@0: nsCOMPtr file; michael@0: rv = mEntriesDir->Clone(getter_AddRefs(file)); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: rv = file->AppendNative(leafName); michael@0: } michael@0: if (NS_SUCCEEDED(rv)) { michael@0: rv = file->GetLastModifiedTime(&lastModifiedTime); michael@0: } michael@0: if (NS_FAILED(rv)) { michael@0: LOG(("CacheFileContextEvictor::EvictEntries() - Cannot get last modified " michael@0: "time, skipping entry.")); michael@0: continue; michael@0: } michael@0: michael@0: if (lastModifiedTime > mEntries[0]->mTimeStamp) { michael@0: LOG(("CacheFileContextEvictor::EvictEntries() - Skipping newer entry. " michael@0: "[mTimeStamp=%lld, lastModifiedTime=%lld]", mEntries[0]->mTimeStamp, michael@0: lastModifiedTime)); michael@0: continue; michael@0: } michael@0: michael@0: LOG(("CacheFileContextEvictor::EvictEntries - Removing entry.")); michael@0: file->Remove(false); michael@0: CacheIndex::RemoveEntry(&hash); michael@0: } michael@0: michael@0: NS_NOTREACHED("We should never get here"); michael@0: return NS_OK; michael@0: } michael@0: michael@0: } // net michael@0: } // mozilla