Bug 624047 - LocalStorage value is lost after a few seconds, r=jst, a=final+

This commit is contained in:
Honza Bambas 2011-01-16 17:58:49 +01:00
parent 4d98a79ca9
commit 8e3917e7c8
9 changed files with 234 additions and 206 deletions

View File

@ -281,6 +281,12 @@ nsDOMStorageManager::Initialize()
os->AddObserver(gStorageManager, "profile-after-change", PR_FALSE);
os->AddObserver(gStorageManager, "perm-changed", PR_FALSE);
os->AddObserver(gStorageManager, "browser:purge-domain-data", PR_FALSE);
#ifdef MOZ_STORAGE
// Used for temporary table flushing
os->AddObserver(gStorageManager, "profile-before-change", PR_FALSE);
os->AddObserver(gStorageManager, NS_XPCOM_SHUTDOWN_OBSERVER_ID, PR_FALSE);
os->AddObserver(gStorageManager, NS_DOMSTORAGE_FLUSH_TIMER_OBSERVER, PR_FALSE);
#endif
return NS_OK;
}
@ -441,8 +447,6 @@ nsDOMStorageManager::Observe(nsISupports *aSubject,
nsCOMPtr<nsIObserverService> obsserv = mozilla::services::GetObserverService();
if (obsserv)
obsserv->NotifyObservers(nsnull, NS_DOMSTORAGE_FLUSH_TIMER_OBSERVER, nsnull);
if (!UnflushedDataExists())
DOMStorageImpl::gStorageDB->StopTempTableFlushTimer();
} else if (!strcmp(aTopic, "browser:purge-domain-data")) {
// Convert the domain name to the ACE format
nsCAutoString aceDomain;
@ -470,6 +474,19 @@ nsDOMStorageManager::Observe(nsISupports *aSubject,
NS_ENSURE_SUCCESS(rv, rv);
DOMStorageImpl::gStorageDB->RemoveOwner(aceDomain, PR_TRUE);
} else if (!strcmp(aTopic, "profile-before-change") ||
!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
if (DOMStorageImpl::gStorageDB) {
nsresult rv = DOMStorageImpl::gStorageDB->FlushAndDeleteTemporaryTables(true);
if (NS_FAILED(rv))
NS_WARNING("DOMStorage: temporary table commit failed");
}
} else if (!strcmp(aTopic, NS_DOMSTORAGE_FLUSH_TIMER_OBSERVER)) {
if (DOMStorageImpl::gStorageDB) {
nsresult rv = DOMStorageImpl::gStorageDB->FlushAndDeleteTemporaryTables(false);
if (NS_FAILED(rv))
NS_WARNING("DOMStorage: temporary table commit failed");
}
#endif
}
@ -539,26 +556,6 @@ nsDOMStorageManager::RemoveFromStoragesHash(DOMStorageImpl* aStorage)
mStorages.RemoveEntry(aStorage);
}
static PLDHashOperator
CheckUnflushedData(nsDOMStorageEntry* aEntry, void* userArg)
{
if (aEntry->mStorage->WasTemporaryTableLoaded()) {
PRBool *unflushedData = (PRBool*)userArg;
*unflushedData = PR_TRUE;
return PL_DHASH_STOP;
}
return PL_DHASH_NEXT;
}
PRBool
nsDOMStorageManager::UnflushedDataExists()
{
PRBool unflushedData = PR_FALSE;
mStorages.EnumerateEntries(CheckUnflushedData, &unflushedData);
return unflushedData;
}
//
// nsDOMStorage
//
@ -726,25 +723,19 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMStorageImpl)
}
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTING_ADDREF_AMBIGUOUS(DOMStorageImpl, nsIObserver)
NS_IMPL_CYCLE_COLLECTING_RELEASE_AMBIGUOUS(DOMStorageImpl, nsIObserver)
NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMStorageImpl)
NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMStorageImpl)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMStorageImpl)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
NS_INTERFACE_MAP_ENTRY(nsIObserver)
NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
DOMStorageImpl::DOMStorageImpl(nsDOMStorage* aStorage)
: mLoadedTemporaryTable(false)
{
Init(aStorage);
}
DOMStorageImpl::DOMStorageImpl(nsDOMStorage* aStorage, DOMStorageImpl& aThat)
: DOMStorageBase(aThat)
, mLoadedTemporaryTable(aThat.mLoadedTemporaryTable)
, mLastTemporaryTableAccessTime(aThat.mLastTemporaryTableAccessTime)
, mTemporaryTableAge(aThat.mTemporaryTableAge)
{
Init(aStorage);
}
@ -807,8 +798,6 @@ DOMStorageImpl::InitFromChild(bool aUseDB, bool aCanUseChromePersist,
mQuotaDomainDBKey = aQuotaDomainDBKey;
mQuotaETLDplus1DomainDBKey = aQuotaETLDplus1DomainDBKey;
mStorageType = static_cast<nsPIDOMStorage::nsDOMStorageType>(aStorageType);
if (mStorageType != nsPIDOMStorage::SessionStorage)
RegisterObservers();
}
void
@ -828,14 +817,12 @@ DOMStorageImpl::InitAsLocalStorage(nsIURI* aDomainURI,
bool aCanUseChromePersist)
{
DOMStorageBase::InitAsLocalStorage(aDomainURI, aCanUseChromePersist);
RegisterObservers();
}
void
DOMStorageImpl::InitAsGlobalStorage(const nsACString& aDomainDemanded)
{
DOMStorageBase::InitAsGlobalStorage(aDomainDemanded);
RegisterObservers();
}
bool
@ -1032,86 +1019,6 @@ DOMStorageImpl::CloneFrom(bool aCallerSecure, DOMStorageBase* aThat)
return NS_OK;
}
nsresult
DOMStorageImpl::RegisterObservers()
{
nsCOMPtr<nsIObserverService> obsserv = mozilla::services::GetObserverService();
if (obsserv) {
obsserv->AddObserver(this, "profile-before-change", PR_TRUE);
obsserv->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, PR_TRUE);
obsserv->AddObserver(this, NS_DOMSTORAGE_FLUSH_TIMER_OBSERVER, PR_TRUE);
}
return NS_OK;
}
nsresult
DOMStorageImpl::MaybeCommitTemporaryTable(bool force)
{
#ifdef MOZ_STORAGE
if (!UseDB())
return NS_OK;
if (!mLoadedTemporaryTable)
return NS_OK;
// If we are not forced to flush (e.g. on shutdown) then don't flush if the
// last table access is less then 5 seconds ago or the table itself is not
// older then 30 secs
if (!force &&
((TimeStamp::Now() - mLastTemporaryTableAccessTime).ToSeconds() <
NS_DOMSTORAGE_MAXIMUM_TEMPTABLE_INACTIVITY_TIME) &&
((TimeStamp::Now() - mTemporaryTableAge).ToSeconds() <
NS_DOMSTORAGE_MAXIMUM_TEMPTABLE_AGE))
return NS_OK;
return gStorageDB->FlushAndDeleteTemporaryTableForStorage(this);
#endif
return NS_OK;
}
bool
DOMStorageImpl::WasTemporaryTableLoaded()
{
return mLoadedTemporaryTable;
}
void
DOMStorageImpl::SetTemporaryTableLoaded(bool loaded)
{
if (loaded) {
mLastTemporaryTableAccessTime = TimeStamp::Now();
if (!mLoadedTemporaryTable)
mTemporaryTableAge = mLastTemporaryTableAccessTime;
gStorageDB->EnsureTempTableFlushTimer();
}
mLoadedTemporaryTable = loaded;
}
NS_IMETHODIMP
DOMStorageImpl::Observe(nsISupports *subject,
const char *topic,
const PRUnichar *data)
{
bool isProfileBeforeChange = !strcmp(topic, "profile-before-change");
bool isXPCOMShutdown = !strcmp(topic, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
bool isFlushTimer = !strcmp(topic, NS_DOMSTORAGE_FLUSH_TIMER_OBSERVER);
if (isXPCOMShutdown || isProfileBeforeChange || isFlushTimer) {
nsresult rv = MaybeCommitTemporaryTable(isXPCOMShutdown || isProfileBeforeChange);
if (NS_FAILED(rv)) {
NS_WARNING("DOMStorage: temporary table commit failed");
}
return NS_OK;
}
NS_WARNING("Unrecognized topic in nsDOMStorage::Observe");
return NS_OK;
}
nsresult
DOMStorageImpl::CacheKeysFromDB()
{

View File

@ -61,7 +61,6 @@
#include "nsIObserver.h"
#include "nsITimer.h"
#include "nsWeakReference.h"
#include "mozilla/TimeStamp.h"
#define NS_DOMSTORAGE_FLUSH_TIMER_OBSERVER "domstorage-flush-timer"
@ -76,9 +75,7 @@
class nsDOMStorage;
class nsIDOMStorage;
class nsDOMStorageItem;
using mozilla::TimeStamp;
using mozilla::TimeDuration;
class nsDOMStoragePersistentDB;
namespace mozilla {
namespace dom {
@ -244,14 +241,11 @@ protected:
};
class DOMStorageImpl : public DOMStorageBase
, public nsIObserver
, public nsSupportsWeakReference
{
public:
NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(DOMStorageImpl, nsIObserver)
NS_DECL_CYCLE_COLLECTION_CLASS(DOMStorageImpl)
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_NSIOBSERVER
DOMStorageImpl(nsDOMStorage*);
DOMStorageImpl(nsDOMStorage*, DOMStorageImpl&);
@ -314,12 +308,6 @@ public:
virtual nsresult
CloneFrom(bool aCallerSecure, DOMStorageBase* aThat);
nsresult RegisterObservers();
nsresult MaybeCommitTemporaryTable(bool force);
bool WasTemporaryTableLoaded();
void SetTemporaryTableLoaded(bool loaded);
virtual bool CacheStoragePermissions();
private:
@ -327,6 +315,7 @@ private:
static nsDOMStorageDBWrapper* gStorageDB;
#endif
friend class nsDOMStorageManager;
friend class nsDOMStoragePersistentDB;
friend class StorageParent;
void Init(nsDOMStorage*);
@ -351,10 +340,6 @@ private:
// Weak reference to the owning storage instance
nsDOMStorage* mOwner;
bool mLoadedTemporaryTable;
TimeStamp mLastTemporaryTableAccessTime;
TimeStamp mTemporaryTableAge;
};
class nsDOMStorage : public nsIDOMStorageObsolete,

View File

@ -75,7 +75,6 @@ nsDOMStorageDBWrapper::nsDOMStorageDBWrapper()
nsDOMStorageDBWrapper::~nsDOMStorageDBWrapper()
{
StopTempTableFlushTimer();
}
nsresult
@ -99,29 +98,18 @@ nsDOMStorageDBWrapper::Init()
}
nsresult
nsDOMStorageDBWrapper::EnsureLoadTemporaryTableForStorage(DOMStorageImpl* aStorage)
nsDOMStorageDBWrapper::FlushAndDeleteTemporaryTables(bool force)
{
if (aStorage->CanUseChromePersist())
return mChromePersistentDB.EnsureLoadTemporaryTableForStorage(aStorage);
if (nsDOMStorageManager::gStorageManager->InPrivateBrowsingMode())
return NS_OK;
if (aStorage->SessionOnly())
return NS_OK;
nsresult rv1, rv2;
rv1 = mChromePersistentDB.FlushTemporaryTables(force);
rv2 = mPersistentDB.FlushTemporaryTables(force);
return mPersistentDB.EnsureLoadTemporaryTableForStorage(aStorage);
}
// Everything flushed? Then no need for a timer.
if (!mChromePersistentDB.mTempTableLoads.Count() &&
!mPersistentDB.mTempTableLoads.Count())
StopTempTableFlushTimer();
nsresult
nsDOMStorageDBWrapper::FlushAndDeleteTemporaryTableForStorage(DOMStorageImpl* aStorage)
{
if (aStorage->CanUseChromePersist())
return mChromePersistentDB.FlushAndDeleteTemporaryTableForStorage(aStorage);
if (nsDOMStorageManager::gStorageManager->InPrivateBrowsingMode())
return NS_OK;
if (aStorage->SessionOnly())
return NS_OK;
return mPersistentDB.FlushAndDeleteTemporaryTableForStorage(aStorage);
return NS_FAILED(rv1) ? rv1 : rv2;
}
nsresult

View File

@ -92,11 +92,6 @@ public:
nsresult
Init();
nsresult
EnsureLoadTemporaryTableForStorage(DOMStorageImpl* aStorage);
nsresult
FlushAndDeleteTemporaryTableForStorage(DOMStorageImpl* aStorage);
/**
* Retrieve a list of all the keys associated with a particular domain.
*/
@ -227,6 +222,14 @@ public:
*/
void EnsureTempTableFlushTimer();
/**
* Called by the timer or on shutdown/profile change to flush all temporary
* tables that are too long in memory to disk.
* Set force to flush even a table doesn't meet the age limits. Used during
* shutdown.
*/
nsresult FlushAndDeleteTemporaryTables(bool force);
/**
* Stops the temp table flush timer.
*/

View File

@ -55,6 +55,10 @@
#include "nsPrintfCString.h"
#include "nsNetUtil.h"
// Temporary tables for a storage scope will be flushed if found older
// then this time in seconds since the load
#define TEMP_TABLE_MAX_AGE (10) // seconds
class nsReverseStringSQLFunction : public mozIStorageFunction
{
NS_DECL_ISUPPORTS
@ -98,6 +102,7 @@ NS_IMPL_ISUPPORTS1(nsIsOfflineSQLFunction, mozIStorageFunction)
nsDOMStoragePersistentDB::nsDOMStoragePersistentDB()
{
mTempTableLoads.Init(16);
}
NS_IMETHODIMP
@ -446,7 +451,9 @@ nsDOMStoragePersistentDB::Init(const nsString& aDatabaseName)
nsresult
nsDOMStoragePersistentDB::EnsureLoadTemporaryTableForStorage(DOMStorageImpl* aStorage)
{
if (!aStorage->WasTemporaryTableLoaded()) {
TimeStamp timeStamp;
if (!mTempTableLoads.Get(aStorage->GetScopeDBKey(), &timeStamp)) {
nsresult rv;
rv = MaybeCommitInsertTransaction();
@ -466,57 +473,76 @@ nsDOMStoragePersistentDB::EnsureLoadTemporaryTableForStorage(DOMStorageImpl* aSt
rv = mCopyToTempTableStatement->Execute();
NS_ENSURE_SUCCESS(rv, rv);
}
// Always call this to update the last access time
aStorage->SetTemporaryTableLoaded(true);
mTempTableLoads.Put(aStorage->GetScopeDBKey(), TimeStamp::Now());
DOMStorageImpl::gStorageDB->EnsureTempTableFlushTimer();
}
return NS_OK;
}
nsresult
nsDOMStoragePersistentDB::FlushAndDeleteTemporaryTableForStorage(DOMStorageImpl* aStorage)
/* static */
PLDHashOperator
nsDOMStoragePersistentDB::FlushTemporaryTable(nsCStringHashKey::KeyType aKey,
TimeStamp& aData,
void* aUserArg)
{
if (!aStorage->WasTemporaryTableLoaded())
return NS_OK;
FlushTemporaryTableData* data = (FlushTemporaryTableData*)aUserArg;
if (!data->mForce &&
((TimeStamp::Now() - aData).ToSeconds() < TEMP_TABLE_MAX_AGE))
return PL_DHASH_NEXT;
{
mozStorageStatementScoper scope(data->mDB->mCopyBackToDiskStatement);
Binder binder(data->mDB->mCopyBackToDiskStatement, &data->mRV);
NS_ENSURE_SUCCESS(data->mRV, PL_DHASH_STOP);
data->mRV = binder->BindUTF8StringByName(NS_LITERAL_CSTRING("scope"), aKey);
NS_ENSURE_SUCCESS(data->mRV, PL_DHASH_STOP);
data->mRV = binder.Add();
NS_ENSURE_SUCCESS(data->mRV, PL_DHASH_STOP);
data->mRV = data->mDB->mCopyBackToDiskStatement->Execute();
NS_ENSURE_SUCCESS(data->mRV, PL_DHASH_STOP);
}
{
mozStorageStatementScoper scope(data->mDB->mDeleteTemporaryTableStatement);
Binder binder(data->mDB->mDeleteTemporaryTableStatement, &data->mRV);
NS_ENSURE_SUCCESS(data->mRV, PL_DHASH_STOP);
data->mRV = binder->BindUTF8StringByName(NS_LITERAL_CSTRING("scope"), aKey);
NS_ENSURE_SUCCESS(data->mRV, PL_DHASH_STOP);
data->mRV = binder.Add();
NS_ENSURE_SUCCESS(data->mRV, PL_DHASH_STOP);
data->mRV = data->mDB->mDeleteTemporaryTableStatement->Execute();
NS_ENSURE_SUCCESS(data->mRV, PL_DHASH_STOP);
}
return PL_DHASH_REMOVE;
}
nsresult
nsDOMStoragePersistentDB::FlushTemporaryTables(bool force)
{
mozStorageTransaction trans(mConnection, PR_FALSE);
nsresult rv;
{
mozStorageStatementScoper scope(mCopyBackToDiskStatement);
FlushTemporaryTableData data;
data.mDB = this;
data.mForce = force;
data.mRV = NS_OK;
Binder binder(mCopyBackToDiskStatement, &rv);
NS_ENSURE_SUCCESS(rv, rv);
rv = binder->BindUTF8StringByName(NS_LITERAL_CSTRING("scope"),
aStorage->GetScopeDBKey());
NS_ENSURE_SUCCESS(rv, rv);
rv = binder.Add();
NS_ENSURE_SUCCESS(rv, rv);
rv = mCopyBackToDiskStatement->Execute();
NS_ENSURE_SUCCESS(rv, rv);
}
{
mozStorageStatementScoper scope(mDeleteTemporaryTableStatement);
Binder binder(mDeleteTemporaryTableStatement, &rv);
NS_ENSURE_SUCCESS(rv, rv);
rv = binder->BindUTF8StringByName(NS_LITERAL_CSTRING("scope"),
aStorage->GetScopeDBKey());
NS_ENSURE_SUCCESS(rv, rv);
rv = binder.Add();
NS_ENSURE_SUCCESS(rv, rv);
rv = mDeleteTemporaryTableStatement->Execute();
NS_ENSURE_SUCCESS(rv, rv);
}
mTempTableLoads.Enumerate(FlushTemporaryTable, &data);
NS_ENSURE_SUCCESS(data.mRV, data.mRV);
rv = trans.Commit();
NS_ENSURE_SUCCESS(rv, rv);
@ -524,8 +550,6 @@ nsDOMStoragePersistentDB::FlushAndDeleteTemporaryTableForStorage(DOMStorageImpl*
rv = MaybeCommitInsertTransaction();
NS_ENSURE_SUCCESS(rv, rv);
aStorage->SetTemporaryTableLoaded(false);
return NS_OK;
}

View File

@ -43,10 +43,15 @@
#include "mozIStorageConnection.h"
#include "mozIStorageStatement.h"
#include "nsTHashtable.h"
#include "nsDataHashtable.h"
#include "mozilla/TimeStamp.h"
class DOMStorageImpl;
class nsSessionStorageEntry;
using mozilla::TimeStamp;
using mozilla::TimeDuration;
class nsDOMStoragePersistentDB
{
public:
@ -56,11 +61,6 @@ public:
nsresult
Init(const nsString& aDatabaseName);
nsresult
EnsureLoadTemporaryTableForStorage(DOMStorageImpl* aStorage);
nsresult
FlushAndDeleteTemporaryTableForStorage(DOMStorageImpl* aStorage);
/**
* Retrieve a list of all the keys associated with a particular domain.
*/
@ -162,7 +162,26 @@ public:
*/
nsresult MaybeCommitInsertTransaction();
/**
* Flushes all temporary tables based on time or forcibly during shutdown.
*/
nsresult FlushTemporaryTables(bool force);
protected:
/**
* Ensures that a temporary table is correctly filled for the scope of
* the given storage.
*/
nsresult EnsureLoadTemporaryTableForStorage(DOMStorageImpl* aStorage);
struct FlushTemporaryTableData {
nsDOMStoragePersistentDB* mDB;
bool mForce;
nsresult mRV;
};
static PLDHashOperator FlushTemporaryTable(nsCStringHashKey::KeyType aKey,
TimeStamp& aData,
void* aUserArg);
nsCOMPtr<mozIStorageConnection> mConnection;
@ -183,6 +202,11 @@ protected:
nsCString mCachedOwner;
PRInt32 mCachedUsage;
// Maps ScopeDBKey to time of the temporary table load for that scope.
// If a record is present, the temp table has been loaded. If it is not
// present, the table has not yet been loaded or has alrady been flushed.
nsDataHashtable<nsCStringHashKey, TimeStamp> mTempTableLoads;
friend class nsDOMStorageDBWrapper;
friend class nsDOMStorageMemoryDB;
nsresult

View File

@ -47,6 +47,7 @@ include $(DEPTH)/config/autoconf.mk
include $(topsrcdir)/config/rules.mk
_TEST_FILES = \
frameBug624047.html \
frameChromeSlave.html \
frameMasterEqual.html \
frameMasterNotEqual.html \
@ -61,6 +62,7 @@ _TEST_FILES = \
interOriginTest2.js \
pbSwitch.js \
test_brokenUTF-16.html \
test_bug624047.html \
test_cookieBlock.html \
test_cookieSession-phase1.html \
test_cookieSession-phase2.html \

View File

@ -0,0 +1,30 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>slave for bug 624047 test</title>
<script type="text/javascript" src="interOriginFrame.js"></script>
<script type="text/javascript">
function doStep()
{
localStorage.name = 1;
var timer = setInterval(function() {
is(localStorage.name, 1, "Value is still present");
}, 1000);
setTimeout(function() {
clearTimeout(timer);
localStorage.clear();
postMsg("done");
}, 12000);
return false;
}
</script>
</head>
<body onload="postMsg('frame loaded');">
</body>
</html>

View File

@ -0,0 +1,65 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>bug 624047</title>
<script type="text/javascript" src="/MochiKit/packed.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="interOriginTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
<script type="text/javascript">
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
/*
This test does the folliwing:
- loads a page in an iframe that stores a value to localStorage
- sooner then in 5 seconds reloads the page
- now the page later then 5 seconds after load in the first step checks the
value is still present in localStorage (what is expected)
- if not, the bug is still present
*/
function flushTables()
{
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
var storageManager = Components.classes["@mozilla.org/dom/storagemanager;1"]
.getService(Components.interfaces.nsIObserver);
storageManager.observe(null, "profile-before-change", null);
}
function startTest()
{
slaveOrigin = "http://sub2.test2.example.org";
slave = document.getElementById("__test_frame").contentWindow;
flushTables();
slave.location = slaveOrigin + slavePath + "frameBug624047.html";
setTimeout(function() {
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
slaveLoadsPending = 1;
slave.location.reload();
}, 2000);
}
function doNextTest()
{
SimpleTest.finish();
}
function doStep()
{
}
SimpleTest.waitForExplicitFinish();
</script>
</head>
<body onload="startTest();">
This test takes about 15s to complete... Please wait...
<br/>
<iframe src="" id="__test_frame"></iframe>
</body>
</html>