twirling icon for ActiveX too

This commit is contained in:
David Rose 2011-08-26 21:39:40 +00:00
parent 922bb6f48a
commit 1d60925220
10 changed files with 305 additions and 75 deletions

View File

@ -388,7 +388,7 @@ init_plugin(const string &contents_filename, const string &host_url,
// space and clears all of the pointers. // space and clears all of the pointers.
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
void void
unload_plugin() { unload_plugin(ostream &logfile) {
if (!plugin_loaded) { if (!plugin_loaded) {
return; return;
} }

View File

@ -75,7 +75,7 @@ init_plugin(const string &contents_filename, const string &host_url,
bool trusted_environment, bool console_environment, bool trusted_environment, bool console_environment,
const string &root_dir, const string &host_dir, ostream &logfile); const string &root_dir, const string &host_dir, ostream &logfile);
void unload_plugin(); void unload_plugin(ostream &logfile);
bool is_plugin_loaded(); bool is_plugin_loaded();
#endif #endif

View File

@ -163,7 +163,10 @@ CP3DActiveXCtrl::CP3DActiveXCtrl() : m_instance( *this ), m_pPandaObject( NULL )
{ {
InitializeIIDs(&IID_DP3DActiveX, &IID_DP3DActiveXEvents); InitializeIIDs(&IID_DP3DActiveX, &IID_DP3DActiveXEvents);
// TODO: Initialize your control's instance data here. // TODO: Initialize your control's instance data here.
_state = S_init;
// The init thread is initially not running.
_init_not_running.SetEvent();
} }
// CP3DActiveXCtrl::~CP3DActiveXCtrl - Destructor // CP3DActiveXCtrl::~CP3DActiveXCtrl - Destructor
@ -186,20 +189,97 @@ void CP3DActiveXCtrl::OnDraw(CDC* pdc, const CRect& rcBounds, const CRect& rcInv
if (!pdc) if (!pdc)
return; return;
if ( !m_instance.IsInit( ) ) switch (_state) {
case S_init:
{ {
Init( ); _state = S_loading;
// The first time we get the Draw message, we know we're
// sufficiently set up to start downloading the instance.
m_instance.read_tokens();
get_twirl_bitmaps();
// Start the twirl timer going.
SetTimer(1, 100, timer_callback);
// But do most of the setup in a child thread, so we don't lock
// up the browser GUI while the instance gets itself downloaded
// and such.
_init_not_running.ResetEvent(); // Now the init thread is running.
if (_beginthread(st_init, 0, this) == -1L) {
nout << "Couldn't start thread.\n";
_init_not_running.SetEvent();
_state = S_failed;
}
}
break;
case S_loading:
// Waiting for the init thread to finish.
break;
case S_ready:
// Now the instance has downloaded, so set it going.
{
_state = S_started;
KillTimer(1);
m_instance.Start( m_instance.GetP3DFilename( ) );
}
break;
case S_started:
// The instance is running, no need to draw anything.
KillTimer(1);
DoSuperclassPaint(pdc, rcBounds);
return;
case S_failed:
// Something went wrong.
KillTimer(1);
DoSuperclassPaint(pdc, rcBounds);
return;
} }
CBrush brBackGnd(TranslateColor(AmbientBackColor())); // The instance is setting itself up. In the meantime, draw the
// background and the twirling icon.
// Paint the background.
CBrush brBackGnd(RGB(m_instance._bgcolor_r, m_instance._bgcolor_g, m_instance._bgcolor_b));
pdc->FillRect(rcBounds, &brBackGnd); pdc->FillRect(rcBounds, &brBackGnd);
DoSuperclassPaint(pdc, rcBounds); // Create an in-memory DC compatible with the display DC we're
// using to paint
CDC dcMemory;
dcMemory.CreateCompatibleDC(pdc);
// Select the bitmap into the in-memory DC
DWORD now = GetTickCount();
int step = (now / 100) % twirl_num_steps;
dcMemory.SelectObject(&_twirl_bitmaps[step]);
// Find a centerpoint for the bitmap in the client area
CRect rect;
GetClientRect(&rect);
int nX = rect.left + (rect.Width() - twirl_width) / 2;
int nY = rect.top + (rect.Height() - twirl_height) / 2;
// Copy the bits from the in-memory DC into the on-screen DC to
// actually do the painting. Use the centerpoint we computed for
// the target offset.
pdc->BitBlt(nX, nY, twirl_width, twirl_height, &dcMemory,
0, 0, SRCCOPY);
} }
void CP3DActiveXCtrl::OnClose( DWORD dwSaveOption ) void CP3DActiveXCtrl::OnClose( DWORD dwSaveOption )
{ {
m_instance.Stop(); m_instance.Stop();
// Make sure the init thread has finished.
if (_state == S_loading) {
nout << "Waiting for thread stop\n" << flush;
::WaitForSingleObject( _init_not_running.m_hObject, INFINITE );
nout << "Done waiting for thread stop\n" << flush;
}
COleControl::OnClose( dwSaveOption ); COleControl::OnClose( dwSaveOption );
} }
@ -367,7 +447,17 @@ LRESULT CP3DActiveXCtrl::OnPandaNotification(WPARAM wParam, LPARAM lParam)
return 0; return 0;
} }
int CP3DActiveXCtrl::Init( ) // The static init method.
void CP3DActiveXCtrl::
st_init(void *data) {
CP3DActiveXCtrl *self = (CP3DActiveXCtrl *)data;
self->init();
self->_init_not_running.SetEvent();
}
// The init method. This is called once at startup, in a child
// thread.
int CP3DActiveXCtrl::init( )
{ {
int error( 0 ); int error( 0 );
std::string p3dDllFilename; std::string p3dDllFilename;
@ -379,9 +469,18 @@ int CP3DActiveXCtrl::Init( )
if ( !error ) if ( !error )
{ {
m_pPandaObject = new PPandaObject( this, NULL ); m_pPandaObject = new PPandaObject( this, NULL );
m_instance.Start( m_instance.GetP3DFilename( ) ); if (_state == S_loading) {
// Ready to start.
_state = S_ready;
return error;
} }
} }
}
if (_state == S_loading) {
// Something went wrong.
_state = S_failed;
}
return error; return error;
} }
@ -457,8 +556,11 @@ HRESULT CP3DActiveXCtrl::ExchangeProperties( CPropExchange* pPX )
P3D_object* CP3DActiveXCtrl::GetP3DObject( ) P3D_object* CP3DActiveXCtrl::GetP3DObject( )
{ {
if (_state == S_started) {
return m_instance.m_p3dObject; return m_instance.m_p3dObject;
} }
return NULL;
}
IOleClientSite* CP3DActiveXCtrl::GetClientSte( ) IOleClientSite* CP3DActiveXCtrl::GetClientSte( )
{ {
@ -473,3 +575,41 @@ void CP3DActiveXCtrl::OnmainChanged(void)
SetModifiedFlag(); SetModifiedFlag();
} }
void CP3DActiveXCtrl::
get_twirl_bitmaps() {
static const size_t twirl_size = twirl_width * twirl_height;
unsigned char twirl_data[twirl_size * 3];
unsigned char new_data[twirl_size * 4];
for (int step = 0; step < twirl_num_steps; ++step) {
get_twirl_data(twirl_data, twirl_size, step,
m_instance._fgcolor_r, m_instance._fgcolor_g, m_instance._fgcolor_b,
m_instance._bgcolor_r, m_instance._bgcolor_g, m_instance._bgcolor_b);
// Expand out the RGB channels into RGBA.
for (int yi = 0; yi < twirl_height; ++yi) {
const unsigned char *sp = twirl_data + yi * twirl_width * 3;
unsigned char *dp = new_data + yi * twirl_width * 4;
for (int xi = 0; xi < twirl_width; ++xi) {
// RGB <= BGR.
dp[0] = sp[2];
dp[1] = sp[1];
dp[2] = sp[0];
dp[3] = (unsigned char)0xff;
sp += 3;
dp += 4;
}
}
// Now load the image.
_twirl_bitmaps[step].CreateBitmap(twirl_width, twirl_height, 1, 32, new_data);
}
}
void CP3DActiveXCtrl::
timer_callback(HWND hwnd, UINT msg, UINT_PTR id, DWORD time) {
// Just invalidate the region and make it draw again.
::InvalidateRect(hwnd, NULL, FALSE);
}

