2 Copyright (c) 2001, Loki software, inc.
5 Redistribution and use in source and binary forms, with or without modification,
6 are permitted provided that the following conditions are met:
8 Redistributions of source code must retain the above copyright notice, this list
9 of conditions and the following disclaimer.
11 Redistributions in binary form must reproduce the above copyright notice, this
12 list of conditions and the following disclaimer in the documentation and/or
13 other materials provided with the distribution.
15 Neither the name of Loki software nor the names of its contributors may be used
16 to endorse or promote products derived from this software without specific prior
19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
20 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
23 DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 //-----------------------------------------------------------------------------
34 // monitoring window for running BSP processes (and possibly various other stuff)
37 #include "globaldefs.h"
43 #include "string/string.h"
44 #include "stream/stringstream.h"
46 #include "gtkutil/messagebox.h"
49 #include "preferences.h"
52 #include "mainframe.h"
55 void message_flush(message_info_t *self)
57 Sys_Print(self->msg_level, self->m_buffer, self->m_length);
61 void message_print(message_info_t *self, const char *characters, std::size_t length)
63 const char *end = characters + length;
64 while (characters != end) {
65 std::size_t space = message_info_t::bufsize - 1 - self->m_length;
69 std::size_t size = std::min(space, std::size_t(end - characters));
70 memcpy(self->m_buffer + self->m_length, characters, size);
71 self->m_length += size;
79 #include <uilib/uilib.h>
84 // a flag we have set to true when using an external BSP plugin
85 // the resulting code with that is a bit dirty, cleaner solution would be to seperate the succession of commands from the listening loop
86 // (in two seperate classes probably)
89 // EIdle: we are not listening
90 // DoMonitoringLoop will change state to EBeginStep
91 // EBeginStep: the socket is up for listening, we are expecting incoming connection
92 // incoming connection will change state to EWatching
93 // EWatching: we have a connection, monitor it
94 // connection closed will see if we start a new step (EBeginStep) or launch Quake3 and end (EIdle)
95 enum EWatchBSPState { EIdle, EBeginStep, EWatching } m_eState;
96 socket_t *m_pListenSocket;
97 socket_t *m_pInSocket;
100 // used to timeout EBeginStep
102 std::size_t m_iCurrentStep;
103 // name of the map so we can run the engine
105 // buffer we use in push mode to receive data directly from the network
106 xmlParserInputBufferPtr m_xmlInputBuffer;
107 xmlParserInputPtr m_xmlInput;
108 xmlParserCtxtPtr m_xmlParserCtxt;
110 // call this to switch the set listening mode
111 bool SetupListening();
113 // start a new EBeginStep
116 // the xml and sax parser state
117 char m_xmlBuf[MAX_NETMESSAGE];
118 bool m_bNeedCtxtInit;
119 message_info_t m_message_info;
125 m_bBSPPlugin = false;
126 m_pListenSocket = NULL;
129 m_pTimer = g_timer_new();
131 m_xmlInputBuffer = NULL;
132 m_bNeedCtxtInit = true;
140 g_timer_destroy(m_pTimer);
143 bool HasBSPPlugin() const
144 { return m_bBSPPlugin; }
146 // called regularly to keep listening
147 void RoutineProcessing();
149 // start a monitoring loop with the following steps
150 void DoMonitoringLoop(GPtrArray *pCmd, const char *sBSPName);
152 void EndMonitoringLoop()
156 string_release(m_sBSPName, string_length(m_sBSPName));
160 g_ptr_array_free(m_pCmd, TRUE);
165 // close everything - may be called from the outside to abort the process
168 // start a listening loop for an external process, possibly a BSP plugin
169 void ExternalListen();
172 CWatchBSP *g_pWatchBSP;
174 // watch the BSP process through network connections
175 // true: trigger the BSP steps one by one and monitor them through the network
176 // false: create a BAT / .sh file and execute it. don't bother monitoring it.
177 bool g_WatchBSP_Enabled = true;
178 // do we stop the compilation process if we come accross a leak?
179 bool g_WatchBSP_LeakStop = true;
180 bool g_WatchBSP_RunQuake = false;
181 // store prefs setting for automatic sleep mode activation
182 bool g_WatchBSP_DoSleep = true;
183 // timeout when beginning a step (in seconds)
184 // if we don't get a connection quick enough we assume something failed and go back to idling
185 int g_WatchBSP_Timeout = 10;
188 void Build_constructPreferences(PreferencesPage &page)
190 ui::CheckButton monitorbsp = page.appendCheckBox("", "Enable Build Process Monitoring", g_WatchBSP_Enabled);
191 ui::CheckButton leakstop = page.appendCheckBox("", "Stop Compilation on Leak", g_WatchBSP_LeakStop);
192 ui::CheckButton runengine = page.appendCheckBox("", "Run Engine After Compile", g_WatchBSP_RunQuake);
193 ui::CheckButton sleep = page.appendCheckBox("", "Sleep When Running the Engine", g_WatchBSP_DoSleep);
194 Widget_connectToggleDependency(leakstop, monitorbsp);
195 Widget_connectToggleDependency(runengine, monitorbsp);
196 Widget_connectToggleDependency(sleep, runengine);
199 void Build_constructPage(PreferenceGroup &group)
201 PreferencesPage page(group.createPage("Build", "Build Preferences"));
202 Build_constructPreferences(page);
205 void Build_registerPreferencesPage()
207 PreferencesDialog_addSettingsPage(makeCallbackF(Build_constructPage));
210 #include "preferencesystem.h"
211 #include "stringio.h"
213 void BuildMonitor_Construct()
215 g_pWatchBSP = new CWatchBSP();
217 g_WatchBSP_Enabled = !string_equal(g_pGameDescription->getKeyValue("no_bsp_monitor"), "1");
219 GlobalPreferenceSystem().registerPreference("WatchBSP", make_property_string(g_WatchBSP_Enabled));
220 GlobalPreferenceSystem().registerPreference("RunQuake2Run", make_property_string(g_WatchBSP_RunQuake));
221 GlobalPreferenceSystem().registerPreference("LeakStop", make_property_string(g_WatchBSP_LeakStop));
222 GlobalPreferenceSystem().registerPreference("SleepMode", make_property_string(g_WatchBSP_DoSleep));
224 Build_registerPreferencesPage();
227 void BuildMonitor_Destroy()
232 CWatchBSP *GetWatchBSP()
237 void BuildMonitor_Run(GPtrArray *commands, const char *mapName)
239 GetWatchBSP()->DoMonitoringLoop(commands, mapName);
243 // Static functions for the SAX callbacks -------------------------------------------------------
245 // utility for saxStartElement below
246 static void abortStream(message_info_t *data)
248 GetWatchBSP()->EndMonitoringLoop();
249 // tell there has been an error
251 if ( GetWatchBSP()->HasBSPPlugin() ) {
252 g_BSPFrontendTable.m_pfnEndListen( 2 );
255 // yeah this doesn't look good.. but it's needed so that everything will be ignored until the stream goes out
256 data->ignore_depth = -1;
260 #include "stream_version.h"
262 static void saxStartElement(message_info_t *data, const xmlChar *name, const xmlChar **attrs)
265 globalOutputStream() << "<" << name;
267 for ( const xmlChar** p = attrs; *p != 0; p += 2 )
269 globalOutputStream() << " " << p[0] << "=" << makeQuoted( p[1] );
272 globalOutputStream() << ">\n";
275 if (data->ignore_depth == 0) {
276 if (data->pGeometry != 0) {
278 data->pGeometry->saxStartElement(data, name, attrs);
280 if (strcmp(reinterpret_cast<const char *>( name ), "q3map_feedback") == 0) {
281 // check the correct version
282 // old q3map don't send a version attribute
283 // the ones we support .. send Q3MAP_STREAM_VERSION
284 if (!attrs[0] || !attrs[1] || (strcmp(reinterpret_cast<const char *>( attrs[0] ), "version") != 0)) {
287 << "No stream version given in the feedback stream, this is an old q3map version.\n"
288 "Please turn off monitored compiling if you still wish to use this q3map executable\n";
291 } else if (strcmp(reinterpret_cast<const char *>( attrs[1] ), Q3MAP_STREAM_VERSION) != 0) {
293 globalErrorStream() <<
294 "This version of Radiant reads version " Q3MAP_STREAM_VERSION " debug streams, I got an incoming connection with version "
295 << reinterpret_cast<const char *>( attrs[1] ) << "\n"
296 "Please make sure your versions of Radiant and q3map are matching.\n";
301 // we don't treat locally
302 else if (strcmp(reinterpret_cast<const char *>( name ), "message") == 0) {
303 int msg_level = atoi(reinterpret_cast<const char *>( attrs[1] ));
304 if (msg_level != data->msg_level) {
306 data->msg_level = msg_level;
308 } else if (strcmp(reinterpret_cast<const char *>( name ), "polyline") == 0) {
309 // polyline has a particular status .. right now we only use it for leakfile ..
310 data->geometry_depth = data->recurse;
311 data->pGeometry = &g_pointfile;
312 data->pGeometry->saxStartElement(data, name, attrs);
313 } else if (strcmp(reinterpret_cast<const char *>( name ), "select") == 0) {
314 CSelectMsg *pSelect = new CSelectMsg();
315 data->geometry_depth = data->recurse;
316 data->pGeometry = pSelect;
317 data->pGeometry->saxStartElement(data, name, attrs);
318 } else if (strcmp(reinterpret_cast<const char *>( name ), "pointmsg") == 0) {
319 CPointMsg *pPoint = new CPointMsg();
320 data->geometry_depth = data->recurse;
321 data->pGeometry = pPoint;
322 data->pGeometry->saxStartElement(data, name, attrs);
323 } else if (strcmp(reinterpret_cast<const char *>( name ), "windingmsg") == 0) {
324 CWindingMsg *pWinding = new CWindingMsg();
325 data->geometry_depth = data->recurse;
326 data->pGeometry = pWinding;
327 data->pGeometry->saxStartElement(data, name, attrs);
329 globalErrorStream() << "Warning: ignoring unrecognized node in XML stream ("
330 << reinterpret_cast<const char *>( name ) << ")\n";
331 // we don't recognize this node, jump over it
332 // (NOTE: the ignore mechanism is a bit screwed, only works when starting an ignore at the highest level)
333 data->ignore_depth = data->recurse;
340 static void saxEndElement(message_info_t *data, const xmlChar *name)
343 globalOutputStream() << "<" << name << "/>\n";
347 // we are out of an ignored chunk
348 if (data->recurse == data->ignore_depth) {
349 data->ignore_depth = 0;
352 if (data->pGeometry != 0) {
353 data->pGeometry->saxEndElement(data, name);
354 // we add the object to the debug window
355 if (data->geometry_depth == data->recurse) {
356 g_DbgDlg.Push(data->pGeometry);
360 if (data->recurse == data->stop_depth) {
363 globalOutputStream() << "Received error msg .. shutting down..\n";
365 GetWatchBSP()->EndMonitoringLoop();
366 // tell there has been an error
368 if ( GetWatchBSP()->HasBSPPlugin() ) {
369 g_BSPFrontendTable.m_pfnEndListen( 2 );
376 class MessageOutputStream : public TextOutputStream {
377 message_info_t *m_data;
379 MessageOutputStream(message_info_t *data) : m_data(data)
383 std::size_t write(const char *buffer, std::size_t length)
385 if (m_data->pGeometry != 0) {
386 m_data->pGeometry->saxCharacters(m_data, reinterpret_cast<const xmlChar *>( buffer ), int(length));
388 if (m_data->ignore_depth == 0) {
389 // output the message using the level
390 message_print(m_data, buffer, length);
391 // if this message has error level flag, we mark the depth to stop the compilation when we get out
392 // we don't set the msg level if we don't stop on leak
393 if (m_data->msg_level == 3) {
394 m_data->stop_depth = m_data->recurse - 1;
404 inline MessageOutputStream &operator<<(MessageOutputStream &ostream, const T &t)
406 return ostream_write(ostream, t);
409 static void saxCharacters(message_info_t *data, const xmlChar *ch, int len)
411 MessageOutputStream ostream(data);
412 ostream << StringRange(reinterpret_cast<const char *>( ch ), reinterpret_cast<const char *>( ch + len ));
415 static void saxComment(void *ctx, const xmlChar *msg)
417 globalOutputStream() << "XML comment: " << reinterpret_cast<const char *>( msg ) << "\n";
420 static void saxWarning(void *ctx, const char *msg, ...)
422 char saxMsgBuffer[4096];
426 vsprintf(saxMsgBuffer, msg, args);
428 globalOutputStream() << "XML warning: " << saxMsgBuffer << "\n";
431 static void saxError(void *ctx, const char *msg, ...)
433 char saxMsgBuffer[4096];
437 vsprintf(saxMsgBuffer, msg, args);
439 globalErrorStream() << "XML error: " << saxMsgBuffer << "\n";
442 static void saxFatal(void *ctx, const char *msg, ...)
449 vsprintf(buffer, msg, args);
451 globalErrorStream() << "XML fatal error: " << buffer << "\n";
454 static xmlSAXHandler saxParser = {
455 0, /* internalSubset */
456 0, /* isStandalone */
457 0, /* hasInternalSubset */
458 0, /* hasExternalSubset */
459 0, /* resolveEntity */
462 0, /* notationDecl */
463 0, /* attributeDecl */
465 0, /* unparsedEntityDecl */
466 0, /* setDocumentLocator */
467 0, /* startDocument */
469 (startElementSAXFunc) saxStartElement, /* startElement */
470 (endElementSAXFunc) saxEndElement, /* endElement */
472 (charactersSAXFunc) saxCharacters, /* characters */
473 0, /* ignorableWhitespace */
474 0, /* processingInstruction */
475 (commentSAXFunc) saxComment, /* comment */
476 (warningSAXFunc) saxWarning, /* warning */
477 (errorSAXFunc) saxError, /* error */
478 (fatalErrorSAXFunc) saxFatal, /* fatalError */
489 // ------------------------------------------------------------------------------------------------
494 static gint watchbsp_routine(gpointer data)
496 reinterpret_cast<CWatchBSP *>( data )->RoutineProcessing();
500 void CWatchBSP::Reset()
503 Net_Disconnect(m_pInSocket);
506 if (m_pListenSocket) {
507 Net_Disconnect(m_pListenSocket);
508 m_pListenSocket = NULL;
510 if (m_xmlInputBuffer) {
511 xmlFreeParserInputBuffer(m_xmlInputBuffer);
512 m_xmlInputBuffer = NULL;
516 g_source_remove(s_routine_id);
520 bool CWatchBSP::SetupListening()
523 if (m_pListenSocket) {
524 globalOutputStream() << "ERROR: m_pListenSocket != NULL in CWatchBSP::SetupListening\n";
528 globalOutputStream() << "Setting up\n";
530 m_pListenSocket = Net_ListenSocket(39000);
531 if (m_pListenSocket == NULL) {
534 globalOutputStream() << "Listening...\n";
538 void CWatchBSP::DoEBeginStep()
541 if (SetupListening() == false) {
542 const char *msg = "Failed to get a listening socket on port 39000.\nTry running with Build monitoring disabled if you can't fix this.\n";
543 globalOutputStream() << msg;
544 ui::alert(MainFrame_getWindow(), msg, "Build monitoring", ui::alert_type::OK, ui::alert_icon::Error);
547 // set the timer for timeouts and step cancellation
548 g_timer_reset(m_pTimer);
549 g_timer_start(m_pTimer);
552 globalOutputStream() << "=== running build command ===\n"
553 << static_cast<const char *>(g_ptr_array_index(m_pCmd, m_iCurrentStep) ) << "\n";
555 if (!Q_Exec(NULL, (char *) g_ptr_array_index(m_pCmd, m_iCurrentStep), NULL, true, false)) {
556 StringOutputStream msg(256);
557 msg << "Failed to execute the following command: ";
558 msg << reinterpret_cast<const char *>(g_ptr_array_index(m_pCmd, m_iCurrentStep) );
559 msg << "\nCheck that the file exists and that you don't run out of system resources.\n";
560 globalOutputStream() << msg.c_str();
561 ui::alert(MainFrame_getWindow(), msg.c_str(), "Build monitoring", ui::alert_type::OK,
562 ui::alert_icon::Error);
565 // re-initialise the debug window
566 if (m_iCurrentStep == 0) {
570 m_eState = EBeginStep;
571 s_routine_id = g_timeout_add(25, watchbsp_routine, this);
576 const char *ENGINE_ATTRIBUTE = "engine_win32";
577 const char *MP_ENGINE_ATTRIBUTE = "mp_engine_win32";
578 #elif GDEF_OS_LINUX || GDEF_OS_BSD
579 const char *ENGINE_ATTRIBUTE = "engine_linux";
580 const char *MP_ENGINE_ATTRIBUTE = "mp_engine_linux";
582 const char *ENGINE_ATTRIBUTE = "engine_macos";
583 const char *MP_ENGINE_ATTRIBUTE = "mp_engine_macos";
585 #error "unsupported platform"
588 class RunEngineConfiguration {
590 const char *executable;
591 const char *mp_executable;
594 RunEngineConfiguration() :
595 executable(g_pGameDescription->getRequiredKeyValue(ENGINE_ATTRIBUTE)),
596 mp_executable(g_pGameDescription->getKeyValue(MP_ENGINE_ATTRIBUTE))
598 do_sp_mp = !string_empty(mp_executable);
602 inline void GlobalGameDescription_string_write_mapparameter(StringOutputStream &string, const char *mapname)
604 if (g_pGameDescription->mGameType == "q2"
605 || g_pGameDescription->mGameType == "heretic2") {
606 string << ". +exec radiant.cfg +map " << mapname;
608 string << "+set sv_pure 0 ";
609 // TTimo: a check for vm_* but that's all fine
610 //cmdline = "+set sv_pure 0 +set vm_ui 0 +set vm_cgame 0 +set vm_game 0 ";
611 const char *fs_game = gamename_get();
612 if (!string_equal(fs_game, basegame_get())) {
613 string << "+set fs_game " << fs_game << " ";
615 if (g_pGameDescription->mGameType == "wolf") {
616 //|| g_pGameDescription->mGameType == "et")
617 if (string_equal(gamemode_get(), "mp")) {
619 string << "+devmap " << mapname;
622 string << "+set nextmap \"spdevmap " << mapname << "\"";
625 string << "+devmap " << mapname;
631 void CWatchBSP::RoutineProcessing()
635 // timeout: if we don't get an incoming connection fast enough, go back to idle
636 if (g_timer_elapsed(m_pTimer, NULL) > g_WatchBSP_Timeout) {
637 ui::alert(MainFrame_getWindow(),
638 "The connection timed out, assuming the build process failed\nMake sure you are using a networked version of Q3Map?\nOtherwise you need to disable BSP Monitoring in prefs.",
639 "BSP process monitoring", ui::alert_type::OK);
642 if ( m_bBSPPlugin ) {
643 // status == 1 : didn't get the connection
644 g_BSPFrontendTable.m_pfnEndListen( 1 );
651 if (!m_pListenSocket) {
653 << "ERROR: m_pListenSocket == NULL in CWatchBSP::RoutineProcessing EBeginStep state\n";
657 // we are not connected yet, accept any incoming connection
658 m_pInSocket = Net_Accept(m_pListenSocket);
660 globalOutputStream() << "Connected.\n";
661 // prepare the message info struct for diving in
662 memset(&m_message_info, 0, sizeof(message_info_t));
663 // a dumb flag to make sure we init the push parser context when first getting a msg
664 m_bNeedCtxtInit = true;
665 m_eState = EWatching;
672 globalErrorStream() << "ERROR: m_pInSocket == NULL in CWatchBSP::RoutineProcessing EWatching state\n";
677 int ret = Net_Wait(m_pInSocket, 0, 0);
679 globalOutputStream() << "WARNING: SOCKET_ERROR in CWatchBSP::RoutineProcessing\n";
680 globalOutputStream() << "Terminating the connection.\n";
686 // the socket has been identified, there's something (message or disconnection)
687 // see if there's anything in input
688 ret = Net_Receive(m_pInSocket, &msg);
690 // unsigned int size = msg.size; //++timo just a check
691 strcpy(m_xmlBuf, NMSG_ReadString(&msg));
692 if (m_bNeedCtxtInit) {
693 m_xmlParserCtxt = NULL;
694 m_xmlParserCtxt = xmlCreatePushParserCtxt(&saxParser, &m_message_info, m_xmlBuf,
695 static_cast<int>( strlen(m_xmlBuf)), NULL);
697 if (m_xmlParserCtxt == NULL) {
698 globalErrorStream() << "Failed to create the XML parser (incoming stream began with: "
699 << m_xmlBuf << ")\n";
702 m_bNeedCtxtInit = false;
704 xmlParseChunk(m_xmlParserCtxt, m_xmlBuf, static_cast<int>( strlen(m_xmlBuf)), 0);
707 message_flush(&m_message_info);
708 // error or connection closed/reset
709 // NOTE: if we get an error down the XML stream we don't reach here
710 Net_Disconnect(m_pInSocket);
712 globalOutputStream() << "Connection closed.\n";
714 if ( m_bBSPPlugin ) {
716 // let the BSP plugin know that the job is done
717 g_BSPFrontendTable.m_pfnEndListen( 0 );
721 // move to next step or finish
723 if (m_iCurrentStep < m_pCmd->len) {
726 // launch the engine .. OMG
727 if (g_WatchBSP_RunQuake) {
729 // do we enter sleep mode before?
730 if ( g_WatchBSP_DoSleep ) {
731 globalOutputStream() << "Going into sleep mode..\n";
732 g_pParentWnd->OnSleep();
735 globalOutputStream() << "Running engine...\n";
736 StringOutputStream cmd(256);
737 // build the command line
738 cmd << EnginePath_get();
739 // this is game dependant
741 RunEngineConfiguration engineConfig;
743 if (engineConfig.do_sp_mp) {
744 if (string_equal(gamemode_get(), "mp")) {
745 cmd << engineConfig.mp_executable;
747 cmd << engineConfig.executable;
750 cmd << engineConfig.executable;
753 StringOutputStream cmdline;
755 GlobalGameDescription_string_write_mapparameter(cmdline, m_sBSPName);
757 globalOutputStream() << cmd.c_str() << " " << cmdline.c_str() << "\n";
760 if (!Q_Exec(cmd.c_str(), (char *) cmdline.c_str(), EnginePath_get(), false, false)) {
761 StringOutputStream msg;
762 msg << "Failed to execute the following command: " << cmd.c_str() << cmdline.c_str();
763 globalOutputStream() << msg.c_str();
764 ui::alert(MainFrame_getWindow(), msg.c_str(), "Build monitoring", ui::alert_type::OK,
765 ui::alert_icon::Error);
779 GPtrArray *str_ptr_array_clone(GPtrArray *array)
781 GPtrArray *cloned = g_ptr_array_sized_new(array->len);
782 for (guint i = 0; i < array->len; ++i) {
783 g_ptr_array_add(cloned, g_strdup((char *) g_ptr_array_index(array, i)));
788 void CWatchBSP::DoMonitoringLoop(GPtrArray *pCmd, const char *sBSPName)
790 m_sBSPName = string_clone(sBSPName);
791 if (m_eState != EIdle) {
792 globalOutputStream() << "WatchBSP got a monitoring request while not idling...\n";
793 // prompt the user, should we cancel the current process and go ahead?
794 if (ui::alert(MainFrame_getWindow(),
795 "I am already monitoring a Build process.\nDo you want me to override and start a new compilation?",
796 "Build process monitoring", ui::alert_type::YESNO) == ui::alert_response::YES) {
797 // disconnect and set EIdle state
801 m_pCmd = str_ptr_array_clone(pCmd);
806 void CWatchBSP::ExternalListen()
812 // the part of the watchbsp interface we export to plugins
813 // NOTE: in the long run, the whole watchbsp.cpp interface needs to go out and be handled at the BSP plugin level
814 // for now we provide something really basic and limited, the essential is to have something that works fine and fast (for 1.1 final)
817 // open the listening socket
818 GetWatchBSP()->ExternalListen();