show wxMutexGuiEnter/Leave by drawing into a bitmap from a secondary thread; show the result while it's being created by a MyImageDialog dialog; small cleanup
git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@59059 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
This commit is contained in:
@@ -59,7 +59,10 @@
|
|||||||
class MyThread;
|
class MyThread;
|
||||||
WX_DEFINE_ARRAY_PTR(wxThread *, wxArrayThread);
|
WX_DEFINE_ARRAY_PTR(wxThread *, wxArrayThread);
|
||||||
|
|
||||||
// Define a new application type
|
// ----------------------------------------------------------------------------
|
||||||
|
// the application object
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
class MyApp : public wxApp
|
class MyApp : public wxApp
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -82,7 +85,10 @@ public:
|
|||||||
bool m_shuttingDown;
|
bool m_shuttingDown;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Define a new frame type
|
// ----------------------------------------------------------------------------
|
||||||
|
// the main application frame
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
class MyFrame: public wxFrame
|
class MyFrame: public wxFrame
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -119,8 +125,8 @@ private:
|
|||||||
void OnResumeThread(wxCommandEvent& event);
|
void OnResumeThread(wxCommandEvent& event);
|
||||||
|
|
||||||
void OnStartWorker(wxCommandEvent& event);
|
void OnStartWorker(wxCommandEvent& event);
|
||||||
|
|
||||||
void OnExecMain(wxCommandEvent& event);
|
void OnExecMain(wxCommandEvent& event);
|
||||||
|
void OnStartGUIThread(wxCommandEvent& event);
|
||||||
|
|
||||||
void OnShowCPUs(wxCommandEvent& event);
|
void OnShowCPUs(wxCommandEvent& event);
|
||||||
void OnAbout(wxCommandEvent& event);
|
void OnAbout(wxCommandEvent& event);
|
||||||
@@ -163,9 +169,7 @@ private:
|
|||||||
|
|
||||||
// was the worker thread cancelled by user?
|
// was the worker thread cancelled by user?
|
||||||
bool m_cancelled;
|
bool m_cancelled;
|
||||||
|
wxCriticalSection m_csCancelled; // protects m_cancelled
|
||||||
// protects m_cancelled
|
|
||||||
wxCriticalSection m_critsectWork;
|
|
||||||
|
|
||||||
DECLARE_EVENT_TABLE()
|
DECLARE_EVENT_TABLE()
|
||||||
};
|
};
|
||||||
@@ -186,18 +190,19 @@ enum
|
|||||||
THREAD_STOP_THREAD,
|
THREAD_STOP_THREAD,
|
||||||
THREAD_PAUSE_THREAD,
|
THREAD_PAUSE_THREAD,
|
||||||
THREAD_RESUME_THREAD,
|
THREAD_RESUME_THREAD,
|
||||||
THREAD_START_WORKER,
|
|
||||||
|
|
||||||
|
THREAD_START_WORKER,
|
||||||
THREAD_EXEC_MAIN,
|
THREAD_EXEC_MAIN,
|
||||||
THREAD_EXEC_THREAD,
|
THREAD_START_GUI_THREAD,
|
||||||
|
|
||||||
THREAD_SHOWCPUS,
|
THREAD_SHOWCPUS,
|
||||||
|
|
||||||
WORKER_EVENT = wxID_HIGHEST+1 // this one gets sent from the worker thread
|
WORKER_EVENT = wxID_HIGHEST+1, // this one gets sent from MyWorkerThread
|
||||||
|
GUITHREAD_EVENT // this one gets sent from MyGUIThread
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// GUI thread
|
// a simple thread
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
class MyThread : public wxThread
|
class MyThread : public wxThread
|
||||||
@@ -221,7 +226,7 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// worker thread
|
// a worker thread
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
class MyWorkerThread : public wxThread
|
class MyWorkerThread : public wxThread
|
||||||
@@ -241,6 +246,53 @@ public:
|
|||||||
unsigned m_count;
|
unsigned m_count;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// a thread which executes GUI calls using wxMutexGuiEnter/Leave
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#define GUITHREAD_BMP_SIZE 300
|
||||||
|
#define GUITHREAD_NUM_UPDATES 50
|
||||||
|
class MyImageDialog;
|
||||||
|
|
||||||
|
class MyGUIThread : public wxThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MyGUIThread(MyImageDialog *dlg) : wxThread(wxTHREAD_JOINABLE)
|
||||||
|
{
|
||||||
|
m_dlg = dlg;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ExitCode Entry();
|
||||||
|
|
||||||
|
private:
|
||||||
|
MyImageDialog *m_dlg;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// an helper dialog used by MyFrame::OnStartGUIThread
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class MyImageDialog: public wxDialog
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// ctor
|
||||||
|
MyImageDialog(wxFrame *frame);
|
||||||
|
~MyImageDialog();
|
||||||
|
|
||||||
|
// stuff used by MyGUIThread:
|
||||||
|
wxBitmap m_bmp; // the bitmap drawn by MyGUIThread
|
||||||
|
wxCriticalSection m_csBmp; // protects m_bmp
|
||||||
|
|
||||||
|
private:
|
||||||
|
void OnGUIThreadEvent(wxThreadEvent& event);
|
||||||
|
void OnPaint(wxPaintEvent&);
|
||||||
|
|
||||||
|
MyGUIThread m_thread;
|
||||||
|
int m_nCurrentProgress;
|
||||||
|
|
||||||
|
DECLARE_EVENT_TABLE()
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// implementation
|
// implementation
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -290,8 +342,10 @@ BEGIN_EVENT_TABLE(MyFrame, wxFrame)
|
|||||||
EVT_MENU(THREAD_STOP_THREAD, MyFrame::OnStopThread)
|
EVT_MENU(THREAD_STOP_THREAD, MyFrame::OnStopThread)
|
||||||
EVT_MENU(THREAD_PAUSE_THREAD, MyFrame::OnPauseThread)
|
EVT_MENU(THREAD_PAUSE_THREAD, MyFrame::OnPauseThread)
|
||||||
EVT_MENU(THREAD_RESUME_THREAD, MyFrame::OnResumeThread)
|
EVT_MENU(THREAD_RESUME_THREAD, MyFrame::OnResumeThread)
|
||||||
|
|
||||||
EVT_MENU(THREAD_START_WORKER, MyFrame::OnStartWorker)
|
EVT_MENU(THREAD_START_WORKER, MyFrame::OnStartWorker)
|
||||||
EVT_MENU(THREAD_EXEC_MAIN, MyFrame::OnExecMain)
|
EVT_MENU(THREAD_EXEC_MAIN, MyFrame::OnExecMain)
|
||||||
|
EVT_MENU(THREAD_START_GUI_THREAD, MyFrame::OnStartGUIThread)
|
||||||
|
|
||||||
EVT_MENU(THREAD_SHOWCPUS, MyFrame::OnShowCPUs)
|
EVT_MENU(THREAD_SHOWCPUS, MyFrame::OnShowCPUs)
|
||||||
EVT_MENU(THREAD_ABOUT, MyFrame::OnAbout)
|
EVT_MENU(THREAD_ABOUT, MyFrame::OnAbout)
|
||||||
@@ -327,6 +381,7 @@ MyFrame::MyFrame(wxFrame *frame, const wxString& title,
|
|||||||
menuThread->AppendSeparator();
|
menuThread->AppendSeparator();
|
||||||
menuThread->Append(THREAD_START_WORKER, _T("Start a &worker thread\tCtrl-W"));
|
menuThread->Append(THREAD_START_WORKER, _T("Start a &worker thread\tCtrl-W"));
|
||||||
menuThread->Append(THREAD_EXEC_MAIN, _T("&Launch a program from main thread\tF5"));
|
menuThread->Append(THREAD_EXEC_MAIN, _T("&Launch a program from main thread\tF5"));
|
||||||
|
menuThread->Append(THREAD_START_GUI_THREAD, _T("Launch a &GUI thread\tF6"));
|
||||||
menuBar->Append(menuThread, _T("&Thread"));
|
menuBar->Append(menuThread, _T("&Thread"));
|
||||||
|
|
||||||
wxMenu *menuHelp = new wxMenu;
|
wxMenu *menuHelp = new wxMenu;
|
||||||
@@ -392,6 +447,53 @@ MyThread *MyFrame::CreateThread()
|
|||||||
return thread;
|
return thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MyFrame::DoLogThreadMessages()
|
||||||
|
{
|
||||||
|
wxCriticalSectionLocker lock(m_csMessages);
|
||||||
|
|
||||||
|
const size_t count = m_messages.size();
|
||||||
|
for ( size_t n = 0; n < count; n++ )
|
||||||
|
{
|
||||||
|
m_txtctrl->AppendText(m_messages[n]);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_messages.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyFrame::UpdateThreadStatus()
|
||||||
|
{
|
||||||
|
wxCriticalSectionLocker enter(wxGetApp().m_critsect);
|
||||||
|
|
||||||
|
// update the counts of running/total threads
|
||||||
|
size_t nRunning = 0,
|
||||||
|
nCount = wxGetApp().m_threads.Count();
|
||||||
|
for ( size_t n = 0; n < nCount; n++ )
|
||||||
|
{
|
||||||
|
if ( wxGetApp().m_threads[n]->IsRunning() )
|
||||||
|
nRunning++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( nCount != m_nCount || nRunning != m_nRunning )
|
||||||
|
{
|
||||||
|
m_nRunning = nRunning;
|
||||||
|
m_nCount = nCount;
|
||||||
|
|
||||||
|
wxLogStatus(this, wxT("%u threads total, %u running."), unsigned(nCount), unsigned(nRunning));
|
||||||
|
}
|
||||||
|
//else: avoid flicker - don't print anything
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MyFrame::Cancelled()
|
||||||
|
{
|
||||||
|
wxCriticalSectionLocker lock(m_csCancelled);
|
||||||
|
|
||||||
|
return m_cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// MyFrame - event handlers
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
void MyFrame::OnStartThreads(wxCommandEvent& WXUNUSED(event) )
|
void MyFrame::OnStartThreads(wxCommandEvent& WXUNUSED(event) )
|
||||||
{
|
{
|
||||||
static long s_num;
|
static long s_num;
|
||||||
@@ -528,42 +630,6 @@ void MyFrame::OnIdle(wxIdleEvent& event)
|
|||||||
event.Skip();
|
event.Skip();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyFrame::DoLogThreadMessages()
|
|
||||||
{
|
|
||||||
wxCriticalSectionLocker lock(m_csMessages);
|
|
||||||
|
|
||||||
const size_t count = m_messages.size();
|
|
||||||
for ( size_t n = 0; n < count; n++ )
|
|
||||||
{
|
|
||||||
m_txtctrl->AppendText(m_messages[n]);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_messages.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MyFrame::UpdateThreadStatus()
|
|
||||||
{
|
|
||||||
wxCriticalSectionLocker enter(wxGetApp().m_critsect);
|
|
||||||
|
|
||||||
// update the counts of running/total threads
|
|
||||||
size_t nRunning = 0,
|
|
||||||
nCount = wxGetApp().m_threads.Count();
|
|
||||||
for ( size_t n = 0; n < nCount; n++ )
|
|
||||||
{
|
|
||||||
if ( wxGetApp().m_threads[n]->IsRunning() )
|
|
||||||
nRunning++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( nCount != m_nCount || nRunning != m_nRunning )
|
|
||||||
{
|
|
||||||
m_nRunning = nRunning;
|
|
||||||
m_nCount = nCount;
|
|
||||||
|
|
||||||
wxLogStatus(this, wxT("%u threads total, %u running."), unsigned(nCount), unsigned(nRunning));
|
|
||||||
}
|
|
||||||
//else: avoid flicker - don't print anything
|
|
||||||
}
|
|
||||||
|
|
||||||
void MyFrame::OnQuit(wxCommandEvent& WXUNUSED(event) )
|
void MyFrame::OnQuit(wxCommandEvent& WXUNUSED(event) )
|
||||||
{
|
{
|
||||||
Close(true);
|
Close(true);
|
||||||
@@ -681,21 +747,99 @@ void MyFrame::OnWorkerEvent(wxThreadEvent& event)
|
|||||||
{
|
{
|
||||||
if ( !m_dlgProgress->Update(n) )
|
if ( !m_dlgProgress->Update(n) )
|
||||||
{
|
{
|
||||||
wxCriticalSectionLocker lock(m_critsectWork);
|
wxCriticalSectionLocker lock(m_csCancelled);
|
||||||
|
|
||||||
m_cancelled = true;
|
m_cancelled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MyFrame::Cancelled()
|
void MyFrame::OnStartGUIThread(wxCommandEvent& WXUNUSED(event))
|
||||||
{
|
{
|
||||||
wxCriticalSectionLocker lock(m_critsectWork);
|
MyImageDialog dlg(this);
|
||||||
|
|
||||||
return m_cancelled;
|
dlg.ShowModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// MyImageDialog
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
BEGIN_EVENT_TABLE(MyImageDialog, wxDialog)
|
||||||
|
EVT_THREAD(GUITHREAD_EVENT, MyImageDialog::OnGUIThreadEvent)
|
||||||
|
EVT_PAINT(MyImageDialog::OnPaint)
|
||||||
|
END_EVENT_TABLE()
|
||||||
|
|
||||||
|
MyImageDialog::MyImageDialog(wxFrame *parent)
|
||||||
|
: wxDialog(parent, wxID_ANY, "Image created by a secondary thread",
|
||||||
|
wxDefaultPosition, wxSize(GUITHREAD_BMP_SIZE,GUITHREAD_BMP_SIZE)*1.5, wxDEFAULT_DIALOG_STYLE),
|
||||||
|
m_thread(this)
|
||||||
|
{
|
||||||
|
m_nCurrentProgress = 0;
|
||||||
|
|
||||||
|
CentreOnScreen();
|
||||||
|
|
||||||
|
// NOTE: no need to lock m_csBmp until the thread isn't started:
|
||||||
|
|
||||||
|
// create the bitmap
|
||||||
|
if (!m_bmp.Create(GUITHREAD_BMP_SIZE,GUITHREAD_BMP_SIZE) || !m_bmp.IsOk())
|
||||||
|
{
|
||||||
|
wxLogError("Couldn't create the bitmap!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean it
|
||||||
|
wxMemoryDC dc(m_bmp);
|
||||||
|
dc.SetBackground(*wxBLACK_BRUSH);
|
||||||
|
dc.Clear();
|
||||||
|
|
||||||
|
// draw the bitmap from a secondary thread
|
||||||
|
if ( m_thread.Create() != wxTHREAD_NO_ERROR ||
|
||||||
|
m_thread.Run() != wxTHREAD_NO_ERROR )
|
||||||
|
{
|
||||||
|
wxLogError(wxT("Can't create/run thread!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MyImageDialog::~MyImageDialog()
|
||||||
|
{
|
||||||
|
// in case our thread is still running and for some reason we are destroyed,
|
||||||
|
// do wait for the thread to complete as it assumes that its MyImageDialog
|
||||||
|
// pointer is always valid
|
||||||
|
m_thread.Delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyImageDialog::OnGUIThreadEvent(wxThreadEvent& event)
|
||||||
|
{
|
||||||
|
m_nCurrentProgress = int(((float)event.GetInt()*100)/GUITHREAD_NUM_UPDATES);
|
||||||
|
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyImageDialog::OnPaint(wxPaintEvent& WXUNUSED(evt))
|
||||||
|
{
|
||||||
|
wxPaintDC dc(this);
|
||||||
|
|
||||||
|
const wxSize& sz = dc.GetSize();
|
||||||
|
|
||||||
|
{
|
||||||
|
// paint the bitmap
|
||||||
|
wxCriticalSectionLocker locker(m_csBmp);
|
||||||
|
dc.DrawBitmap(m_bmp, (sz.GetWidth()-GUITHREAD_BMP_SIZE)/2,
|
||||||
|
(sz.GetHeight()-GUITHREAD_BMP_SIZE)/2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// paint a sort of progress bar with a 10px border:
|
||||||
|
dc.SetBrush(*wxRED_BRUSH);
|
||||||
|
dc.DrawRectangle(10,10, 10+m_nCurrentProgress*(GUITHREAD_BMP_SIZE-20)/100,30);
|
||||||
|
dc.SetTextForeground(*wxBLUE);
|
||||||
|
dc.DrawText(wxString::Format("%d%%", m_nCurrentProgress),
|
||||||
|
(sz.GetWidth()-dc.GetCharWidth()*2)/2,
|
||||||
|
25-dc.GetCharHeight()/2);
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// MyThread
|
// MyThread
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
@@ -727,7 +871,7 @@ MyThread::~MyThread()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void *MyThread::Entry()
|
wxThread::ExitCode MyThread::Entry()
|
||||||
{
|
{
|
||||||
wxString text;
|
wxString text;
|
||||||
|
|
||||||
@@ -769,6 +913,12 @@ void *MyThread::Entry()
|
|||||||
// MyWorkerThread
|
// MyWorkerThread
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// define this symbol to 1 to test if the YieldFor() call in the wxProgressDialog::Update
|
||||||
|
// function provokes a race condition in which the second wxThreadEvent posted by
|
||||||
|
// MyWorkerThread::Entry is processed by the YieldFor() call of wxProgressDialog::Update
|
||||||
|
// and results in the destruction of the progress dialog itself, resulting in a crash later.
|
||||||
|
#define TEST_YIELD_RACE_CONDITION 0
|
||||||
|
|
||||||
MyWorkerThread::MyWorkerThread(MyFrame *frame)
|
MyWorkerThread::MyWorkerThread(MyFrame *frame)
|
||||||
: wxThread()
|
: wxThread()
|
||||||
{
|
{
|
||||||
@@ -780,13 +930,7 @@ void MyWorkerThread::OnExit()
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
// define this symbol to 1 to test if the YieldFor() call in the wxProgressDialog::Update
|
wxThread::ExitCode MyWorkerThread::Entry()
|
||||||
// function provokes a race condition in which the second wxThreadEvent posted by
|
|
||||||
// MyWorkerThread::Entry is processed by the YieldFor() call of wxProgressDialog::Update
|
|
||||||
// and results in the destruction of the progress dialog itself, resulting in a crash later.
|
|
||||||
#define TEST_YIELD_RACE_CONDITION 0
|
|
||||||
|
|
||||||
void *MyWorkerThread::Entry()
|
|
||||||
{
|
{
|
||||||
#if TEST_YIELD_RACE_CONDITION
|
#if TEST_YIELD_RACE_CONDITION
|
||||||
if ( TestDestroy() )
|
if ( TestDestroy() )
|
||||||
@@ -824,3 +968,43 @@ void *MyWorkerThread::Entry()
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// MyGUIThread
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
wxThread::ExitCode MyGUIThread::Entry()
|
||||||
|
{
|
||||||
|
for (int i=0; i<GUITHREAD_NUM_UPDATES && !TestDestroy(); i++)
|
||||||
|
{
|
||||||
|
// inform the GUI toolkit that we're going to use GUI functions
|
||||||
|
// from a secondary thread:
|
||||||
|
wxMutexGuiEnter();
|
||||||
|
|
||||||
|
{
|
||||||
|
wxCriticalSectionLocker lock(m_dlg->m_csBmp);
|
||||||
|
|
||||||
|
// draw some more stuff on the bitmap
|
||||||
|
wxMemoryDC dc(m_dlg->m_bmp);
|
||||||
|
dc.SetBrush((i%2)==0 ? *wxBLUE_BRUSH : *wxGREEN_BRUSH);
|
||||||
|
dc.DrawRectangle(rand()%GUITHREAD_BMP_SIZE, rand()%GUITHREAD_BMP_SIZE, 30, 30);
|
||||||
|
|
||||||
|
// simulate long drawing time:
|
||||||
|
wxMilliSleep(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we don't release the GUI mutex the MyImageDialog won't be able to refresh
|
||||||
|
wxMutexGuiLeave();
|
||||||
|
|
||||||
|
// notify the dialog that another piece of our masterpiece is complete:
|
||||||
|
wxThreadEvent event( wxEVT_COMMAND_THREAD, GUITHREAD_EVENT );
|
||||||
|
event.SetInt(i);
|
||||||
|
wxQueueEvent( m_dlg, event.Clone() );
|
||||||
|
|
||||||
|
// give the main thread the time to refresh before we lock the GUI mutex again
|
||||||
|
// FIXME: find a better way to do this!
|
||||||
|
wxMilliSleep(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ExitCode)0;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user