View File

@ -18,6 +18,7 @@
#include "PPInstance.h" #include "PPInstance.h"
#include "PPPandaObject.h" #include "PPPandaObject.h"
#include "PPInterface.h" #include "PPInterface.h"
#include "get_twirl_data.h"
#include "Mshtml.h" #include "Mshtml.h"
#include <vector> #include <vector>
@ -75,7 +76,8 @@ public:
}; };
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
int Init( ); static void st_init(void *data);
int init();
virtual P3D_object* GetP3DObject( ); virtual P3D_object* GetP3DObject( );
virtual IOleClientSite* GetClientSte(); virtual IOleClientSite* GetClientSte();
@ -88,11 +90,27 @@ protected:
HRESULT ExchangeProperties( CPropExchange* pPropBag ); HRESULT ExchangeProperties( CPropExchange* pPropBag );
LRESULT OnPandaNotification(WPARAM wParam, LPARAM lParam); LRESULT OnPandaNotification(WPARAM wParam, LPARAM lParam);
void OnmainChanged(void);
void get_twirl_bitmaps();
static void CALLBACK timer_callback(HWND hwnd, UINT msg, UINT_PTR id, DWORD time);
PPandaObject* m_pPandaObject; PPandaObject* m_pPandaObject;
CComPtr<IOleClientSite> m_spClientSite; CComPtr<IOleClientSite> m_spClientSite;
void OnmainChanged(void); CBitmap _twirl_bitmaps[twirl_num_steps];
enum State {
S_init, // before starting the download
S_loading, // the instance is downloading
// From S_loading, only the "init" thread may change the state.
S_ready, // the instance is ready to run
S_started, // the instance has successfully started
S_failed, // something went wrong
};
State _state;
CEvent _init_not_running; // set when the init thread has finished, or before it has started.
}; };

