ProcessEnumerator tracks processes properly now.
parent
bd4456b5f6
commit
ab40868b29
@ -1,135 +0,0 @@
|
||||
/*
|
||||
www.sourceforge.net/projects/dfhack
|
||||
Copyright (c) 2009 Petr Mrázek (peterix), Kenneth Ferland (Impaler[WrG]), dorf
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any
|
||||
damages arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any
|
||||
purpose, including commercial applications, and to alter it and
|
||||
redistribute it freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must
|
||||
not claim that you wrote the original software. If you use this
|
||||
software in a product, an acknowledgment in the product documentation
|
||||
would be appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and
|
||||
must not be misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#include "Internal.h"
|
||||
#include "dfhack/DFProcessEnumerator.h"
|
||||
#include "dfhack/DFProcess.h"
|
||||
#include "dfhack/DFMemInfo.h"
|
||||
#include "dfhack/DFMemInfoManager.h"
|
||||
#include <sys/shm.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/ipc.h>
|
||||
#include <time.h>
|
||||
#include "shms.h"
|
||||
|
||||
using namespace DFHack;
|
||||
|
||||
class DFHack::ProcessEnumerator::Private
|
||||
{
|
||||
public:
|
||||
Private(){};
|
||||
MemInfoManager *meminfo;
|
||||
std::vector<Process *> processes;
|
||||
};
|
||||
|
||||
bool ProcessEnumerator::findProcessess()
|
||||
{
|
||||
DIR *dir_p;
|
||||
struct dirent *dir_entry_p;
|
||||
// Open /proc/ directory
|
||||
dir_p = opendir("/proc/");
|
||||
// Reading /proc/ entries
|
||||
while(NULL != (dir_entry_p = readdir(dir_p)))
|
||||
{
|
||||
// Only PID folders (numbers)
|
||||
if (strspn(dir_entry_p->d_name, "0123456789") != strlen(dir_entry_p->d_name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Process *p1 = new SHMProcess(atoi(dir_entry_p->d_name),d->meminfo->meminfo);
|
||||
if(p1->isIdentified())
|
||||
{
|
||||
d->processes.push_back(p1);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
delete p1;
|
||||
}
|
||||
Process *p2 = new NormalProcess(atoi(dir_entry_p->d_name),d->meminfo->meminfo);
|
||||
if(p2->isIdentified())
|
||||
{
|
||||
d->processes.push_back(p2);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
delete p2;
|
||||
}
|
||||
Process *p3 = new WineProcess(atoi(dir_entry_p->d_name),d->meminfo->meminfo);
|
||||
if(p3->isIdentified())
|
||||
{
|
||||
d->processes.push_back(p3);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
delete p3;
|
||||
}
|
||||
|
||||
}
|
||||
closedir(dir_p);
|
||||
// return value depends on if we found some DF processes
|
||||
if(d->processes.size())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t ProcessEnumerator::size()
|
||||
{
|
||||
return d->processes.size();
|
||||
}
|
||||
|
||||
|
||||
Process * ProcessEnumerator::operator[](uint32_t index)
|
||||
{
|
||||
assert(index < d->processes.size());
|
||||
return d->processes[index];
|
||||
}
|
||||
|
||||
|
||||
ProcessEnumerator::ProcessEnumerator( string path_to_xml )
|
||||
: d(new Private())
|
||||
{
|
||||
d->meminfo = new MemInfoManager(path_to_xml);
|
||||
}
|
||||
|
||||
void ProcessEnumerator::purge()
|
||||
{
|
||||
for(uint32_t i = 0;i < d->processes.size();i++)
|
||||
{
|
||||
delete d->processes[i];
|
||||
}
|
||||
d->processes.clear();
|
||||
}
|
||||
|
||||
ProcessEnumerator::~ProcessEnumerator()
|
||||
{
|
||||
// delete all processes
|
||||
purge();
|
||||
delete d->meminfo;
|
||||
delete d;
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
/*
|
||||
www.sourceforge.net/projects/dfhack
|
||||
Copyright (c) 2009 Petr Mrázek (peterix), Kenneth Ferland (Impaler[WrG]), dorf
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any
|
||||
damages arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any
|
||||
purpose, including commercial applications, and to alter it and
|
||||
redistribute it freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must
|
||||
not claim that you wrote the original software. If you use this
|
||||
software in a product, an acknowledgment in the product documentation
|
||||
would be appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and
|
||||
must not be misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#include "Internal.h"
|
||||
#include "dfhack/DFProcessEnumerator.h"
|
||||
#include "dfhack/DFProcess.h"
|
||||
#include "dfhack/DFMemInfo.h"
|
||||
#include "dfhack/DFMemInfoManager.h"
|
||||
using namespace DFHack;
|
||||
|
||||
class DFHack::ProcessEnumerator::Private
|
||||
{
|
||||
public:
|
||||
Private(){};
|
||||
MemInfoManager *meminfo;
|
||||
std::vector<Process *> processes;
|
||||
};
|
||||
|
||||
// some magic - will come in handy when we start doing debugger stuff on Windows
|
||||
bool EnableDebugPriv()
|
||||
{
|
||||
bool bRET = FALSE;
|
||||
TOKEN_PRIVILEGES tp;
|
||||
HANDLE hToken;
|
||||
|
||||
if (LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid))
|
||||
{
|
||||
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
|
||||
{
|
||||
if (hToken != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
||||
tp.PrivilegeCount = 1;
|
||||
if (AdjustTokenPrivileges(hToken, FALSE, &tp, 0, 0, 0))
|
||||
{
|
||||
bRET = TRUE;
|
||||
}
|
||||
CloseHandle(hToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bRET;
|
||||
}
|
||||
|
||||
// WINDOWS version of the process finder
|
||||
bool ProcessEnumerator::findProcessess()
|
||||
{
|
||||
// Get the list of process identifiers.
|
||||
DWORD ProcArray[2048], memoryNeeded, numProccesses;
|
||||
//EnableDebugPriv();
|
||||
if ( !EnumProcesses( ProcArray, sizeof(ProcArray), &memoryNeeded ) )
|
||||
{
|
||||
cout << "EnumProcesses fail'd" << endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate how many process identifiers were returned.
|
||||
numProccesses = memoryNeeded / sizeof(DWORD);
|
||||
EnableDebugPriv();
|
||||
|
||||
// iterate through processes
|
||||
for ( int i = 0; i < (int)numProccesses; i++ )
|
||||
{
|
||||
Process *p = new SHMProcess(ProcArray[i],d->meminfo->meminfo);
|
||||
if(p->isIdentified())
|
||||
{
|
||||
d->processes.push_back(p);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
delete p;
|
||||
p = 0;
|
||||
}
|
||||
p = new NormalProcess(ProcArray[i],d->meminfo->meminfo);
|
||||
if(p->isIdentified())
|
||||
{
|
||||
d->processes.push_back(p);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
delete p;
|
||||
p = 0;
|
||||
}
|
||||
}
|
||||
if(d->processes.size())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t ProcessEnumerator::size()
|
||||
{
|
||||
return d->processes.size();
|
||||
};
|
||||
|
||||
|
||||
Process * ProcessEnumerator::operator[](uint32_t index)
|
||||
{
|
||||
assert(index < d->processes.size());
|
||||
return d->processes[index];
|
||||
};
|
||||
|
||||
|
||||
ProcessEnumerator::ProcessEnumerator( string path_to_xml )
|
||||
: d(new Private())
|
||||
{
|
||||
d->meminfo = new MemInfoManager(path_to_xml);
|
||||
}
|
||||
|
||||
void ProcessEnumerator::purge()
|
||||
{
|
||||
for(uint32_t i = 0;i < d->processes.size();i++)
|
||||
{
|
||||
delete d->processes[i];
|
||||
}
|
||||
d->processes.clear();
|
||||
}
|
||||
|
||||
ProcessEnumerator::~ProcessEnumerator()
|
||||
{
|
||||
// delete all processes
|
||||
purge();
|
||||
delete d->meminfo;
|
||||
delete d;
|
||||
}
|
@ -0,0 +1,318 @@
|
||||
/*
|
||||
www.sourceforge.net/projects/dfhack
|
||||
Copyright (c) 2009 Petr Mrázek (peterix), Kenneth Ferland (Impaler[WrG]), dorf
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any
|
||||
damages arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any
|
||||
purpose, including commercial applications, and to alter it and
|
||||
redistribute it freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must
|
||||
not claim that you wrote the original software. If you use this
|
||||
software in a product, an acknowledgment in the product documentation
|
||||
would be appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and
|
||||
must not be misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#include "Internal.h"
|
||||
#include "dfhack/DFProcessEnumerator.h"
|
||||
#include "dfhack/DFProcess.h"
|
||||
#include "dfhack/DFMemInfo.h"
|
||||
#include "dfhack/DFMemInfoManager.h"
|
||||
|
||||
using namespace DFHack;
|
||||
|
||||
typedef std::vector<Process *> PROC_V;
|
||||
typedef std::map<ProcessID, Process*> PID2PROC;
|
||||
|
||||
class DFHack::ProcessEnumerator::Private
|
||||
{
|
||||
public:
|
||||
Private(){};
|
||||
MemInfoManager *meminfo;
|
||||
PROC_V Processes;
|
||||
PID2PROC ProcMap;
|
||||
Process *GetProcessObject(ProcessID ID);
|
||||
void EnumPIDs (vector <ProcessID> &PIDs);
|
||||
};
|
||||
|
||||
#ifdef LINUX_BUILD
|
||||
//FIXME: wasteful
|
||||
Process *ProcessEnumerator::Private::GetProcessObject(ProcessID ID)
|
||||
{
|
||||
|
||||
Process *p1 = new SHMProcess(ID.pid,meminfo->meminfo);
|
||||
if(p1->isIdentified())
|
||||
return p1;
|
||||
else
|
||||
delete p1;
|
||||
|
||||
Process *p2 = new NormalProcess(ID.pid,meminfo->meminfo);
|
||||
if(p2->isIdentified())
|
||||
return p2;
|
||||
else
|
||||
delete p2;
|
||||
|
||||
Process *p3 = new WineProcess(ID.pid,meminfo->meminfo);
|
||||
if(p3->isIdentified())
|
||||
return p3;
|
||||
else
|
||||
delete p3;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ProcessEnumerator::Private::EnumPIDs (vector <ProcessID> &PIDs)
|
||||
{
|
||||
DIR *dir_p;
|
||||
struct dirent *dir_entry_p;
|
||||
struct stat st;
|
||||
char fullname[512];
|
||||
fullname[0] = 0;
|
||||
PIDs.clear(); // make sure the vector is clear
|
||||
|
||||
// Open /proc/ directory
|
||||
dir_p = opendir("/proc/");
|
||||
// Reading /proc/ entries
|
||||
while(NULL != (dir_entry_p = readdir(dir_p)))
|
||||
{
|
||||
// Only PID folders (numbers)
|
||||
if (strspn(dir_entry_p->d_name, "0123456789") != strlen(dir_entry_p->d_name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
sprintf(fullname, "/proc/%s", dir_entry_p->d_name);
|
||||
int ierr = stat (fullname, &st);
|
||||
if (ierr != 0)
|
||||
{
|
||||
printf("Cannot stat %s: ierr= %d\n", fullname, ierr);
|
||||
continue;
|
||||
}
|
||||
uint64_t Pnum = atoi(dir_entry_p->d_name);
|
||||
uint64_t ctime = st.st_ctime;
|
||||
PIDs.push_back(ProcessID(ctime,Pnum));
|
||||
}
|
||||
closedir(dir_p);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef LINUX_BUILD
|
||||
Process *ProcessEnumerator::Private::GetProcessObject(ProcessID ID)
|
||||
{
|
||||
|
||||
Process *p1 = new SHMProcess(ID.pid,meminfo->meminfo);
|
||||
if(p1->isIdentified())
|
||||
return p1;
|
||||
else
|
||||
delete p1;
|
||||
|
||||
Process *p2 = new NormalProcess(ID.pid,meminfo->meminfo);
|
||||
if(p2->isIdentified())
|
||||
return p2;
|
||||
else
|
||||
delete p2;
|
||||
return 0;
|
||||
}
|
||||
// some magic - will come in handy when we start doing debugger stuff on Windows
|
||||
bool EnableDebugPriv()
|
||||
{
|
||||
bool bRET = FALSE;
|
||||
TOKEN_PRIVILEGES tp;
|
||||
HANDLE hToken;
|
||||
|
||||
if (LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid))
|
||||
{
|
||||
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
|
||||
{
|
||||
if (hToken != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
||||
tp.PrivilegeCount = 1;
|
||||
if (AdjustTokenPrivileges(hToken, FALSE, &tp, 0, 0, 0))
|
||||
{
|
||||
bRET = TRUE;
|
||||
}
|
||||
CloseHandle(hToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bRET;
|
||||
}
|
||||
|
||||
// Convert Windows FileTime structs to POSIX timestamp
|
||||
// from http://frenk.wordpress.com/2009/12/14/convert-filetime-to-unix-timestamp/
|
||||
uint64_t FileTime_to_POSIX(FILETIME ft)
|
||||
{
|
||||
// takes the last modified date
|
||||
LARGE_INTEGER date, adjust;
|
||||
date.HighPart = ft.dwHighDateTime;
|
||||
date.LowPart = ft.dwLowDateTime;
|
||||
|
||||
// 100-nanoseconds = milliseconds * 10000
|
||||
adjust.QuadPart = 11644473600000 * 10000;
|
||||
|
||||
// removes the diff between 1970 and 1601
|
||||
date.QuadPart -= adjust.QuadPart;
|
||||
|
||||
// converts back from 100-nanoseconds to seconds
|
||||
return date.QuadPart / 10000000;
|
||||
}
|
||||
|
||||
void ProcessEnumerator::Private::EnumPIDs (vector <ProcessID> &PIDs)
|
||||
{
|
||||
FILETIME ftCreate, ftExit, ftKernel, ftUser;
|
||||
|
||||
PIDs.clear(); // make sure the vector is clear
|
||||
|
||||
// Get the list of process identifiers.
|
||||
DWORD ProcArray[2048], memoryNeeded, numProccesses;
|
||||
//EnableDebugPriv();
|
||||
if ( !EnumProcesses( ProcArray, sizeof(ProcArray), &memoryNeeded ) )
|
||||
{
|
||||
cout << "EnumProcesses fail'd" << endl;
|
||||
return;
|
||||
}
|
||||
// Calculate how many process identifiers were returned.
|
||||
numProccesses = memoryNeeded / sizeof(DWORD);
|
||||
EnableDebugPriv();
|
||||
// iterate through processes
|
||||
for ( int i = 0; i < (int)numProccesses; i++ )
|
||||
{
|
||||
HANDLE proc = OpenProcess (PROCESS_QUERY_INFORMATION, false, ProcArray[i]);
|
||||
if(!proc)
|
||||
continue;
|
||||
if(GetProcessTimes(proc, &ftCreate, &ftExit, &ftKernel, &ftUser))
|
||||
{
|
||||
uint64_t ctime = FileTime_to_POSIX(ftCreate);
|
||||
uint64_t Pnum = ProcArray[i];
|
||||
PIDs.push_back(ProcessID(ctime,Pnum));
|
||||
}
|
||||
CloseHandle(proc);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool ProcessEnumerator::Refresh( PROC_V * invalidated_processes)
|
||||
{
|
||||
// PIDs to process
|
||||
vector <ProcessID> PIDs;
|
||||
// this will be the new process map
|
||||
PID2PROC temporary;
|
||||
// clear the vector other access
|
||||
d->Processes.clear();
|
||||
if(invalidated_processes)
|
||||
invalidated_processes->clear();
|
||||
|
||||
d->EnumPIDs(PIDs);
|
||||
|
||||
for(uint64_t i = 0; i < PIDs.size();i++)
|
||||
{
|
||||
ProcessID & PID = PIDs[i];
|
||||
// check if we know about the OS process already
|
||||
PID2PROC::iterator found= d->ProcMap.find(PID);
|
||||
if( found != d->ProcMap.end())
|
||||
{
|
||||
// we do
|
||||
// check if it does have a DFHack Process object associated with it
|
||||
Process * p = (*found).second;
|
||||
if(p)
|
||||
{
|
||||
// add it back to the vector we export
|
||||
d->Processes.push_back(p);
|
||||
}
|
||||
// remove the OS Process from ProcMap
|
||||
d->ProcMap.erase(found);
|
||||
// add the OS Process to what will be the new ProcMap
|
||||
temporary[PID] = p;
|
||||
}
|
||||
else
|
||||
{
|
||||
// an OS process we don't know yet!
|
||||
// try to make a DFHack Process object for it
|
||||
if(Process*p = d->GetProcessObject(PID))
|
||||
{
|
||||
// allright. this is something that can be used
|
||||
d->Processes.push_back(p);
|
||||
temporary[PID] = p;
|
||||
}
|
||||
else
|
||||
{
|
||||
// just a process. we track it anyway. Why not.
|
||||
temporary[PID] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// now the vector we export is filled again and a temporary map with valid processes is created.
|
||||
// we iterate over the old Process map and destroy all the processes that are dead.
|
||||
for(PID2PROC::const_iterator idx = d->ProcMap.begin(); idx != d->ProcMap.end();++idx)
|
||||
{
|
||||
Process * p = (*idx).second;
|
||||
if(p)
|
||||
{
|
||||
if(invalidated_processes)
|
||||
{
|
||||
invalidated_processes->push_back(p);
|
||||
}
|
||||
else
|
||||
{
|
||||
delete p;
|
||||
}
|
||||
}
|
||||
}
|
||||
d->ProcMap.swap(temporary);
|
||||
// return value depends on if we found some DF processes
|
||||
if(d->Processes.size())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ProcessEnumerator::findProcessess()
|
||||
{
|
||||
return Refresh();
|
||||
}
|
||||
|
||||
uint32_t ProcessEnumerator::size()
|
||||
{
|
||||
return d->Processes.size();
|
||||
}
|
||||
|
||||
|
||||
Process * ProcessEnumerator::operator[](uint32_t index)
|
||||
{
|
||||
assert(index < d->Processes.size());
|
||||
return d->Processes[index];
|
||||
}
|
||||
|
||||
|
||||
ProcessEnumerator::ProcessEnumerator( string path_to_xml )
|
||||
: d(new Private())
|
||||
{
|
||||
d->meminfo = new MemInfoManager(path_to_xml);
|
||||
}
|
||||
|
||||
void ProcessEnumerator::purge()
|
||||
{
|
||||
for(uint32_t i = 0;i < d->Processes.size();i++)
|
||||
{
|
||||
delete d->Processes[i];
|
||||
}
|
||||
d->ProcMap.clear();
|
||||
d->Processes.clear();
|
||||
}
|
||||
|
||||
ProcessEnumerator::~ProcessEnumerator()
|
||||
{
|
||||
// delete all processes
|
||||
purge();
|
||||
delete d->meminfo;
|
||||
delete d;
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
// Demonstrates the use of ProcessEnumerator
|
||||
// queries the Enumerator for all DF Processes on user input. Prints them to the terminal.
|
||||
// also tracks processes that were invalidated
|
||||
|
||||
#include <iostream>
|
||||
#include <climits>
|
||||
#include <vector>
|
||||
#include <ctime>
|
||||
using namespace std;
|
||||
|
||||
#include <DFHack.h>
|
||||
#include <dfhack/DFProcessEnumerator.h>
|
||||
using namespace DFHack;
|
||||
#ifndef LINUX_BUILD
|
||||
#endif
|
||||
int main (void)
|
||||
{
|
||||
vector<Process*> inval;
|
||||
ProcessEnumerator Penum("Memory.xml");
|
||||
memory_info * mem;
|
||||
for(int cnt = 0; cnt < 100; cnt++)
|
||||
{
|
||||
// make the ProcessEnumerator update its list of Processes
|
||||
// by passing the pointer to 'inval', we make it export expired
|
||||
// processes instead of destroying them outright
|
||||
// (processes expire when the OS kills them for whatever reason)
|
||||
Penum.Refresh(&inval);
|
||||
int nProc = Penum.size();
|
||||
int nInval = inval.size();
|
||||
|
||||
cout << "Processes:" << endl;
|
||||
for(int i = 0; i < nProc; i++)
|
||||
{
|
||||
mem = Penum[i]->getDescriptor();
|
||||
cout << "DF instance: " << Penum[i]->getPID()
|
||||
<< ", " << mem->getVersion() << endl;
|
||||
}
|
||||
|
||||
cout << "Invalidated:" << endl;
|
||||
for(int i = 0; i < nInval; i++)
|
||||
{
|
||||
mem = inval[i]->getDescriptor();
|
||||
cout << "DF instance: " << inval[i]->getPID()
|
||||
<< ", " << mem->getVersion() << endl;
|
||||
// we own the expired process, we must take care of freeing its resources
|
||||
delete inval[i];
|
||||
}
|
||||
cout << "<-* Press Enter to refresh *->" << endl << endl;
|
||||
cin.ignore();
|
||||
}
|
||||
|
||||
#ifndef LINUX_BUILD
|
||||
cout << "Done. Press any key to continue" << endl;
|
||||
cin.ignore();
|
||||
#endif
|
||||
return 0;
|
||||
}
|
Loading…
Reference in New Issue