You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
501 lines
16 KiB
501 lines
16 KiB
#include "stdinc.h"
|
|
#include "csrss.h"
|
|
|
|
#define CSRSS_TEST_DIR_NAME (L"csrss")
|
|
#define CSRSS_TEST_DIR_NAME_CCH (NUMBER_OF(CSRSS_TEST_DIR_NAME) - 1)
|
|
#define CSRSS_SETTINGS_FILE_NAME (L"csrss.ini")
|
|
#define CSRSS_SETTINGS_FILE_NAME_CCH (NUMBER_OF(CSRSS_SETTINGS_FILE_NAME) - 1)
|
|
#define CSRSS_INI_SECTION_TITLE (L"csrss")
|
|
|
|
|
|
BOOL pOpenStreamOnFile(
|
|
PCWSTR pcwszFilename,
|
|
IStream** ppStream,
|
|
PCWSTR pcwszResourceType = NULL,
|
|
PCWSTR pcwszResourceName = NULL,
|
|
WORD Language = 0)
|
|
{
|
|
FN_PROLOG_WIN32
|
|
|
|
PARAMETER_CHECK(pcwszFilename);
|
|
PARAMETER_CHECK(ppStream);
|
|
|
|
*ppStream = NULL;
|
|
|
|
//
|
|
// If this is non-null, then we have to open the file as an image and get the
|
|
// resource specified. Otherwise, we just open the file like a normal file.
|
|
//
|
|
if ( pcwszResourceName )
|
|
{
|
|
CResourceStream *pResourceStream = NULL;
|
|
|
|
IFW32NULL_EXIT(pResourceStream = FUSION_NEW_SINGLETON(CResourceStream));
|
|
IFW32FALSE_EXIT(pResourceStream->Initialize(
|
|
pcwszFilename,
|
|
pcwszResourceType,
|
|
pcwszResourceName,
|
|
Language));
|
|
*ppStream = pResourceStream;
|
|
}
|
|
else
|
|
{
|
|
CReferenceCountedFileStream *pFileStream = NULL;
|
|
CImpersonationData ImpData;
|
|
IFW32NULL_EXIT(pFileStream = FUSION_NEW_SINGLETON(CReferenceCountedFileStream));
|
|
IFW32FALSE_EXIT(pFileStream->OpenForRead(pcwszFilename, ImpData, FILE_SHARE_READ, OPEN_EXISTING, 0));
|
|
*ppStream = pFileStream;
|
|
}
|
|
|
|
if ( *ppStream ) (*ppStream)->AddRef();
|
|
|
|
FN_EPILOG
|
|
}
|
|
|
|
BOOL ParseDecimalOrHexString(PCWSTR pcwszString, SIZE_T cch, ULONG &out )
|
|
{
|
|
BOOL fIsHex;
|
|
|
|
FN_PROLOG_WIN32
|
|
|
|
PARAMETER_CHECK(pcwszString != NULL);
|
|
|
|
fIsHex = ((cch > 2 ) && ( pcwszString[0] == L'0' ) &&
|
|
((pcwszString[1] == L'x') || (pcwszString[1] == L'X')));
|
|
|
|
if ( fIsHex )
|
|
{
|
|
pcwszString += 2;
|
|
cch -= 2;
|
|
}
|
|
|
|
out = 0;
|
|
|
|
while ( cch )
|
|
{
|
|
const int val = SxspHexDigitToValue((*pcwszString));
|
|
PARAMETER_CHECK( fIsHex || ( val < 10 ) );
|
|
out = out * ( fIsHex ? 16 : 10 ) + val;
|
|
cch--;
|
|
pcwszString++;
|
|
}
|
|
|
|
FN_EPILOG
|
|
}
|
|
|
|
|
|
class CCsrssPoundingThreadEntry
|
|
{
|
|
PRIVATIZE_COPY_CONSTRUCTORS(CCsrssPoundingThreadEntry);
|
|
public:
|
|
CDequeLinkage Linkage;
|
|
SXS_GENERATE_ACTIVATION_CONTEXT_PARAMETERS Request;
|
|
ULONG ulRuns;
|
|
BOOL fStopNextRound;
|
|
BOOL fShouldSucceed;
|
|
CThread hOurThreadHandle;
|
|
CStringBuffer buffTestDirectory;
|
|
CSmallStringBuffer buffTestName;
|
|
DWORD dwSleepTime;
|
|
|
|
CSmallStringBuffer buffProcArch;
|
|
CStringBuffer buffAssemblyDirectory;
|
|
CStringBuffer buffTextualIdentityString;
|
|
CStringBuffer buffManifestStreamPath;
|
|
CStringBuffer buffPolicyStreamPath;
|
|
|
|
CCsrssPoundingThreadEntry() : ulRuns(0), fStopNextRound(FALSE) { }
|
|
BOOL AcquireSettingsFrom( PCWSTR pcwszSettingsFile );
|
|
DWORD DoWork();
|
|
BOOL StopAndWaitForCompletion();
|
|
|
|
static DWORD WINAPI ThreadProcEntry( PVOID pv )
|
|
{
|
|
CCsrssPoundingThreadEntry *pEntry = NULL;
|
|
|
|
pEntry = reinterpret_cast<CCsrssPoundingThreadEntry*>(pv);
|
|
return ( pEntry != NULL ) ? pEntry->DoWork() : 0;
|
|
}
|
|
};
|
|
|
|
|
|
BOOL
|
|
CCsrssPoundingThreadEntry::StopAndWaitForCompletion()
|
|
{
|
|
this->fStopNextRound = true;
|
|
return WaitForSingleObject(this->hOurThreadHandle, INFINITE) == WAIT_OBJECT_0;
|
|
}
|
|
|
|
DWORD
|
|
CCsrssPoundingThreadEntry::DoWork()
|
|
{
|
|
if ( !WaitForThreadResumeEvent() )
|
|
goto Exit;
|
|
|
|
while ( !this->fStopNextRound )
|
|
{
|
|
//
|
|
// Call to generate the structure
|
|
//
|
|
BOOL fResult;
|
|
SXS_GENERATE_ACTIVATION_CONTEXT_PARAMETERS TempParams = this->Request;
|
|
CSmartRef<IStream> isManifest;
|
|
CSmartRef<IStream> isPolicy;
|
|
|
|
if ( this->buffManifestStreamPath.Cch() != 0 )
|
|
{
|
|
if (pOpenStreamOnFile(this->buffManifestStreamPath, &isManifest))
|
|
{
|
|
TempParams.Manifest.Path = this->buffManifestStreamPath;
|
|
TempParams.Manifest.PathType = ACTIVATION_CONTEXT_PATH_TYPE_WIN32_FILE;
|
|
TempParams.Manifest.Stream = isManifest;
|
|
}
|
|
}
|
|
|
|
if ( this->buffPolicyStreamPath.Cch() != 0 )
|
|
{
|
|
if (pOpenStreamOnFile(this->buffPolicyStreamPath, &isPolicy))
|
|
{
|
|
TempParams.Policy.Path = this->buffManifestStreamPath;
|
|
TempParams.Policy.PathType = ACTIVATION_CONTEXT_PATH_TYPE_WIN32_FILE;
|
|
TempParams.Policy.Stream = isPolicy;
|
|
}
|
|
}
|
|
|
|
fResult = SxsGenerateActivationContext( &TempParams );
|
|
|
|
//
|
|
// Did we fail when we were to succeed, or succeed when we were to fail?
|
|
//
|
|
if ( ( !fResult && this->fShouldSucceed ) || ( fResult && !this->fShouldSucceed ) )
|
|
{
|
|
const DWORD dwLastError = ::FusionpGetLastWin32Error();
|
|
::ReportFailure("CsrssStress: Test %ls expected %ls, got %ls; Error %ld\n",
|
|
static_cast<PCWSTR>(this->buffTestName),
|
|
this->fShouldSucceed ? L"success" : L"failure",
|
|
fResult ? L"success" : L"failure",
|
|
dwLastError);
|
|
}
|
|
else
|
|
{
|
|
wprintf(L"CsrssStress: Test %ls passed\n", static_cast<PCWSTR>(this->buffTestName));
|
|
}
|
|
|
|
if ((TempParams.SectionObjectHandle != INVALID_HANDLE_VALUE ) &&
|
|
(TempParams.SectionObjectHandle != NULL))
|
|
{
|
|
CloseHandle(TempParams.SectionObjectHandle);
|
|
}
|
|
|
|
if ( !this->fStopNextRound )
|
|
::Sleep(this->dwSleepTime);
|
|
|
|
}
|
|
|
|
Exit:
|
|
return 0;
|
|
}
|
|
|
|
#define SLEN(n) (NUMBER_OF(n)-1)
|
|
#define CSRSS_INI_KEY_PROC_ARCH (L"ProcArch")
|
|
#define CSRSS_INI_KEY_PROC_ARCH_CCH SLEN(CSRSS_INI_KEY_PROC_ARCH)
|
|
|
|
#define CSRSS_INI_KEY_LANGID (L"LangId")
|
|
#define CSRSS_INI_KEY_LANGID_CCH SLEN(CSRSS_INI_KEY_PROC_ARCH)
|
|
|
|
#define CSRSS_INI_KEY_ASMDIR (L"AssemblyDirectory")
|
|
#define CSRSS_INI_KEY_ASMDIR_CCH SLEN(CSRSS_INI_KEY_PROC_ARCH)
|
|
|
|
#define CSRSS_INI_KEY_TEXTUALIDENT (L"TextualIdentity")
|
|
#define CSRSS_INI_KEY_TEXTUALIDENT_CCH SLEN(CSRSS_INI_KEY_PROC_ARCH)
|
|
|
|
#define CSRSS_INI_KEY_MANIFEST (L"ManifestPath")
|
|
#define CSRSS_INI_KEY_MANIFEST_CCH SLEN(CSRSS_INI_KEY_PROC_ARCH)
|
|
|
|
#define CSRSS_INI_KEY_POLICY (L"PolicyPath")
|
|
#define CSRSS_INI_KEY_POLICY_CCH SLEN(CSRSS_INI_KEY_PROC_ARCH)
|
|
|
|
#define CSRSS_INI_KEY_SUCCESS (L"ShouldSucceed")
|
|
#define CSRSS_INI_KEY_SUCCESS_CCH SLEN(CSRSS_INI_KEY_SUCCESS)
|
|
|
|
#define CSRSS_INI_KEY_SLEEP (L"SleepTime")
|
|
#define CSRSS_INI_KEY_SLEEP_CCH SLEN(CSRSS_INI_KEY_SLEEP)
|
|
|
|
#define CSRSS_INI_KEY_SYSDEFAULTIDENTFLAG (L"SysDefaultTextualIdentityFlag")
|
|
#define CSRSS_INI_KEY_SYSDEFAULTIDENTFLAG_CCH SLEN(CSRSS_INI_KEY_SYSDEFAULTIDENTFLAG)
|
|
|
|
#define CSRSS_INI_KEY_TEXTUALIDENTFLAG (L"TextualIdentityFlag")
|
|
#define CSRSS_INI_KEY_TEXTUALIDENTFLAG_CCH SLEN(CSRSS_INI_KEY_TEXTUALIDENTFLAG);
|
|
|
|
BOOL CCsrssPoundingThreadEntry::AcquireSettingsFrom( PCWSTR pcwszSettingsFile )
|
|
{
|
|
FN_PROLOG_WIN32
|
|
|
|
LANGID lidCurrentLang = GetUserDefaultUILanguage();
|
|
CSmallStringBuffer buffJunk;
|
|
BOOL fDumpBool;
|
|
|
|
ZeroMemory(&this->Request, sizeof(this->Request));
|
|
|
|
//
|
|
// Format of the settings file:
|
|
//
|
|
// [testname]
|
|
// SysDefaultTextualIdentityFlag = yes|no (add SXS_GENERATE_ACTIVATION_CONTEXT_FLAG_SYSTEM_DEFAULT_TEXTUAL_ASSEMBLY_IDENTITY)
|
|
// TextualIdentityFlag = yes|no (add SXS_GENERATE_ACTIVATION_CONTEXT_FLAG_TEXTUAL_ASSEMBLY_IDENTITY)
|
|
// ProcArch = PA ident string (will use FusionpParseProcessorArchitecture)
|
|
// LangId = number or string
|
|
// AssemblyDirectory = dirname
|
|
// TextualIdentity = textualIdentityString
|
|
// ManifestPath = manifest name under test directory
|
|
// PolicyPath = policy path file name under test directory
|
|
// ShouldSucceed = yes|no - whether this test succeeds or fails
|
|
//
|
|
// Flags is required.
|
|
// PA and LangId, if not present, are defaulted to the current user's settings.
|
|
// AssemblyDirectory, if not present, defaults to %systemroot%\winsxs
|
|
// TextualIdentity is required.
|
|
// ManifestPath is required.
|
|
//
|
|
// If textualIdentity is present, then the streams are not created.
|
|
//
|
|
|
|
//
|
|
// Flags are set by key names
|
|
//
|
|
IFW32FALSE_EXIT(SxspIsPrivateProfileStringEqual(CSRSS_INI_SECTION_TITLE, CSRSS_INI_KEY_SYSDEFAULTIDENTFLAG, L"yes", fDumpBool, pcwszSettingsFile));
|
|
if ( fDumpBool )
|
|
this->Request.Flags |= SXS_GENERATE_ACTIVATION_CONTEXT_FLAG_SYSTEM_DEFAULT_TEXTUAL_ASSEMBLY_IDENTITY;
|
|
|
|
IFW32FALSE_EXIT(SxspIsPrivateProfileStringEqual(CSRSS_INI_SECTION_TITLE, CSRSS_INI_KEY_TEXTUALIDENTFLAG, L"yes", fDumpBool, pcwszSettingsFile));
|
|
if ( fDumpBool )
|
|
this->Request.Flags |= SXS_GENERATE_ACTIVATION_CONTEXT_FLAG_TEXTUAL_ASSEMBLY_IDENTITY;
|
|
|
|
|
|
//
|
|
// Get the success/failure value
|
|
//
|
|
IFW32FALSE_EXIT(SxspIsPrivateProfileStringEqual(CSRSS_INI_SECTION_TITLE, CSRSS_INI_KEY_SUCCESS, L"yes", this->fShouldSucceed, pcwszSettingsFile));
|
|
|
|
//
|
|
// And how long this is to sleep
|
|
//
|
|
INT dump;
|
|
IFW32FALSE_EXIT(SxspGetPrivateProfileIntW(CSRSS_INI_SECTION_TITLE, CSRSS_INI_KEY_SLEEP, 200, dump, pcwszSettingsFile));
|
|
this->dwSleepTime = dump;
|
|
|
|
//
|
|
// PA setting is a string
|
|
//
|
|
IFW32FALSE_EXIT(SxspGetPrivateProfileStringW(CSRSS_INI_SECTION_TITLE, CSRSS_INI_KEY_PROC_ARCH, L"x86", buffJunk, pcwszSettingsFile));
|
|
if ( buffJunk.Cch() != 0 )
|
|
{
|
|
bool fValid = false;
|
|
IFW32FALSE_EXIT(FusionpParseProcessorArchitecture(
|
|
buffJunk,
|
|
buffJunk.Cch(),
|
|
&this->Request.ProcessorArchitecture,
|
|
fValid));
|
|
if ( !fValid ) this->Request.ProcessorArchitecture = PROCESSOR_ARCHITECTURE_INTEL;
|
|
}
|
|
else
|
|
{
|
|
this->Request.ProcessorArchitecture = PROCESSOR_ARCHITECTURE_INTEL;
|
|
}
|
|
|
|
//
|
|
// Maybe this is a string like en-us, or maybe just a number.
|
|
//
|
|
IFW32FALSE_EXIT(SxspGetPrivateProfileStringW(CSRSS_INI_SECTION_TITLE, CSRSS_INI_KEY_LANGID, L"", buffJunk, pcwszSettingsFile));
|
|
if ( buffJunk.Cch() != 0 )
|
|
{
|
|
ULONG ulTemp;
|
|
if ( !ParseDecimalOrHexString(buffJunk, buffJunk.Cch(), ulTemp) )
|
|
{
|
|
BOOL fFound = FALSE;
|
|
|
|
IFW32FALSE_EXIT(SxspMapCultureToLANGID(buffJunk, lidCurrentLang, &fFound));
|
|
if ( !fFound )
|
|
{
|
|
goto Exit;
|
|
}
|
|
}
|
|
else lidCurrentLang = static_cast<LANGID>(ulTemp);
|
|
}
|
|
this->Request.LangId = lidCurrentLang;
|
|
|
|
//
|
|
// Assembly root directory. Not really required to be present?
|
|
//
|
|
IFW32FALSE_EXIT(SxspGetAssemblyRootDirectory(buffJunk));
|
|
IFW32FALSE_EXIT(SxspGetPrivateProfileStringW(CSRSS_INI_SECTION_TITLE, CSRSS_INI_KEY_ASMDIR, buffJunk, this->buffAssemblyDirectory, pcwszSettingsFile));
|
|
this->Request.AssemblyDirectory = this->buffAssemblyDirectory;
|
|
|
|
//
|
|
// Textual identity string - if not present, null out the value
|
|
//
|
|
IFW32FALSE_EXIT(SxspGetPrivateProfileStringW(CSRSS_INI_SECTION_TITLE, CSRSS_INI_KEY_TEXTUALIDENT, L"", this->buffTextualIdentityString, pcwszSettingsFile));
|
|
if ( this->buffTextualIdentityString.Cch() != 0 )
|
|
{
|
|
this->Request.TextualAssemblyIdentity = this->buffTextualIdentityString;
|
|
}
|
|
|
|
//
|
|
// File paths
|
|
//
|
|
IFW32FALSE_EXIT(SxspGetPrivateProfileStringW(CSRSS_INI_SECTION_TITLE, CSRSS_INI_KEY_MANIFEST, L"", buffJunk, pcwszSettingsFile));
|
|
if ( buffJunk.Cch() != 0 )
|
|
{
|
|
IFW32FALSE_EXIT(this->buffManifestStreamPath.Win32Assign(this->buffTestDirectory));
|
|
IFW32FALSE_EXIT(this->buffManifestStreamPath.Win32AppendPathElement(buffJunk));
|
|
}
|
|
|
|
IFW32FALSE_EXIT(SxspGetPrivateProfileStringW(CSRSS_INI_SECTION_TITLE, CSRSS_INI_KEY_POLICY, L"", buffJunk, pcwszSettingsFile));
|
|
if ( buffJunk.Cch() != 0 )
|
|
{
|
|
IFW32FALSE_EXIT(this->buffPolicyStreamPath.Win32Assign(this->buffTestDirectory));
|
|
IFW32FALSE_EXIT(this->buffPolicyStreamPath.Win32AppendPathElement(buffJunk));
|
|
}
|
|
|
|
FN_EPILOG
|
|
}
|
|
|
|
typedef CDeque<CCsrssPoundingThreadEntry, offsetof(CCsrssPoundingThreadEntry, Linkage)> CStressEntryDeque;
|
|
typedef CDequeIterator<CCsrssPoundingThreadEntry, offsetof(CCsrssPoundingThreadEntry, Linkage)> CStressEntryDequeIter;
|
|
|
|
CStressEntryDeque g_CsrssStressers;
|
|
|
|
BOOL InitializeCsrssStress(
|
|
PCWSTR pcwszTargetDirectory,
|
|
DWORD dwFlags
|
|
)
|
|
{
|
|
FN_PROLOG_WIN32
|
|
|
|
CFindFile Finder;
|
|
WIN32_FIND_DATAW FindData;
|
|
CStringBuffer buffTemp;
|
|
CStringBuffer buffTestActualRoot;
|
|
|
|
//
|
|
// The target directory here is the root of all the test case dirs, not the
|
|
// csrss-specific directory.
|
|
//
|
|
IFW32FALSE_EXIT(buffTestActualRoot.Win32Assign(
|
|
pcwszTargetDirectory,
|
|
wcslen(pcwszTargetDirectory)));
|
|
IFW32FALSE_EXIT(buffTestActualRoot.Win32AppendPathElement(
|
|
CSRSS_TEST_DIR_NAME,
|
|
CSRSS_TEST_DIR_NAME_CCH));
|
|
|
|
if ((FindData.dwFileAttributes = ::GetFileAttributesW(buffTestActualRoot)) == 0xffffffff
|
|
&& (FindData.dwFileAttributes = ::FusionpGetLastWin32Error()) == ERROR_FILE_NOT_FOUND)
|
|
{
|
|
printf("no %ls tests, skipping\n", CSRSS_TEST_DIR_NAME);
|
|
FN_SUCCESSFUL_EXIT();
|
|
}
|
|
|
|
IFW32FALSE_EXIT(buffTestActualRoot.Win32AppendPathElement(L"*", 1));
|
|
IFW32FALSE_EXIT(Finder.Win32FindFirstFile(buffTestActualRoot, &FindData));
|
|
IFW32FALSE_EXIT(buffTestActualRoot.Win32RemoveLastPathElement());
|
|
|
|
do
|
|
{
|
|
CStringBuffer buffSettingsFile;
|
|
CCsrssPoundingThreadEntry *TestEntry;
|
|
|
|
if (( ( FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) == 0 ) ||
|
|
FusionpIsDotOrDotDot(FindData.cFileName))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//
|
|
// Tack on the name of this test
|
|
//
|
|
IFW32NULL_EXIT(TestEntry = FUSION_NEW_SINGLETON(CCsrssPoundingThreadEntry));
|
|
IFW32FALSE_EXIT(TestEntry->buffTestName.Win32Assign(
|
|
FindData.cFileName,
|
|
wcslen(FindData.cFileName)));
|
|
IFW32FALSE_EXIT(TestEntry->buffTestDirectory.Win32Assign(buffTestActualRoot));
|
|
IFW32FALSE_EXIT(TestEntry->buffTestDirectory.Win32AppendPathElement(
|
|
FindData.cFileName,
|
|
wcslen(FindData.cFileName)));
|
|
|
|
IFW32FALSE_EXIT(buffSettingsFile.Win32Assign(TestEntry->buffTestDirectory));
|
|
IFW32FALSE_EXIT(buffSettingsFile.Win32AppendPathElement(
|
|
CSRSS_SETTINGS_FILE_NAME,
|
|
CSRSS_SETTINGS_FILE_NAME_CCH));
|
|
|
|
//
|
|
// Acquire settings for this test
|
|
//
|
|
IFW32FALSE_EXIT(TestEntry->AcquireSettingsFrom(buffSettingsFile));
|
|
g_CsrssStressers.AddToTail(TestEntry);
|
|
TestEntry = NULL;
|
|
|
|
} while (::FindNextFileW(Finder, &FindData));
|
|
|
|
FN_EPILOG
|
|
}
|
|
|
|
BOOL WaitForCsrssStressShutdown()
|
|
{
|
|
FN_PROLOG_WIN32
|
|
|
|
CStressEntryDequeIter Iter(&g_CsrssStressers);
|
|
|
|
for ( Iter.Reset(); Iter.More(); Iter.Next() )
|
|
{
|
|
CCsrssPoundingThreadEntry *Item = Iter.Current();
|
|
Item->StopAndWaitForCompletion();
|
|
Item->hOurThreadHandle.Win32Close();
|
|
}
|
|
|
|
FN_EPILOG
|
|
}
|
|
|
|
BOOL CsrssStressStartThreads( ULONG &ulThreadsCreated )
|
|
{
|
|
FN_PROLOG_WIN32
|
|
|
|
CStressEntryDequeIter Iter(&g_CsrssStressers);
|
|
|
|
ulThreadsCreated = 0;
|
|
|
|
for ( Iter.Reset(); Iter.More(); Iter.Next() )
|
|
{
|
|
CCsrssPoundingThreadEntry *Item = Iter.Current();
|
|
|
|
IFW32FALSE_EXIT(Item->hOurThreadHandle.Win32CreateThread(
|
|
Item->ThreadProcEntry,
|
|
Item));
|
|
ulThreadsCreated++;
|
|
}
|
|
|
|
FN_EPILOG
|
|
}
|
|
|
|
BOOL CleanupCsrssTests()
|
|
{
|
|
FN_PROLOG_WIN32
|
|
|
|
g_CsrssStressers.ClearAndDeleteAll();
|
|
|
|
FN_EPILOG
|
|
}
|
|
|
|
RequestCsrssStressShutdown()
|
|
{
|
|
FN_PROLOG_WIN32
|
|
|
|
CStressEntryDequeIter Iter(&g_CsrssStressers);
|
|
|
|
for ( Iter.Reset(); Iter.More(); Iter.Next() )
|
|
{
|
|
CCsrssPoundingThreadEntry *Item = Iter.Current();
|
|
Item->fStopNextRound = true;
|
|
}
|
|
|
|
FN_EPILOG
|
|
}
|