View File

@ -39,6 +39,7 @@
#include "load_plugin.h" #include "load_plugin.h"
#include "find_root_dir.h" #include "find_root_dir.h"
#include "mkdir_complete.h" #include "mkdir_complete.h"
#include "parse_color.h"
// We can include this header file to get the DTOOL_PLATFORM // We can include this header file to get the DTOOL_PLATFORM
// definition, even though we don't link with dtool. // definition, even though we don't link with dtool.
@ -86,15 +87,70 @@ PPInstance::PPInstance( CP3DActiveXCtrl& parentCtrl ) :
_contents_expiration = 0; _contents_expiration = 0;
_failed = false; _failed = false;
_tokens = NULL;
_num_tokens = 0;
// Ensure this event is initially in the "set" state, in case we // Ensure this event is initially in the "set" state, in case we
// never get a download request before we get a close request. // never get a download request before we get a close request.
m_eventDownloadStopped.SetEvent( ); m_eventDownloadStopped.SetEvent( );
m_eventStop.ResetEvent();
nout << "Plugin is built with " << PANDA_PACKAGE_HOST_URL << "\n"; nout << "Plugin is built with " << PANDA_PACKAGE_HOST_URL << "\n";
} }
PPInstance::~PPInstance( ) PPInstance::~PPInstance() {
{ assert(_tokens == NULL);
}
// This is called at setup time to read the set of web tokens from the
// ActiveX control.
void PPInstance::
read_tokens() {
assert(_tokens == NULL);
_num_tokens = (int)m_parentCtrl.m_parameters.size();
_tokens = new P3D_token[ _num_tokens ];
for (int i = 0; i < _num_tokens; i++ ) {
std::pair< CString, CString > keyAndValue = m_parentCtrl.m_parameters[ i ];
// Make the token lowercase, since HTML is case-insensitive but
// we're not.
string keyword;
for (const char *p = m_parentCtrl.m_parameters[ i ].first; *p; ++p) {
keyword += tolower(*p);
}
_tokens[i]._keyword = strdup( keyword.c_str() );
_tokens[i]._value = strdup( m_parentCtrl.m_parameters[ i ].second );
}
// fgcolor and bgcolor are useful to know here (in case we have to
// draw a twirling icon).
// The default bgcolor is white.
_bgcolor_r = _bgcolor_g = _bgcolor_b = 0xff;
if (has_token("bgcolor")) {
int r, g, b;
if (parse_color(r, g, b, lookup_token("bgcolor"))) {
_bgcolor_r = r;
_bgcolor_g = g;
_bgcolor_b = b;
}
}
// The default fgcolor is either black or white, according to the
// brightness of the bgcolor.
if (_bgcolor_r + _bgcolor_g + _bgcolor_b > 0x80 + 0x80 + 0x80) {
_fgcolor_r = _fgcolor_g = _fgcolor_b = 0x00;
} else {
_fgcolor_r = _fgcolor_g = _fgcolor_b = 0xff;
}
if (has_token("fgcolor")) {
int r, g, b;
if (parse_color(r, g, b, lookup_token("fgcolor"))) {
_fgcolor_r = r;
_fgcolor_g = g;
_fgcolor_b = b;
}
}
} }
int PPInstance::DownloadFile( const std::string& from, const std::string& to ) int PPInstance::DownloadFile( const std::string& from, const std::string& to )
@ -499,6 +555,8 @@ int PPInstance::DownloadP3DComponents( std::string& p3dDllFilename )
int PPInstance::LoadPlugin( const std::string& dllFilename ) int PPInstance::LoadPlugin( const std::string& dllFilename )
{ {
CSingleLock lock(&_load_mutex);
lock.Lock();
if ( !m_pluginLoaded ) if ( !m_pluginLoaded )
{ {
ref_plugin(); ref_plugin();
@ -558,6 +616,9 @@ int PPInstance::LoadPlugin( const std::string& dllFilename )
int PPInstance::UnloadPlugin() int PPInstance::UnloadPlugin()
{ {
CSingleLock lock(&_load_mutex);
lock.Lock();
int error( 0 ); int error( 0 );
if ( m_pluginLoaded ) if ( m_pluginLoaded )
@ -586,7 +647,7 @@ unref_plugin() {
if ( s_instanceCount == 0 && is_plugin_loaded() ) { if ( s_instanceCount == 0 && is_plugin_loaded() ) {
nout << "Unloading core API\n"; nout << "Unloading core API\n";
unload_plugin(); unload_plugin(nout);
// This pointer is no longer valid and must be reset for next // This pointer is no longer valid and must be reset for next
// time. // time.
@ -596,7 +657,13 @@ unref_plugin() {
int PPInstance::Start( const std::string& p3dFilename ) int PPInstance::Start( const std::string& p3dFilename )
{ {
m_eventStop.ResetEvent(); {
CSingleLock lock(&_load_mutex);
lock.Lock();
assert(!m_isInit);
m_isInit = true;
}
P3D_window_handle parent_window; P3D_window_handle parent_window;
memset(&parent_window, 0, sizeof(parent_window)); memset(&parent_window, 0, sizeof(parent_window));
@ -606,29 +673,8 @@ int PPInstance::Start( const std::string& p3dFilename )
RECT rect; RECT rect;
GetClientRect( m_parentCtrl.m_hWnd, &rect ); GetClientRect( m_parentCtrl.m_hWnd, &rect );
P3D_token* p3dTokens = new P3D_token[ m_parentCtrl.m_parameters.size() ]; nout << "Creating new P3D instance object for " << p3dFilename << "\n";
for ( UINT i = 0; i < m_parentCtrl.m_parameters.size(); i++ ) m_p3dInstance = P3D_new_instance_ptr( &P3D_NotificationSync, _tokens, _num_tokens, 0, NULL, (void*)&m_parentCtrl );
{
std::pair< CString, CString > keyAndValue = m_parentCtrl.m_parameters[ i ];
// Make the token lowercase, since HTML is case-insensitive but
// we're not.
string keyword;
for (const char *p = m_parentCtrl.m_parameters[ i ].first; *p; ++p) {
keyword += tolower(*p);
}
p3dTokens[i]._keyword = strdup( keyword.c_str() );
p3dTokens[i]._value = strdup( m_parentCtrl.m_parameters[ i ].second );
}
nout << "Creating new P3D instance object \n";
m_p3dInstance = P3D_new_instance_ptr( &P3D_NotificationSync, p3dTokens, m_parentCtrl.m_parameters.size(), 0, NULL, (void*)&m_parentCtrl );
for ( UINT j = 0; j < m_parentCtrl.m_parameters.size(); j++ )
{
delete [] p3dTokens[j]._keyword;
delete [] p3dTokens[j]._value;
}
delete [] p3dTokens;
if ( !m_p3dInstance ) if ( !m_p3dInstance )
{ {
@ -646,15 +692,12 @@ int PPInstance::Start( const std::string& p3dFilename )
P3D_instance_setup_window_ptr( m_p3dInstance, P3D_WT_embedded, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, &parent_window ); P3D_instance_setup_window_ptr( m_p3dInstance, P3D_WT_embedded, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, &parent_window );
nout << "Starting new P3D instance " << p3dFilename << "\n"; nout << "Starting new P3D instance " << p3dFilename << "\n";
if ( !P3D_instance_start_ptr( m_p3dInstance, false, p3dFilename.c_str(), 0 ) ) if ( !P3D_instance_start_ptr( m_p3dInstance, false, p3dFilename.c_str(), 0 ) )
{ {
nout << "Error starting P3D instance: " << GetLastError() << "\n"; nout << "Error starting P3D instance: " << GetLastError() << "\n";
return 1; return 1;
} }
m_isInit = true;
return 0; return 0;
} }
@ -676,6 +719,17 @@ int PPInstance::Stop( )
{ {
UnloadPlugin(); UnloadPlugin();
} }
if (_tokens != NULL) {
for ( int j = 0; j < _num_tokens; ++j) {
delete [] _tokens[j]._keyword;
delete [] _tokens[j]._value;
}
delete [] _tokens;
_tokens = NULL;
_num_tokens = 0;
}
return 0; return 0;
} }
@ -839,33 +893,6 @@ HandleRequest( P3D_request *request ) {
return continue_loop; return continue_loop;
} }
////////////////////////////////////////////////////////////////////
// Function: PPInstance::lookup_token
// Access: Private
// Description: Returns the value associated with the first
// appearance of the named token, or empty string if the
// token does not appear.
////////////////////////////////////////////////////////////////////
string PPInstance::
lookup_token(const string &keyword) const {
for (UINT i = 0; i < m_parentCtrl.m_parameters.size(); i++) {
std::pair<CString, CString> keyAndValue = m_parentCtrl.m_parameters[i];
// Make the token lowercase, since HTML is case-insensitive but
// we're not.
const CString &orig_keyword = m_parentCtrl.m_parameters[i].first;
string lower_keyword;
for (const char *p = orig_keyword; *p; ++p) {
lower_keyword += tolower(*p);
}
if (lower_keyword == keyword) {
return (const char *)m_parentCtrl.m_parameters[i].second;
}
}
return string();
}
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
// Function: PPInstance::compare_seq // Function: PPInstance::compare_seq
// Access: Private, Static // Access: Private, Static
@ -963,4 +990,38 @@ set_failed() {
} }
////////////////////////////////////////////////////////////////////
// Function: PPInstance::lookup_token
// Access: Private
// Description: Returns the value associated with the first
// appearance of the named token, or empty string if the
// token does not appear.
////////////////////////////////////////////////////////////////////
std::string PPInstance::
lookup_token(const std::string &keyword) const {
for (int i = 0; i < _num_tokens; ++i) {
if (strcmp(_tokens[i]._keyword, keyword.c_str()) == 0) {
return _tokens[i]._value;
}
}
return string();
}
////////////////////////////////////////////////////////////////////
// Function: PPInstance::has_token
// Access: Private
// Description: Returns true if the named token appears in the list,
// false otherwise.
////////////////////////////////////////////////////////////////////
bool PPInstance::
has_token(const std::string &keyword) const {
for (int i = 0; i < _num_tokens; ++i) {
if (strcmp(_tokens[i]._keyword, keyword.c_str()) == 0) {
return true;
}
}
return false;
}

View File

@ -37,6 +37,7 @@ public:
PPInstance( CP3DActiveXCtrl& parentCtrl ); PPInstance( CP3DActiveXCtrl& parentCtrl );
virtual ~PPInstance( ); virtual ~PPInstance( );
void read_tokens();
int DownloadP3DComponents( std::string& p3dDllFilename ); int DownloadP3DComponents( std::string& p3dDllFilename );
int LoadPlugin( const std::string& dllFilename ); int LoadPlugin( const std::string& dllFilename );
@ -61,6 +62,10 @@ public:
P3D_object* m_p3dObject; P3D_object* m_p3dObject;
// Set from fgcolor & bgcolor.
int _fgcolor_r, _fgcolor_b, _fgcolor_g;
int _bgcolor_r, _bgcolor_b, _bgcolor_g;
protected: protected:
PPInstance( ); PPInstance( );
PPInstance( const PPInstance& ); PPInstance( const PPInstance& );
@ -77,18 +82,21 @@ protected:
bool HandleRequest( P3D_request *request ); bool HandleRequest( P3D_request *request );
static void HandleRequestGetUrl( void *data ); static void HandleRequestGetUrl( void *data );
string lookup_token(const string &keyword) const;
static int compare_seq(const string &seq_a, const string &seq_b); static int compare_seq(const string &seq_a, const string &seq_b);
static int compare_seq_int(const char *&num_a, const char *&num_b); static int compare_seq_int(const char *&num_a, const char *&num_b);
void set_failed(); void set_failed();
std::string lookup_token(const std::string &keyword) const;
bool has_token(const std::string &keyword) const;
P3D_instance* m_p3dInstance; P3D_instance* m_p3dInstance;
CP3DActiveXCtrl& m_parentCtrl; CP3DActiveXCtrl& m_parentCtrl;
PPLogger m_logger; PPLogger m_logger;
bool m_isInit; bool m_isInit;
bool m_pluginLoaded; bool m_pluginLoaded;
CMutex _load_mutex;
std::string _download_url_prefix; std::string _download_url_prefix;
typedef std::vector<std::string> Mirrors; typedef std::vector<std::string> Mirrors;
@ -98,6 +106,9 @@ protected:
time_t _contents_expiration; time_t _contents_expiration;
bool _failed; bool _failed;
P3D_token *_tokens;
int _num_tokens;
std::string m_rootDir; std::string m_rootDir;
class ThreadedRequestData { class ThreadedRequestData {

View File

@ -100,7 +100,7 @@ PPInstance(NPMIMEType pluginType, NPP instance, uint16_t mode,
// fgcolor and bgcolor are useful to know here (in case we have to // fgcolor and bgcolor are useful to know here (in case we have to
// draw a twirling icon). // draw a twirling icon).
// The default fgcolor is white. // The default bgcolor is white.
_bgcolor_r = _bgcolor_g = _bgcolor_b = 0xff; _bgcolor_r = _bgcolor_g = _bgcolor_b = 0xff;
if (has_token("bgcolor")) { if (has_token("bgcolor")) {
int r, g, b; int r, g, b;
@ -1975,7 +1975,7 @@ lookup_token(const string &keyword) const {
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
// Function: PPInstance::has_token // Function: PPInstance::has_token
// Access: Public // Access: Private
// Description: Returns true if the named token appears in the list, // Description: Returns true if the named token appears in the list,
// false otherwise. // false otherwise.
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////

View File

@ -269,7 +269,7 @@ NP_GetEntryPoints(NPPluginFuncs *pluginFuncs) {
NPError OSCALL NPError OSCALL
NP_Shutdown(void) { NP_Shutdown(void) {
nout << "shutdown\n"; nout << "shutdown\n";
unload_plugin(); unload_plugin(nout);
PPBrowserObject::clear_class_definition(); PPBrowserObject::clear_class_definition();
// Not clear whether there's a return value or not. Some versions // Not clear whether there's a return value or not. Some versions

View File

@ -241,6 +241,6 @@ run_embedded(streampos read_offset, int argc, char *argv[]) {
run_main_loop(); run_main_loop();
unload_plugin(); unload_plugin(cerr);
return 0; return 0;
} }

View File

@ -285,7 +285,7 @@ run_command_line(int argc, char *argv[]) {
run_main_loop(); run_main_loop();
// All instances have finished; we can exit. // All instances have finished; we can exit.
unload_plugin(); unload_plugin(cerr);
return 0; return 0;
} }