1792 lines
54 KiB
C++
1792 lines
54 KiB
C++
/**
|
|
* PANDA 3D SOFTWARE
|
|
* Copyright (c) Carnegie Mellon University. All rights reserved.
|
|
*
|
|
* All use of this software is subject to the terms of the revised BSD
|
|
* license. You should have received a copy of this license along
|
|
* with this source code in a file named "LICENSE."
|
|
*
|
|
* @file p3dSession.cxx
|
|
* @author drose
|
|
* @date 2009-06-03
|
|
*/
|
|
|
|
#include "p3dSession.h"
|
|
#include "p3dInstance.h"
|
|
#include "p3dInstanceManager.h"
|
|
#include "p3d_plugin_config.h"
|
|
#include "p3dUndefinedObject.h"
|
|
#include "p3dNoneObject.h"
|
|
#include "p3dBoolObject.h"
|
|
#include "p3dIntObject.h"
|
|
#include "p3dFloatObject.h"
|
|
#include "p3dPythonObject.h"
|
|
#include "p3dConcreteSequence.h"
|
|
#include "p3dConcreteStruct.h"
|
|
#include "binaryXml.h"
|
|
#include "mkdir_complete.h"
|
|
#include "wstring_encode.h"
|
|
#include "run_p3dpython.h"
|
|
|
|
#include <ctype.h>
|
|
#include <time.h>
|
|
|
|
#ifndef _WIN32
|
|
#include <fcntl.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/select.h>
|
|
#include <signal.h>
|
|
#include <dlfcn.h>
|
|
#endif
|
|
|
|
#ifdef __APPLE__
|
|
#include <crt_externs.h>
|
|
#endif
|
|
|
|
using std::string;
|
|
using std::wstring;
|
|
|
|
/**
|
|
* Creates a new session, corresponding to a new subprocess with its own copy
|
|
* of Python. The initial parameters for the session are taken from the
|
|
* indicated instance object (but the instance itself is not automatically
|
|
* started within the session).
|
|
*/
|
|
P3DSession::
|
|
P3DSession(P3DInstance *inst) {
|
|
P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
|
|
_session_id = inst_mgr->get_unique_id();
|
|
_session_key = inst->get_session_key();
|
|
_matches_script_origin = inst->get_matches_script_origin();
|
|
_keep_user_env = false;
|
|
_failed = false;
|
|
|
|
#ifdef _WIN32
|
|
_p3dpython_handle = INVALID_HANDLE_VALUE;
|
|
#else
|
|
_p3dpython_pid = -1;
|
|
#endif
|
|
_p3dpython_one_process = false;
|
|
_p3dpython_started = false;
|
|
_p3dpython_running = false;
|
|
|
|
_started_read_thread = false;
|
|
_read_thread_continue = false;
|
|
|
|
INIT_LOCK(_instances_lock);
|
|
INIT_THREAD(_read_thread);
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
*/
|
|
P3DSession::
|
|
~P3DSession() {
|
|
assert(!_p3dpython_running);
|
|
DESTROY_LOCK(_instances_lock);
|
|
}
|
|
|
|
/**
|
|
* Terminates the session by shutting down Python and stopping the subprocess.
|
|
*/
|
|
void P3DSession::
|
|
shutdown() {
|
|
set_failed();
|
|
|
|
int exit_code = 0;
|
|
|
|
if (_p3dpython_started) {
|
|
// Tell the process we're going away.
|
|
TiXmlDocument doc;
|
|
TiXmlElement *xcommand = new TiXmlElement("command");
|
|
xcommand->SetAttribute("cmd", "exit");
|
|
doc.LinkEndChild(xcommand);
|
|
write_xml(_pipe_write, &doc, nout);
|
|
|
|
// Also close the pipe, to help underscore the point.
|
|
_pipe_write.close();
|
|
|
|
// Closing _pipe_read before the thread has stopped can result in a hang.
|
|
// Don't need to close it yet. _pipe_read.close();
|
|
|
|
static const int max_wait_ms = 2000;
|
|
|
|
if (_p3dpython_one_process) {
|
|
// Since it's running in a thread, we can't reliably force-kill it. So,
|
|
// just wait.
|
|
nout << "Waiting for Python thread to exit\n";
|
|
JOIN_THREAD(_p3dpython_thread);
|
|
nout << "Done waiting.\n";
|
|
_p3dpython_one_process = false;
|
|
|
|
} else {
|
|
// Python's running in a sub-process, the preferred way. In this case,
|
|
// we can wait a brief amount of time before it closes itself; but if it
|
|
// doesn't, we can safely force-kill it.
|
|
|
|
#ifdef _WIN32
|
|
// Wait for a certain amount of time for the process to stop by itself.
|
|
while (WaitForSingleObject(_p3dpython_handle, max_wait_ms) == WAIT_TIMEOUT) {
|
|
// It didn't shut down cleanly, so kill it the hard way.
|
|
nout << "Force-killing python process.\n";
|
|
TerminateProcess(_p3dpython_handle, 2);
|
|
}
|
|
|
|
DWORD dw_exit_code = 0;
|
|
if (!GetExitCodeProcess(_p3dpython_handle, &dw_exit_code)) {
|
|
nout << "GetExitCodeProcess failed, error: " << GetLastError() << "\n";
|
|
} else {
|
|
exit_code = (int)dw_exit_code;
|
|
}
|
|
|
|
CloseHandle(_p3dpython_handle);
|
|
_p3dpython_handle = INVALID_HANDLE_VALUE;
|
|
|
|
#else // _WIN32
|
|
// Wait for a certain amount of time for the process to stop by itself.
|
|
struct timeval start;
|
|
gettimeofday(&start, nullptr);
|
|
int start_ms = start.tv_sec * 1000 + start.tv_usec / 1000;
|
|
|
|
int status;
|
|
pid_t result = waitpid(_p3dpython_pid, &status, WNOHANG);
|
|
while (result != _p3dpython_pid) {
|
|
if (result == -1) {
|
|
perror("waitpid");
|
|
break;
|
|
}
|
|
|
|
struct timeval now;
|
|
gettimeofday(&now, nullptr);
|
|
int now_ms = now.tv_sec * 1000 + now.tv_usec / 1000;
|
|
int elapsed = now_ms - start_ms;
|
|
|
|
if (elapsed > max_wait_ms) {
|
|
// Tired of waiting. Kill the process.
|
|
nout << "Force-killing python process, pid " << _p3dpython_pid
|
|
<< "\n";
|
|
kill(_p3dpython_pid, SIGKILL);
|
|
start_ms = now_ms;
|
|
}
|
|
|
|
// Yield the timeslice and wait some more.
|
|
struct timeval tv;
|
|
tv.tv_sec = 0;
|
|
tv.tv_usec = 1;
|
|
select(0, nullptr, nullptr, nullptr, &tv);
|
|
result = waitpid(_p3dpython_pid, &status, WNOHANG);
|
|
}
|
|
_p3dpython_pid = -1;
|
|
|
|
nout << "Python process has successfully stopped.\n";
|
|
if (WIFEXITED(status)) {
|
|
exit_code = WEXITSTATUS(status);
|
|
nout << " exited normally, status = " << exit_code << "\n";
|
|
|
|
} else if (WIFSIGNALED(status)) {
|
|
nout << " signalled by " << WTERMSIG(status) << ", core = "
|
|
<< WCOREDUMP(status) << "\n";
|
|
|
|
// This seems to be a popular shell convention.
|
|
exit_code = 128 + WTERMSIG(status);
|
|
|
|
} else if (WIFSTOPPED(status)) {
|
|
nout << " stopped by " << WSTOPSIG(status) << "\n";
|
|
}
|
|
|
|
#endif // _WIN32
|
|
}
|
|
|
|
_p3dpython_running = false;
|
|
_p3dpython_started = false;
|
|
}
|
|
|
|
// If there are any leftover commands in the queue (presumably implying we
|
|
// have never started the python process), then delete them now, unsent.
|
|
Commands::iterator ci;
|
|
for (ci = _commands.begin(); ci != _commands.end(); ++ci) {
|
|
delete (*ci);
|
|
}
|
|
_commands.clear();
|
|
|
|
join_read_thread();
|
|
|
|
// Close the pipe now.
|
|
_pipe_read.close();
|
|
|
|
// If we had an exit status, pass it on to the runtime process.
|
|
if (exit_code != 0) {
|
|
_exit(exit_code);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the indicated instance to the session, and starts it running. It is
|
|
* an error if the instance has been started anywhere else.
|
|
*
|
|
* The instance must have the same session_key as the one that was passed to
|
|
* the P3DSession constructor.
|
|
*/
|
|
void P3DSession::
|
|
start_instance(P3DInstance *inst) {
|
|
assert(inst->_session == nullptr);
|
|
assert(inst->get_session_key() == _session_key);
|
|
if (_failed) {
|
|
inst->set_failed();
|
|
return;
|
|
}
|
|
|
|
inst->ref();
|
|
ACQUIRE_LOCK(_instances_lock);
|
|
inst->_session = this;
|
|
inst->_instance_started = true;
|
|
bool inserted = _instances.insert(Instances::value_type(inst->get_instance_id(), inst)).second;
|
|
RELEASE_LOCK(_instances_lock);
|
|
assert(inserted);
|
|
|
|
TiXmlDocument *doc = new TiXmlDocument;
|
|
TiXmlElement *xcommand = new TiXmlElement("command");
|
|
xcommand->SetAttribute("cmd", "start_instance");
|
|
TiXmlElement *xinstance = inst->make_xml();
|
|
|
|
doc->LinkEndChild(xcommand);
|
|
xcommand->LinkEndChild(xinstance);
|
|
|
|
send_command(doc);
|
|
inst->send_browser_script_object();
|
|
|
|
// We shouldn't have gotten here unless the instance is fully downloaded and
|
|
// ready to start.
|
|
assert(inst->get_packages_ready());
|
|
|
|
start_p3dpython(inst);
|
|
}
|
|
|
|
/**
|
|
* Removes the indicated instance from the session, and stops it. It is an
|
|
* error if the instance is not already running on this session.
|
|
*/
|
|
void P3DSession::
|
|
terminate_instance(P3DInstance *inst) {
|
|
TiXmlDocument *doc = new TiXmlDocument;
|
|
TiXmlElement *xcommand = new TiXmlElement("command");
|
|
xcommand->SetAttribute("cmd", "terminate_instance");
|
|
xcommand->SetAttribute("instance_id", inst->get_instance_id());
|
|
|
|
doc->LinkEndChild(xcommand);
|
|
|
|
send_command(doc);
|
|
|
|
ACQUIRE_LOCK(_instances_lock);
|
|
if (inst->_session == this) {
|
|
nout << "Assigning " << inst << "->log_pathname = " << _log_pathname << "\n";
|
|
inst->_log_pathname = _log_pathname;
|
|
inst->_session = nullptr;
|
|
_instances.erase(inst->get_instance_id());
|
|
}
|
|
RELEASE_LOCK(_instances_lock);
|
|
p3d_unref_delete(inst);
|
|
}
|
|
|
|
/**
|
|
* Sends the indicated command to the running Python process. If the process
|
|
* has not yet been started, queues it up until it is ready.
|
|
*
|
|
* The command must be a newly-allocated TiXmlDocument; it will be deleted
|
|
* after it has been delivered to the process.
|
|
*/
|
|
void P3DSession::
|
|
send_command(TiXmlDocument *command) {
|
|
if (_p3dpython_started) {
|
|
// Python is running. Send the command.
|
|
write_xml(_pipe_write, command, nout);
|
|
delete command;
|
|
} else {
|
|
// Python not yet running. Queue up the command instead.
|
|
_commands.push_back(command);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends the indicated command to the running Python process, and waits for a
|
|
* response. Returns the newly-allocated response on success, or NULL on
|
|
* failure.
|
|
*
|
|
* The command must be a newly-allocated TiXmlDocument; it will be deleted
|
|
* after it has been delivered to the process.
|
|
*
|
|
* This will fail if the python process is not running or if it suddenly
|
|
* stops.
|
|
*/
|
|
TiXmlDocument *P3DSession::
|
|
command_and_response(TiXmlDocument *command) {
|
|
if (!_p3dpython_started) {
|
|
return nullptr;
|
|
}
|
|
|
|
P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
|
|
int response_id = inst_mgr->get_unique_id();
|
|
|
|
// Add the "want_response_id" attribute to the toplevel command, so the sub-
|
|
// process knows we'll be waiting for its response.
|
|
TiXmlElement *xcommand = command->FirstChildElement("command");
|
|
assert(xcommand != nullptr);
|
|
xcommand->SetAttribute("want_response_id", response_id);
|
|
|
|
write_xml(_pipe_write, command, nout);
|
|
delete command;
|
|
|
|
// Now block, waiting for the response to be delivered.
|
|
_response_ready.acquire();
|
|
Responses::iterator ri = _responses.find(response_id);
|
|
while (ri == _responses.end()) {
|
|
if (!_p3dpython_running) {
|
|
// Hmm, looks like Python has gone away.
|
|
_response_ready.release();
|
|
return nullptr;
|
|
}
|
|
|
|
// Make sure we bake requests while we are waiting, to process recursive
|
|
// script requests. (The child process might have to wait for us to
|
|
// process some of these before it can fulfill the command we're actually
|
|
// waiting for.)
|
|
|
|
// Release the mutex while we do this, so we can safely call back in
|
|
// recursively.
|
|
_response_ready.release();
|
|
Instances::iterator ii;
|
|
// TODO: should we acquire _instances_lock? Deadlock concerns?
|
|
for (ii = _instances.begin(); ii != _instances.end(); ++ii) {
|
|
P3DInstance *inst = (*ii).second;
|
|
inst->bake_requests();
|
|
}
|
|
_response_ready.acquire();
|
|
|
|
ri = _responses.find(response_id);
|
|
if (ri != _responses.end()) {
|
|
// We got the response we were waiting for while we had the mutex
|
|
// unlocked.
|
|
break;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
// Make sure we process the Windows event loop while we're waiting, or
|
|
// everything that depends on Windows messages within the subprocess will
|
|
// starve, and we could end up with deadlock.
|
|
|
|
// A single call to PeekMessage() appears to be sufficient. This will
|
|
// scan the message queue and deliver messages to the appropriate threads,
|
|
// so that our subprocess can find them. If we don't do this, the
|
|
// messages that come into this parent window will never get delivered to
|
|
// the subprocess, even though somehow the subprocess will know they're
|
|
// coming and will block waiting for them.
|
|
|
|
MSG msg;
|
|
PeekMessage(&msg, nullptr, 0, 0, PM_NOREMOVE | PM_NOYIELD);
|
|
|
|
// We wait with a timeout, so we can go back and spin the event loop some
|
|
// more. On Windows, the timeout needs to be small, so we continue to
|
|
// process windows messages in a timely fashion.
|
|
_response_ready.wait(0.01);
|
|
|
|
#else
|
|
// On other platforms, we shouldn't need a timeout at all--we could just
|
|
// block indefinitely--but we go ahead and put one in anyway, just in case
|
|
// a notification slips past somehow, and also so we can see evidence that
|
|
// we're actively waiting. This timeout doesn't need to be nearly so
|
|
// small, since it's only a "just in case" sort of thing.
|
|
_response_ready.wait(0.5);
|
|
#endif // _WIN32
|
|
|
|
ri = _responses.find(response_id);
|
|
}
|
|
// When we exit the loop, we've found the desired response.
|
|
TiXmlDocument *response = (*ri).second;
|
|
_responses.erase(ri);
|
|
|
|
_response_ready.release();
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Converts the XML representation of the particular object value into a
|
|
* corresponding P3D_object. Returns the object, a new reference.
|
|
*/
|
|
P3D_object *P3DSession::
|
|
xml_to_p3dobj(const TiXmlElement *xvalue) {
|
|
const char *type = xvalue->Attribute("type");
|
|
P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
|
|
|
|
if (strcmp(type, "undefined") == 0) {
|
|
return inst_mgr->new_undefined_object();
|
|
|
|
} else if (strcmp(type, "none") == 0) {
|
|
return inst_mgr->new_none_object();
|
|
|
|
} else if (strcmp(type, "bool") == 0) {
|
|
int value;
|
|
if (xvalue->QueryIntAttribute("value", &value) == TIXML_SUCCESS) {
|
|
return inst_mgr->new_bool_object(value != 0);
|
|
}
|
|
|
|
} else if (strcmp(type, "int") == 0) {
|
|
int value;
|
|
if (xvalue->QueryIntAttribute("value", &value) == TIXML_SUCCESS) {
|
|
return new P3DIntObject(value);
|
|
}
|
|
|
|
} else if (strcmp(type, "float") == 0) {
|
|
double value;
|
|
if (xvalue->QueryDoubleAttribute("value", &value) == TIXML_SUCCESS) {
|
|
return new P3DFloatObject(value);
|
|
}
|
|
|
|
} else if (strcmp(type, "string") == 0) {
|
|
// Using the string form here instead of the char * form, so we don't get
|
|
// tripped up on embedded null characters.
|
|
const string *value = xvalue->Attribute(string("value"));
|
|
if (value != nullptr) {
|
|
return new P3DStringObject(*value);
|
|
}
|
|
|
|
} else if (strcmp(type, "concrete_sequence") == 0) {
|
|
P3DConcreteSequence *obj = new P3DConcreteSequence;
|
|
const TiXmlElement *xitem = xvalue->FirstChildElement("value");
|
|
while (xitem != nullptr) {
|
|
P3D_object *item = xml_to_p3dobj(xitem);
|
|
if (item != nullptr) {
|
|
obj->append(item);
|
|
P3D_OBJECT_DECREF(item);
|
|
}
|
|
xitem = xitem->NextSiblingElement("value");
|
|
}
|
|
return obj;
|
|
|
|
} else if (strcmp(type, "concrete_struct") == 0) {
|
|
P3DConcreteStruct *obj = new P3DConcreteStruct;
|
|
const TiXmlElement *xitem = xvalue->FirstChildElement("value");
|
|
while (xitem != nullptr) {
|
|
const char *key = xitem->Attribute("key");
|
|
if (key != nullptr) {
|
|
P3D_object *item = xml_to_p3dobj(xitem);
|
|
if (item != nullptr) {
|
|
obj->set_property(key, item);
|
|
P3D_OBJECT_DECREF(item);
|
|
}
|
|
}
|
|
xitem = xitem->NextSiblingElement("value");
|
|
}
|
|
return obj;
|
|
|
|
} else if (strcmp(type, "browser") == 0) {
|
|
int object_id;
|
|
if (xvalue->QueryIntAttribute("object_id", &object_id) == TIXML_SUCCESS) {
|
|
SentObjects::iterator si = _sent_objects.find(object_id);
|
|
if (si == _sent_objects.end()) {
|
|
// Hmm, the child process gave us a bogus object ID.
|
|
return inst_mgr->new_undefined_object();
|
|
}
|
|
|
|
P3D_object *obj = (*si).second;
|
|
|
|
P3D_OBJECT_INCREF(obj);
|
|
return obj;
|
|
}
|
|
|
|
} else if (strcmp(type, "python") == 0) {
|
|
int object_id;
|
|
if (xvalue->QueryIntAttribute("object_id", &object_id) == TIXML_SUCCESS) {
|
|
return new P3DPythonObject(this, object_id);
|
|
}
|
|
}
|
|
|
|
// Something went wrong in decoding.
|
|
return inst_mgr->new_undefined_object();
|
|
}
|
|
|
|
/**
|
|
* Allocates and returns a new XML structure corresponding to the indicated
|
|
* value. The supplied P3DObject's reference count is not decremented; the
|
|
* caller remains responsible for decrementing it later.
|
|
*/
|
|
TiXmlElement *P3DSession::
|
|
p3dobj_to_xml(P3D_object *obj) {
|
|
TiXmlElement *xvalue = new TiXmlElement("value");
|
|
|
|
switch (P3D_OBJECT_GET_TYPE(obj)) {
|
|
case P3D_OT_undefined:
|
|
xvalue->SetAttribute("type", "undefined");
|
|
break;
|
|
|
|
case P3D_OT_none:
|
|
xvalue->SetAttribute("type", "none");
|
|
break;
|
|
|
|
case P3D_OT_bool:
|
|
xvalue->SetAttribute("type", "bool");
|
|
xvalue->SetAttribute("value", (int)P3D_OBJECT_GET_BOOL(obj));
|
|
break;
|
|
|
|
case P3D_OT_int:
|
|
xvalue->SetAttribute("type", "int");
|
|
xvalue->SetAttribute("value", P3D_OBJECT_GET_INT(obj));
|
|
break;
|
|
|
|
case P3D_OT_float:
|
|
xvalue->SetAttribute("type", "float");
|
|
xvalue->SetDoubleAttribute("value", P3D_OBJECT_GET_FLOAT(obj));
|
|
break;
|
|
|
|
case P3D_OT_string:
|
|
{
|
|
xvalue->SetAttribute("type", "string");
|
|
int size = P3D_OBJECT_GET_STRING(obj, nullptr, 0);
|
|
char *buffer = new char[size];
|
|
P3D_OBJECT_GET_STRING(obj, buffer, size);
|
|
xvalue->SetAttribute("value", string(buffer, size));
|
|
delete [] buffer;
|
|
}
|
|
break;
|
|
|
|
case P3D_OT_object:
|
|
P3DObject *p3dobj = nullptr;
|
|
if (obj->_class == &P3DObject::_object_class) {
|
|
p3dobj = (P3DObject *)obj;
|
|
}
|
|
|
|
if (p3dobj != nullptr && p3dobj->fill_xml(xvalue, this)) {
|
|
// This object has a specialized XML representation, valid for this
|
|
// particular session. It has already been filled into xvalue.
|
|
|
|
} else {
|
|
// Otherwise, it must a host-provided object, or a Python object from
|
|
// another session; which means we should pass a reference down to this
|
|
// particular object, so the Python process knows to call back up to
|
|
// here to query it.
|
|
|
|
P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
|
|
int object_id = inst_mgr->get_unique_id();
|
|
bool inserted = _sent_objects.insert(SentObjects::value_type(object_id, obj)).second;
|
|
while (!inserted) {
|
|
// Hmm, we must have cycled around the entire int space? Either that,
|
|
// or there's a logic bug somewhere. Assume the former, and keep
|
|
// looking for an empty slot.
|
|
object_id = inst_mgr->get_unique_id();
|
|
inserted = _sent_objects.insert(SentObjects::value_type(object_id, obj)).second;
|
|
}
|
|
|
|
// Now that it's stored in the map, increment its reference count.
|
|
P3D_OBJECT_INCREF(obj);
|
|
|
|
xvalue->SetAttribute("type", "browser");
|
|
xvalue->SetAttribute("object_id", object_id);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return xvalue;
|
|
}
|
|
|
|
/**
|
|
* This is called by the splash window to deliver a windows keyboard message
|
|
* to the Panda process. It will be called in a sub-thread, but that's OK,
|
|
* since write_xml() supports locking.
|
|
*/
|
|
void P3DSession::
|
|
send_windows_message(P3DInstance *inst, unsigned int msg, int wparam, int lparam) {
|
|
if (_p3dpython_started) {
|
|
TiXmlDocument doc;
|
|
TiXmlElement *xcommand = new TiXmlElement("command");
|
|
xcommand->SetAttribute("cmd", "windows_message");
|
|
xcommand->SetAttribute("instance_id", inst->get_instance_id());
|
|
xcommand->SetAttribute("msg", msg);
|
|
xcommand->SetAttribute("wparam", wparam);
|
|
xcommand->SetAttribute("lparam", lparam);
|
|
doc.LinkEndChild(xcommand);
|
|
write_xml(_pipe_write, &doc, nout);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* May be called in any thread to indicate that a new P3D_request is available
|
|
* in the indicated instance.
|
|
*/
|
|
void P3DSession::
|
|
signal_request_ready(P3DInstance *inst) {
|
|
// Since a new request might require baking, we should wake up a blocked
|
|
// command_and_request() process, so the main thread can go back and bake
|
|
// the new request.
|
|
|
|
// Technically, a response isn't really ready now, but we still need the
|
|
// main thread to wake up and look around for a bit.
|
|
_response_ready.acquire();
|
|
_response_ready.notify();
|
|
_response_ready.release();
|
|
}
|
|
|
|
/**
|
|
* If the session is still active, issues the command to the child process to
|
|
* release the indicated PyObject from its table. This is intended to be
|
|
* called strictly by the P3DPythonObject destructor.
|
|
*/
|
|
void P3DSession::
|
|
drop_pyobj(int object_id) {
|
|
if (_p3dpython_started) {
|
|
TiXmlDocument doc;
|
|
TiXmlElement *xcommand = new TiXmlElement("command");
|
|
xcommand->SetAttribute("cmd", "drop_pyobj");
|
|
xcommand->SetAttribute("object_id", object_id);
|
|
doc.LinkEndChild(xcommand);
|
|
write_xml(_pipe_write, &doc, nout);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Responds to a drop_p3dobj message from the child process indicating that a
|
|
* particular P3D_object is no longer being used by the child. This removes
|
|
* the corresponding P3D_object from our tables and decrements its reference
|
|
* count.
|
|
*/
|
|
void P3DSession::
|
|
drop_p3dobj(int object_id) {
|
|
SentObjects::iterator si = _sent_objects.find(object_id);
|
|
if (si != _sent_objects.end()) {
|
|
P3D_object *obj = (*si).second;
|
|
P3D_OBJECT_DECREF(obj);
|
|
_sent_objects.erase(si);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts Python running in a child process.
|
|
*/
|
|
void P3DSession::
|
|
start_p3dpython(P3DInstance *inst) {
|
|
if (_p3dpython_started) {
|
|
// Already started.
|
|
return;
|
|
}
|
|
|
|
if (inst->_panda3d_package == nullptr) {
|
|
nout << "Couldn't start Python: no panda3d dependency.\n";
|
|
set_failed();
|
|
return;
|
|
}
|
|
|
|
P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
|
|
|
|
_python_root_dir = inst->_panda3d_package->get_package_dir();
|
|
replace_slashes(_python_root_dir);
|
|
|
|
// If we're not to be preserving the user's current directory, then we'll
|
|
// need to change to the standard start directory.
|
|
_keep_user_env = false;
|
|
if (inst_mgr->get_trusted_environment() &&
|
|
inst_mgr->get_console_environment() &&
|
|
inst->_keep_user_env) {
|
|
_keep_user_env = true;
|
|
}
|
|
if (!_keep_user_env) {
|
|
_start_dir = inst_mgr->get_start_dir() + inst->get_start_dir_suffix();
|
|
mkdir_complete(_start_dir, nout);
|
|
}
|
|
replace_slashes(_start_dir);
|
|
|
|
// Also make sure the prc directory is present.
|
|
string prc_root = inst_mgr->get_root_dir() + "/prc";
|
|
replace_slashes(prc_root);
|
|
mkdir_complete(prc_root, nout);
|
|
|
|
#ifdef _WIN32
|
|
char sep = ';';
|
|
#else
|
|
char sep = ':';
|
|
#endif // _WIN32
|
|
|
|
// Build up a search path that includes all of the required packages that
|
|
// have already been installed. We build this in reverse order, so that the
|
|
// higher-order packages come first in the list; that allows them to shadow
|
|
// settings in the lower-order packages.
|
|
assert(!inst->_packages.empty());
|
|
string search_path;
|
|
size_t pi = inst->_packages.size() - 1;
|
|
search_path = inst->_packages[pi]->get_package_dir();
|
|
while (pi > 0) {
|
|
--pi;
|
|
search_path += sep;
|
|
search_path += inst->_packages[pi]->get_package_dir();
|
|
}
|
|
|
|
replace_slashes(search_path);
|
|
nout << "Search path is " << search_path << "\n";
|
|
|
|
bool keep_pythonpath = false;
|
|
if (inst->_allow_python_dev) {
|
|
// If "allow_python_dev" is set in the instance's p3d_info.xml, *and* we
|
|
// have keep_pythonpath in the tokens, then we set keep_pythonpath true.
|
|
keep_pythonpath = (inst->get_fparams().lookup_token_int("keep_pythonpath") != 0);
|
|
}
|
|
|
|
string sys_path = search_path;
|
|
string ld_path = search_path;
|
|
string dyld_path = search_path;
|
|
string python_path = search_path;
|
|
string prc_path = prc_root + sep + search_path;
|
|
|
|
string prc_name = inst->get_fparams().lookup_token("prc_name");
|
|
if (prc_name.empty()) {
|
|
prc_name = inst->_prc_name;
|
|
|
|
if (!prc_name.empty()) {
|
|
// If the prc_name is taken from the p3d file (and not from the HTML
|
|
// tokens), then we also append the alt_host name to the prc_name, so
|
|
// that each alt_host variant will run in a different directory.
|
|
string alt_host = inst->get_fparams().lookup_token("alt_host");
|
|
if (!alt_host.empty()) {
|
|
prc_name += "_";
|
|
prc_name += alt_host;
|
|
}
|
|
}
|
|
}
|
|
if (!prc_name.empty()) {
|
|
// Add the prc_name to the path too, even if this directory doesn't
|
|
// actually exist.
|
|
string this_prc_dir = inst_mgr->get_root_dir() + "/prc";
|
|
inst_mgr->append_safe_dir(this_prc_dir, prc_name);
|
|
replace_slashes(this_prc_dir);
|
|
prc_path = this_prc_dir + sep + prc_path;
|
|
}
|
|
|
|
if (keep_pythonpath) {
|
|
// With keep_pythonpath true, we preserve the PYTHONPATH setting from the
|
|
// caller's environment; in fact, we put it in the front. This allows the
|
|
// caller's on-disk Python files to shadow the similar-named files in the
|
|
// p3d file, allowing easy iteration on the code in the p3d file.
|
|
if (get_env(python_path, "PYTHONPATH")) {
|
|
replace_slashes(python_path);
|
|
python_path += sep;
|
|
python_path += search_path;
|
|
}
|
|
|
|
// We also preserve PRC_PATH.
|
|
if (get_env(prc_path, "PRC_PATH") || get_env(prc_path, "PANDA_PRC_PATH")) {
|
|
replace_slashes(prc_path);
|
|
prc_path += sep;
|
|
prc_path += search_path;
|
|
}
|
|
|
|
nout << "keep_pythonpath is true\n"
|
|
<< "PYTHONPATH set to: " << python_path << "\n"
|
|
<< "PRC_PATH set to: " << prc_path << "\n";
|
|
}
|
|
|
|
// Get the name of the executable to run. Ideally, we'll run the executable
|
|
// successfully, in a sub-process; this will in turn load and run the
|
|
// dynamic library. If that fails for some reason, we can fall back to
|
|
// loading and running the library directly.
|
|
_p3dpython_exe = P3D_PLUGIN_P3DPYTHON;
|
|
string p3dpythonw_exe = _p3dpython_exe + "w";
|
|
if (_p3dpython_exe.empty()) {
|
|
// Allow package to override the name of the p3dpython executables.
|
|
const char *p3dpython_name_xconfig = nullptr;
|
|
const char *p3dpythonw_name_xconfig = nullptr;
|
|
const TiXmlElement *panda3d_xconfig = inst->_panda3d_package->get_xconfig();
|
|
if (panda3d_xconfig != nullptr) {
|
|
p3dpython_name_xconfig = panda3d_xconfig->Attribute("p3dpython_name");
|
|
p3dpythonw_name_xconfig = panda3d_xconfig->Attribute("p3dpythonw_name");
|
|
}
|
|
|
|
string p3dpython_name = "p3dpython";
|
|
if (p3dpython_name_xconfig != nullptr) {
|
|
nout << "p3dpython_name from panda3d xconfig: " << p3dpython_name_xconfig << "\n";
|
|
p3dpython_name = p3dpython_name_xconfig;
|
|
}
|
|
|
|
string p3dpythonw_name = p3dpython_name + "w";
|
|
if (p3dpythonw_name_xconfig != nullptr) {
|
|
nout << "p3dpythonw_name from panda3d xconfig: " << p3dpythonw_name_xconfig << "\n";
|
|
p3dpythonw_name = p3dpythonw_name_xconfig;
|
|
}
|
|
|
|
// Build full executable path.
|
|
_p3dpython_exe = _python_root_dir + "/" + p3dpython_name;
|
|
p3dpythonw_exe = _python_root_dir + "/" + p3dpythonw_name;
|
|
#ifdef __APPLE__
|
|
// On OSX, run from the packaged bundle, if it exists.
|
|
string bundle_exe = _python_root_dir + "/P3DPython.app/Contents/MacOS/p3dpython";
|
|
if (access(bundle_exe.c_str(), X_OK) == 0) {
|
|
_p3dpython_exe = bundle_exe;
|
|
}
|
|
#endif
|
|
}
|
|
#ifdef _WIN32
|
|
if (!inst_mgr->get_console_environment()) {
|
|
_p3dpython_exe = p3dpythonw_exe;
|
|
}
|
|
_p3dpython_exe += ".exe";
|
|
#endif
|
|
replace_slashes(_p3dpython_exe);
|
|
nout << "_p3dpython_exe: " << _p3dpython_exe << "\n";
|
|
|
|
// Populate the new process' environment.
|
|
_env = string();
|
|
|
|
if (!_keep_user_env) {
|
|
// Reconstruct an environment just for running the process. Completely
|
|
// replace most of the existing environment variables with our own.
|
|
|
|
// These are the enviroment variables we forward from the current
|
|
// environment, if they are set.
|
|
const char *keep[] = {
|
|
"HOME", "USER",
|
|
#ifdef _WIN32
|
|
"SYSTEMROOT", "USERPROFILE", "COMSPEC",
|
|
"SYSTEMDRIVE", "ALLUSERSPROFILE", "APPDATA", "COMMONPROGRAMFILES",
|
|
"PROGRAMFILES", "WINDIR", "PROGRAMDATA", "USERDOMAIN",
|
|
#else
|
|
"LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG",
|
|
#endif
|
|
#ifdef HAVE_X11
|
|
"DISPLAY", "XAUTHORITY",
|
|
#endif
|
|
nullptr
|
|
};
|
|
for (int ki = 0; keep[ki] != nullptr; ++ki) {
|
|
string value;
|
|
if (get_env(value, keep[ki])) {
|
|
_env += keep[ki];
|
|
_env += "=";
|
|
_env += value;
|
|
_env += '\0';
|
|
}
|
|
}
|
|
|
|
} else {
|
|
// In a trusted environment, when the application asks us to, we forward
|
|
// *all* environment variables, except those defined specifically below.
|
|
const char *dont_keep[] = {
|
|
"PATH", "LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH",
|
|
"PYTHONPATH", "PYTHONHOME", "PRC_PATH", "PANDA_PRC_PATH",
|
|
"TEMP", "CTPROJS",
|
|
nullptr
|
|
};
|
|
|
|
#ifdef _WIN32
|
|
// Windows has a leading underscore in the name, and the word "environ" is
|
|
// a keyword. (!)
|
|
extern char **_environ;
|
|
char **global_environ = _environ;
|
|
#elif defined(__APPLE__)
|
|
// Apple doesn't guarantee that environ is available for shared libraries,
|
|
// but provides _NSGetEnviron().
|
|
char **global_environ = *_NSGetEnviron();
|
|
#else
|
|
// Posix is straightforward.
|
|
extern char **environ;
|
|
char **global_environ = environ;
|
|
#endif // _WIN32
|
|
|
|
char **ep;
|
|
for (ep = global_environ; *ep != nullptr; ++ep) {
|
|
string env = *ep;
|
|
size_t equals = env.find('=');
|
|
if (equals != string::npos) {
|
|
string var = env.substr(0, equals);
|
|
const char *varc = var.c_str();
|
|
bool found = false;
|
|
for (int i = 0; dont_keep[i] != nullptr && !found; ++i) {
|
|
#ifdef _WIN32
|
|
found = (_stricmp(dont_keep[i], varc) == 0);
|
|
#else
|
|
found = (strcmp(dont_keep[i], varc) == 0);
|
|
#endif
|
|
}
|
|
if (!found) {
|
|
// This variable is OK, keep it.
|
|
_env += env;
|
|
_env += '\0';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// We also append the original PATH et al to the *end* of the new
|
|
// definitions, even if keep_user_env is not set. This is necessary for
|
|
// os.system() and such to work as expected within the embedded app. It's
|
|
// also necessary for webbrowser on Linux.
|
|
string orig_path;
|
|
if (get_env(orig_path, "PATH")) {
|
|
sys_path += sep;
|
|
sys_path += orig_path;
|
|
}
|
|
string orig_ld_path;
|
|
if (get_env(orig_ld_path, "LD_LIBRARY_PATH")) {
|
|
ld_path += sep;
|
|
ld_path += orig_ld_path;
|
|
}
|
|
string orig_dyld_path;
|
|
if (get_env(orig_dyld_path, "DYLD_LIBRARY_PATH")) {
|
|
dyld_path += sep;
|
|
dyld_path += orig_dyld_path;
|
|
}
|
|
|
|
// Define some new environment variables.
|
|
_env += "PATH=";
|
|
_env += sys_path;
|
|
_env += '\0';
|
|
|
|
_env += "LD_LIBRARY_PATH=";
|
|
_env += ld_path;
|
|
_env += '\0';
|
|
|
|
_env += "DYLD_LIBRARY_PATH=";
|
|
_env += dyld_path;
|
|
_env += '\0';
|
|
|
|
_env += "PYTHONPATH=";
|
|
_env += python_path;
|
|
_env += '\0';
|
|
|
|
// Let's leave PYTHONHOME empty. Setting it adds junk to our carefully-
|
|
// constructed PYTHONPATH.
|
|
_env += "PYTHONHOME=";
|
|
_env += '\0';
|
|
|
|
_env += "PRC_PATH=";
|
|
_env += prc_path;
|
|
_env += '\0';
|
|
|
|
_env += "PANDA_PRC_PATH=";
|
|
_env += prc_path;
|
|
_env += '\0';
|
|
|
|
_env += "TEMP=";
|
|
string temp_dir = inst_mgr->get_temp_directory();
|
|
replace_slashes(temp_dir);
|
|
_env += temp_dir;
|
|
_env += '\0';
|
|
|
|
// Define each package's root directory in an environment variable named
|
|
// after the package, for the convenience of the packages in setting up
|
|
// their config files.
|
|
for (size_t pi = 0; pi < inst->_packages.size(); ++pi) {
|
|
P3DPackage *package = inst->_packages[pi];
|
|
const string package_name = package->get_package_name();
|
|
for (string::const_iterator si = package_name.begin();
|
|
si != package_name.end();
|
|
++si) {
|
|
_env += toupper(*si);
|
|
}
|
|
_env += string("_ROOT=");
|
|
_env += package->get_package_dir();
|
|
_env += '\0';
|
|
|
|
package->mark_used();
|
|
}
|
|
|
|
// Check for a few tokens that have special meaning at this level.
|
|
bool console_output = (inst->get_fparams().lookup_token_int("console_output") != 0);
|
|
bool one_process = (inst->get_fparams().lookup_token_int("one_process") != 0);
|
|
_interactive_console = (inst->get_fparams().lookup_token_int("interactive_console") != 0);
|
|
|
|
if (!inst->_allow_python_dev) {
|
|
// interactive_console is only allowed to be enabled if allow_python_dev
|
|
// is also set within the p3d file.
|
|
_interactive_console = false;
|
|
}
|
|
|
|
if (_interactive_console) {
|
|
// If we have interactive_console set, it follows we also need
|
|
// console_output.
|
|
console_output = true;
|
|
}
|
|
|
|
// Get the log filename from the HTML tokens, or from the p3d_info.xml file.
|
|
string log_basename = inst->get_fparams().lookup_token("log_basename");
|
|
if (log_basename.empty()) {
|
|
log_basename = inst->_log_basename;
|
|
|
|
if (!log_basename.empty()) {
|
|
// If the log_basename is taken from the p3d file (and not from the HTML
|
|
// tokens), then we also append the alt_host name to the log_basename,
|
|
// so that each alt_host variant will run in a different directory.
|
|
string alt_host = inst->get_fparams().lookup_token("alt_host");
|
|
if (!alt_host.empty()) {
|
|
log_basename += "_";
|
|
log_basename += alt_host;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (log_basename.empty()) {
|
|
#ifdef P3D_PLUGIN_LOG_BASENAME3
|
|
// No log_basename specified for the app; use the compiled-in default.
|
|
log_basename = P3D_PLUGIN_LOG_BASENAME3;
|
|
#endif
|
|
if (log_basename.empty()) {
|
|
log_basename = "p3dsession";
|
|
}
|
|
}
|
|
|
|
// However, it is always written into the log directory only; the user may
|
|
// not override the log file to put it anywhere else.
|
|
size_t slash = log_basename.rfind('/');
|
|
if (slash != string::npos) {
|
|
log_basename = log_basename.substr(slash + 1);
|
|
}
|
|
#ifdef _WIN32
|
|
slash = log_basename.rfind('\\');
|
|
if (slash != string::npos) {
|
|
log_basename = log_basename.substr(slash + 1);
|
|
}
|
|
#endif // _WIN32
|
|
|
|
// Get the log history count from the HTML tokens, or from the p3d_info.xml
|
|
// file.
|
|
int log_history = inst->get_fparams().lookup_token_int("log_history");
|
|
|
|
// Check if we want to keep copies of recent logs on disk.
|
|
if (!log_basename.empty()) {
|
|
// Get a list of all logs on disk
|
|
std::vector<string> all_logs;
|
|
string log_directory = inst_mgr->get_log_directory();
|
|
inst_mgr->scan_directory(log_directory, all_logs);
|
|
|
|
// If keeping logs, only logs with a -timestamp suffix are valid.
|
|
if (log_history > 0) {
|
|
// Remove exact match (no suffix) file, if it is on disk
|
|
string log_exact_leafname = (log_basename + string(".log"));
|
|
for (int i=0; i<(int)all_logs.size(); ++i) {
|
|
if (all_logs[i] == log_exact_leafname) {
|
|
string log_exact_pathname = (log_directory + log_exact_leafname);
|
|
unlink(log_exact_pathname.c_str());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove all but the most recent log_history timestamped logs
|
|
string log_basename_dash = (log_basename + string("-"));
|
|
string log_matching_pathname;
|
|
std::vector<string> matching_logs;
|
|
for (int i=0; i<(int)all_logs.size(); ++i) {
|
|
if ((all_logs[i].size() > 4) &&
|
|
(all_logs[i].find(log_basename_dash) == 0) &&
|
|
(all_logs[i].substr(all_logs[i].size() - 4) == string(".log"))) {
|
|
log_matching_pathname = (log_directory + all_logs[i]);
|
|
matching_logs.push_back(log_matching_pathname);
|
|
}
|
|
}
|
|
for (int i=0; i<(int)matching_logs.size()-log_history; ++i) {
|
|
unlink(matching_logs[i].c_str());
|
|
}
|
|
}
|
|
|
|
// Append a timestamp suffix to the log_basename
|
|
if (!log_basename.empty() && (log_history > 0)) {
|
|
#ifdef _WIN32
|
|
_tzset();
|
|
#else
|
|
tzset();
|
|
#endif
|
|
time_t log_time_seconds = time(nullptr);
|
|
struct tm *log_time_local_p = localtime(&log_time_seconds);
|
|
if (log_time_local_p != nullptr) {
|
|
struct tm log_time_local = *log_time_local_p;
|
|
static const size_t buffer_size = 16;
|
|
char buffer[buffer_size];
|
|
sprintf(buffer, "%02d%02d%02d_%02d%02d%02d",
|
|
(int)(log_time_local.tm_year+1900-2000),
|
|
(int)(log_time_local.tm_mon+1),
|
|
(int)(log_time_local.tm_mday),
|
|
(int)(log_time_local.tm_hour),
|
|
(int)(log_time_local.tm_min),
|
|
(int)(log_time_local.tm_sec));
|
|
log_basename += "-";
|
|
log_basename += buffer;
|
|
}
|
|
}
|
|
|
|
if (!console_output && !log_basename.empty()) {
|
|
_log_pathname = inst_mgr->get_log_directory();
|
|
_log_pathname += log_basename;
|
|
|
|
// We always tack on the extension ".log", to make it even more difficult
|
|
// to overwrite a system file.
|
|
_log_pathname += ".log";
|
|
}
|
|
|
|
// Create the pipes for communication.
|
|
#ifdef _WIN32
|
|
// Create a bi-directional pipe to communicate with the sub-process.
|
|
HANDLE r_to, w_to, r_from, w_from;
|
|
|
|
// Create the pipe to the process.
|
|
if (!CreatePipe(&r_to, &w_to, nullptr, 0)) {
|
|
nout << "failed to create pipe\n";
|
|
set_failed();
|
|
} else {
|
|
// Make sure the right end of the pipe is inheritable.
|
|
SetHandleInformation(r_to, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
|
|
SetHandleInformation(w_to, HANDLE_FLAG_INHERIT, 0);
|
|
}
|
|
|
|
// Create the pipe from the process.
|
|
if (!CreatePipe(&r_from, &w_from, nullptr, 0)) {
|
|
nout << "failed to create pipe\n";
|
|
set_failed();
|
|
} else {
|
|
// Make sure the right end of the pipe is inheritable.
|
|
SetHandleInformation(w_from, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
|
|
SetHandleInformation(r_from, HANDLE_FLAG_INHERIT, 0);
|
|
}
|
|
|
|
_output_handle = w_from;
|
|
_input_handle = r_to;
|
|
_pipe_read.open_read(r_from);
|
|
_pipe_write.open_write(w_to);
|
|
|
|
#else
|
|
// Create a bi-directional pipe to communicate with the sub-process.
|
|
int to_fd[2];
|
|
if (pipe(to_fd) < 0) {
|
|
perror("failed to create pipe");
|
|
set_failed();
|
|
}
|
|
int from_fd[2];
|
|
if (pipe(from_fd) < 0) {
|
|
perror("failed to create pipe");
|
|
set_failed();
|
|
}
|
|
|
|
_input_handle = to_fd[0];
|
|
_output_handle = from_fd[1];
|
|
_pipe_read.open_read(from_fd[0]);
|
|
_pipe_write.open_write(to_fd[1]);
|
|
#endif // _WIN32
|
|
|
|
if (_failed) {
|
|
return;
|
|
}
|
|
|
|
nout << "Setting environment:\n";
|
|
write_env();
|
|
|
|
// Get the filename of the Panda3D multifile. We need to pass this to
|
|
// p3dpython.
|
|
_mf_filename = inst->_panda3d_package->get_archive_file_pathname();
|
|
|
|
nout << "Attempting to start python from " << _p3dpython_exe << "\n";
|
|
|
|
bool started_p3dpython;
|
|
if (one_process) {
|
|
nout << "one_process is set; running Python within parent process.\n";
|
|
started_p3dpython = false;
|
|
} else {
|
|
#ifdef _WIN32
|
|
_p3dpython_handle = win_create_process();
|
|
started_p3dpython = (_p3dpython_handle != INVALID_HANDLE_VALUE);
|
|
#else
|
|
_p3dpython_pid = posix_create_process();
|
|
started_p3dpython = (_p3dpython_pid > 0);
|
|
#endif
|
|
if (!started_p3dpython) {
|
|
nout << "Failed to create process.\n";
|
|
}
|
|
}
|
|
|
|
if (!started_p3dpython) {
|
|
// Well, we couldn't run python in a sub-process, for some reason. Fall
|
|
// back to running it in a sub-thread within the same process. This isn't
|
|
// nearly as good, but I guess it's better than nothing.
|
|
|
|
INIT_THREAD(_p3dpython_thread);
|
|
SPAWN_THREAD(_p3dpython_thread, p3dpython_thread_run, this);
|
|
_p3dpython_one_process = true;
|
|
}
|
|
_p3dpython_started = true;
|
|
_p3dpython_running = true;
|
|
|
|
if (!_pipe_read) {
|
|
nout << "unable to open read pipe\n";
|
|
}
|
|
if (!_pipe_write) {
|
|
nout << "unable to open write pipe\n";
|
|
}
|
|
|
|
spawn_read_thread();
|
|
|
|
// The very first command we send to the process is its session_id.
|
|
TiXmlDocument doc;
|
|
TiXmlElement *xcommand = new TiXmlElement("command");
|
|
xcommand->SetAttribute("cmd", "init");
|
|
xcommand->SetAttribute("session_id", _session_id);
|
|
doc.LinkEndChild(xcommand);
|
|
write_xml(_pipe_write, &doc, nout);
|
|
|
|
// Also feed it any commands we may have queued up from before the process
|
|
// was started.
|
|
Commands::iterator ci;
|
|
for (ci = _commands.begin(); ci != _commands.end(); ++ci) {
|
|
write_xml(_pipe_write, (*ci), nout);
|
|
delete (*ci);
|
|
}
|
|
_commands.clear();
|
|
}
|
|
|
|
/**
|
|
* Sets the "failed" indication to display sadness to the user--we're unable
|
|
* to launch the instance for some reason.
|
|
*
|
|
* When this is called on the P3DSession instead of on a particular
|
|
* P3DInstance, it means that all instances attached to this session are
|
|
* marked failed.
|
|
*/
|
|
void P3DSession::
|
|
set_failed() {
|
|
_failed = true;
|
|
|
|
Instances::iterator ii;
|
|
ACQUIRE_LOCK(_instances_lock);
|
|
for (ii = _instances.begin(); ii != _instances.end(); ++ii) {
|
|
P3DInstance *inst = (*ii).second;
|
|
inst->set_failed();
|
|
}
|
|
RELEASE_LOCK(_instances_lock);
|
|
}
|
|
|
|
/**
|
|
* Starts the read thread. This thread is responsible for reading the
|
|
* standard input socket for XML requests and storing them in the _requests
|
|
* queue.
|
|
*/
|
|
void P3DSession::
|
|
spawn_read_thread() {
|
|
assert(!_started_read_thread && !_read_thread_continue);
|
|
|
|
_read_thread_continue = true;
|
|
SPAWN_THREAD(_read_thread, rt_thread_run, this);
|
|
_started_read_thread = true;
|
|
}
|
|
|
|
/**
|
|
* Waits for the read thread to stop.
|
|
*/
|
|
void P3DSession::
|
|
join_read_thread() {
|
|
if (!_started_read_thread) {
|
|
return;
|
|
}
|
|
|
|
_read_thread_continue = false;
|
|
|
|
JOIN_THREAD(_read_thread);
|
|
_started_read_thread = false;
|
|
}
|
|
|
|
/**
|
|
* Changes the forward slashes to backslashes on Windows. Does nothing on the
|
|
* other platforms.
|
|
*/
|
|
void P3DSession::
|
|
replace_slashes(string &str) {
|
|
#ifdef _WIN32
|
|
// It turns out that some very low-level Windows functions fail when you
|
|
// give them a forward slash instead of a backslash. In particular, Windows
|
|
// fails to load the MSVS runtime DLL's (and their associated manifest
|
|
// files) correctly in this case. So we have to be sure to replace forward
|
|
// slashes in our PATH variable (and other environment variables, for good
|
|
// measure) with backslashes.
|
|
for (size_t i = 0; i < str.length(); ++i) {
|
|
if (str[i] == '/') {
|
|
str[i] = '\\';
|
|
}
|
|
}
|
|
#endif // _WIN32
|
|
}
|
|
|
|
/**
|
|
* The main function for the read thread.
|
|
*/
|
|
void P3DSession::
|
|
rt_thread_run() {
|
|
while (_read_thread_continue) {
|
|
TiXmlDocument *doc = read_xml(_pipe_read, nout);
|
|
if (doc == nullptr) {
|
|
// Some error on reading. Abort.
|
|
rt_terminate();
|
|
_p3dpython_running = false;
|
|
_response_ready.acquire();
|
|
_response_ready.notify();
|
|
_response_ready.release();
|
|
return;
|
|
}
|
|
|
|
// Successfully read an XML document.
|
|
rt_handle_request(doc);
|
|
}
|
|
|
|
nout << "Exiting rt_thread_run in " << this << "\n";
|
|
}
|
|
|
|
/**
|
|
* Processes a single request or notification received from an instance. This
|
|
* method runs in the read thread.
|
|
*/
|
|
void P3DSession::
|
|
rt_handle_request(TiXmlDocument *doc) {
|
|
TiXmlElement *xresponse = doc->FirstChildElement("response");
|
|
if (xresponse != nullptr) {
|
|
int response_id;
|
|
if (xresponse->QueryIntAttribute("response_id", &response_id) == TIXML_SUCCESS) {
|
|
// This is a response to a previous command-and-response. Send it to
|
|
// the parent thread.
|
|
_response_ready.acquire();
|
|
bool inserted = _responses.insert(Responses::value_type(response_id, doc)).second;
|
|
assert(inserted);
|
|
_response_ready.notify();
|
|
_response_ready.release();
|
|
return;
|
|
}
|
|
}
|
|
|
|
TiXmlElement *xrequest = doc->FirstChildElement("request");
|
|
if (xrequest != nullptr) {
|
|
int instance_id;
|
|
if (xrequest->QueryIntAttribute("instance_id", &instance_id) == TIXML_SUCCESS) {
|
|
// Look up the particular instance this is related to.
|
|
ACQUIRE_LOCK(_instances_lock);
|
|
Instances::const_iterator ii;
|
|
ii = _instances.find(instance_id);
|
|
if (ii != _instances.end()) {
|
|
P3DInstance *inst = (*ii).second;
|
|
inst->add_raw_request(doc);
|
|
doc = nullptr;
|
|
}
|
|
RELEASE_LOCK(_instances_lock);
|
|
}
|
|
}
|
|
|
|
if (doc != nullptr) {
|
|
delete doc;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Got a closed pipe from the sub-process. Send a terminate request for all
|
|
* instances.
|
|
*/
|
|
void P3DSession::
|
|
rt_terminate() {
|
|
Instances icopy;
|
|
ACQUIRE_LOCK(_instances_lock);
|
|
icopy = _instances;
|
|
RELEASE_LOCK(_instances_lock);
|
|
|
|
// TODO: got a race condition here. What happens if someone deletes an
|
|
// instance while we're processing this loop?
|
|
|
|
for (Instances::iterator ii = icopy.begin(); ii != icopy.end(); ++ii) {
|
|
P3DInstance *inst = (*ii).second;
|
|
inst->request_stop_main_thread();
|
|
}
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
/**
|
|
* Creates a sub-process to run _p3dpython_exe, with the appropriate command-
|
|
* line arguments, and the environment string defined in _env. Standard error
|
|
* is logged to _log_pathname, if that string is nonempty.
|
|
*
|
|
* Opens the two HandleStreams _pipe_read and _pipe_write as the read and
|
|
* write pipes to the child process's standard output and standard input,
|
|
* respectively.
|
|
*
|
|
* Returns the handle to the created process on success, or
|
|
* INVALID_HANDLE_VALUE on falure.
|
|
*/
|
|
HANDLE P3DSession::
|
|
win_create_process() {
|
|
// Make sure we see an error dialog if there is a missing DLL.
|
|
SetErrorMode(0);
|
|
|
|
// Open the log file.
|
|
HANDLE error_handle = GetStdHandle(STD_ERROR_HANDLE);
|
|
bool got_error_handle = false;
|
|
if (!_log_pathname.empty()) {
|
|
wstring log_pathname_w;
|
|
string_to_wstring(log_pathname_w, _log_pathname);
|
|
HANDLE handle = CreateFileW
|
|
(log_pathname_w.c_str(), GENERIC_WRITE,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
nullptr, CREATE_ALWAYS, 0, nullptr);
|
|
if (handle != INVALID_HANDLE_VALUE) {
|
|
error_handle = handle;
|
|
SetHandleInformation(error_handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
|
|
got_error_handle = true;
|
|
} else {
|
|
nout << "Unable to open " << _log_pathname << "\n";
|
|
}
|
|
}
|
|
|
|
STARTUPINFOW startup_info;
|
|
ZeroMemory(&startup_info, sizeof(startup_info));
|
|
startup_info.cb = sizeof(startup_info);
|
|
|
|
// Set up the IO handles. We send stderr and stdout to our error_handle.
|
|
startup_info.hStdError = error_handle;
|
|
startup_info.hStdOutput = error_handle;
|
|
startup_info.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
|
startup_info.dwFlags |= STARTF_USESTDHANDLES;
|
|
|
|
// We want to show the output window these days.
|
|
startup_info.wShowWindow = SW_SHOW;
|
|
startup_info.dwFlags |= STARTF_USESHOWWINDOW;
|
|
|
|
// If _keep_user_env is true, meaning not to change the current directory,
|
|
// then pass NULL in to CreateProcess(). Otherwise pass in _start_dir.
|
|
const wchar_t *start_dir_cstr;
|
|
wstring start_dir_w;
|
|
if (_keep_user_env) {
|
|
start_dir_cstr = nullptr;
|
|
nout << "Not changing working directory.\n";
|
|
} else {
|
|
string_to_wstring(start_dir_w, _start_dir);
|
|
start_dir_cstr = start_dir_w.c_str();
|
|
nout << "Setting working directory: " << _start_dir << "\n";
|
|
}
|
|
|
|
// Construct the command-line string, containing the quoted command-line
|
|
// arguments.
|
|
std::ostringstream stream;
|
|
stream << "\"" << _p3dpython_exe << "\" \"" << _mf_filename
|
|
<< "\" \"" << _input_handle << "\" \"" << _output_handle
|
|
<< "\" \"" << _interactive_console << "\"";
|
|
|
|
// I'm not sure why CreateProcess wants a non-const char pointer for its
|
|
// command-line string, but I'm not taking chances. It gets a non-const
|
|
// char array that it can modify.
|
|
wstring command_line_str;
|
|
string_to_wstring(command_line_str, stream.str());
|
|
wchar_t *command_line = new wchar_t[command_line_str.size() + 1];
|
|
memcpy(command_line, command_line_str.c_str(), sizeof(wchar_t) * command_line_str.size() + 1);
|
|
|
|
nout << "Command line: " << command_line_str << "\n";
|
|
|
|
wstring p3dpython_exe_w;
|
|
string_to_wstring(p3dpython_exe_w, _p3dpython_exe);
|
|
wstring env_w;
|
|
string_to_wstring(env_w, _env);
|
|
|
|
PROCESS_INFORMATION process_info;
|
|
BOOL result = CreateProcessW
|
|
(p3dpython_exe_w.c_str(), command_line, nullptr, nullptr, TRUE,
|
|
CREATE_UNICODE_ENVIRONMENT, (void *)env_w.c_str(),
|
|
start_dir_cstr, &startup_info, &process_info);
|
|
bool started_program = (result != 0);
|
|
|
|
if (!started_program) {
|
|
nout << "CreateProcess failed, error: " << GetLastError() << "\n";
|
|
}
|
|
|
|
delete[] command_line;
|
|
|
|
// Close the pipe handles that are now owned by the child.
|
|
CloseHandle(_output_handle);
|
|
CloseHandle(_input_handle);
|
|
if (got_error_handle) {
|
|
CloseHandle(error_handle);
|
|
}
|
|
|
|
if (!started_program) {
|
|
_pipe_read.close();
|
|
_pipe_write.close();
|
|
return INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
CloseHandle(process_info.hThread);
|
|
return process_info.hProcess;
|
|
}
|
|
#endif // _WIN32
|
|
|
|
|
|
#ifndef _WIN32
|
|
/**
|
|
* Creates a sub-process to run _p3dpython_exe, with the appropriate command-
|
|
* line arguments, and the environment string defined in _env. Standard error
|
|
* is logged to _log_pathname, if that string is nonempty.
|
|
*
|
|
* Opens the two HandleStreams _pipe_read and _pipe_write as the read and
|
|
* write pipes to the child process's standard output and standard input,
|
|
* respectively.
|
|
*
|
|
* Returns the pid of the created process on success, or -1 on falure.
|
|
*/
|
|
int P3DSession::
|
|
posix_create_process() {
|
|
// If the program file doesn't exist or isn't executable, don't even bother
|
|
// to try.
|
|
if (access(_p3dpython_exe.c_str(), X_OK) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
// Fork and exec.
|
|
pid_t child = fork();
|
|
if (child < 0) {
|
|
perror("fork");
|
|
return -1;
|
|
}
|
|
|
|
if (child == 0) {
|
|
// Here we are in the child process.
|
|
|
|
// Close the parent's ends of the pipes.
|
|
_pipe_read.close();
|
|
_pipe_write.close();
|
|
|
|
if (!_log_pathname.empty()) {
|
|
// Open a logfile.
|
|
int logfile_fd = open(_log_pathname.c_str(),
|
|
O_WRONLY | O_CREAT | O_TRUNC, 0666);
|
|
if (logfile_fd < 0) {
|
|
nout << "Unable to open " << _log_pathname << "\n";
|
|
} else {
|
|
// Redirect stderr and stdout onto our logfile.
|
|
dup2(logfile_fd, STDERR_FILENO);
|
|
dup2(logfile_fd, STDOUT_FILENO);
|
|
close(logfile_fd);
|
|
}
|
|
}
|
|
|
|
if (_keep_user_env) {
|
|
nout << "Not changing working directory.\n";
|
|
} else {
|
|
nout << "Setting working directory: " << _start_dir << "\n";
|
|
if (chdir(_start_dir.c_str()) < 0) {
|
|
nout << "Could not chdir to " << _start_dir << "\n";
|
|
// This is a warning, not an error. We don't actually care that much
|
|
// about the starting directory.
|
|
}
|
|
}
|
|
|
|
// build up an array of char strings for the environment.
|
|
std::vector<const char *> ptrs;
|
|
size_t p = 0;
|
|
size_t zero = _env.find('\0', p);
|
|
while (zero != string::npos) {
|
|
ptrs.push_back(_env.data() + p);
|
|
p = zero + 1;
|
|
zero = _env.find('\0', p);
|
|
}
|
|
ptrs.push_back(nullptr);
|
|
|
|
std::stringstream input_handle_stream;
|
|
input_handle_stream << _input_handle;
|
|
string input_handle_str = input_handle_stream.str();
|
|
|
|
std::stringstream output_handle_stream;
|
|
output_handle_stream << _output_handle;
|
|
string output_handle_str = output_handle_stream.str();
|
|
|
|
execle(_p3dpython_exe.c_str(), _p3dpython_exe.c_str(),
|
|
_mf_filename.c_str(), input_handle_str.c_str(),
|
|
output_handle_str.c_str(),
|
|
_interactive_console ? "1" : "0", (char *)0, &ptrs[0]);
|
|
nout << "Failed to exec " << _p3dpython_exe << "\n";
|
|
_exit(1);
|
|
}
|
|
|
|
// Close the handles that are now owned by the child.
|
|
close(_input_handle);
|
|
close(_output_handle);
|
|
|
|
// Let's wait a few milliseconds and see if the child is going to
|
|
// immediately exit with a failure status. This isn't 100% reliable, but
|
|
// it's a lot easier than sending back a "yes I successfully started the
|
|
// program" message. Maybe I'll put in the more reliable test later.
|
|
|
|
struct timeval start;
|
|
gettimeofday(&start, nullptr);
|
|
int start_ms = start.tv_sec * 1000 + start.tv_usec / 1000;
|
|
|
|
int status;
|
|
pid_t result = waitpid(child, &status, WNOHANG);
|
|
while (result != child) {
|
|
if (result == -1) {
|
|
perror("waitpid");
|
|
break;
|
|
}
|
|
|
|
struct timeval now;
|
|
gettimeofday(&now, nullptr);
|
|
int now_ms = now.tv_sec * 1000 + now.tv_usec / 1000;
|
|
int elapsed = now_ms - start_ms;
|
|
if (elapsed > 100) {
|
|
// OK, we've waited, and the child process is still alive. Assume it
|
|
// will stay that way.
|
|
nout << "child still alive after " << elapsed << " ms\n";
|
|
return child;
|
|
}
|
|
|
|
// Yield the timeslice and wait some more.
|
|
struct timeval tv;
|
|
tv.tv_sec = 0;
|
|
tv.tv_usec = 1;
|
|
select(0, nullptr, nullptr, nullptr, &tv);
|
|
result = waitpid(child, &status, WNOHANG);
|
|
}
|
|
|
|
// The child process died for some reason; maybe it couldn't exec() its
|
|
// process. Report an error condition.
|
|
nout << "Python process stopped immediately.\n";
|
|
if (WIFEXITED(status)) {
|
|
int code = WEXITSTATUS(status);
|
|
|
|
nout << " exited normally, status = " << code << "\n";
|
|
if (code != 0) {
|
|
_exit(code);
|
|
}
|
|
|
|
} else if (WIFSIGNALED(status)) {
|
|
nout << " signalled by " << WTERMSIG(status) << ", core = "
|
|
<< WCOREDUMP(status) << "\n";
|
|
|
|
// This seems to be a popular shell convention.
|
|
_exit(128 + WTERMSIG(status));
|
|
|
|
} else if (WIFSTOPPED(status)) {
|
|
nout << " stopped by " << WSTOPSIG(status) << "\n";
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
#endif // _WIN32
|
|
|
|
/**
|
|
* This method is called in a sub-thread to fire up p3dpython within this same
|
|
* process, but only if the above attempt to create a sub-process failed.
|
|
*/
|
|
void P3DSession::
|
|
p3dpython_thread_run() {
|
|
nout << "running p3dpython_thread_run()\n";
|
|
|
|
// Set the environment. Hopefully this won't be too destructive to the
|
|
// current process.
|
|
|
|
// Note that on OSX at least, changing the DYLD_LIBRARY_PATH after the
|
|
// process has started has no effect (and furthermore you can't specify a
|
|
// full path to dlopen() calls), so this whole one-process approach is
|
|
// fatally flawed on OSX.
|
|
size_t p = 0;
|
|
size_t zero = _env.find('\0', p);
|
|
while (zero != string::npos) {
|
|
const char *start = _env.data() + p;
|
|
#ifdef _WIN32
|
|
_putenv(start);
|
|
#else
|
|
const char *equals = strchr(start, '=');
|
|
if (equals != nullptr) {
|
|
string variable(start, equals - start);
|
|
setenv(variable.c_str(), equals + 1, true);
|
|
}
|
|
#endif // _WIN32
|
|
p = zero + 1;
|
|
zero = _env.find('\0', p);
|
|
}
|
|
|
|
// Now load the library.
|
|
string libp3dpython = _python_root_dir + "/libp3dpython";
|
|
#ifdef _WIN32
|
|
#ifdef _DEBUG
|
|
libp3dpython += "_d.dll";
|
|
#else
|
|
libp3dpython += ".dll";
|
|
#endif
|
|
SetErrorMode(0);
|
|
HMODULE module = LoadLibrary(libp3dpython.c_str());
|
|
if (module == nullptr) {
|
|
// Couldn't load the DLL.
|
|
nout << "Couldn't load " << libp3dpython << "\n";
|
|
return;
|
|
}
|
|
|
|
#define get_func GetProcAddress
|
|
|
|
#else // _WIN32
|
|
// Posix case.
|
|
#ifdef __APPLE__
|
|
libp3dpython += ".dylib";
|
|
#else
|
|
libp3dpython += ".so";
|
|
#endif
|
|
void *module = dlopen(libp3dpython.c_str(), RTLD_LAZY | RTLD_LOCAL);
|
|
if (module == nullptr) {
|
|
// Couldn't load the .so.
|
|
nout << "Couldn't load " << libp3dpython << "\n";
|
|
return;
|
|
}
|
|
|
|
#define get_func dlsym
|
|
|
|
#endif // _WIN32
|
|
|
|
run_p3dpython_func *run_p3dpython = (run_p3dpython_func *)get_func(module, "run_p3dpython");
|
|
if (run_p3dpython == nullptr) {
|
|
nout << "Couldn't find run_p3dpython\n";
|
|
return;
|
|
}
|
|
|
|
int status = run_p3dpython(libp3dpython.c_str(), _mf_filename.c_str(),
|
|
_input_handle, _output_handle, _log_pathname.c_str(),
|
|
_interactive_console);
|
|
if (status != 0) {
|
|
nout << "Failure on startup.\n";
|
|
_exit(status);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements getenv(), respecting Windows' Unicode environment. Returns true
|
|
* if the variable is defined, false if it is not. If it is defined, fills
|
|
* value with its definition.
|
|
*/
|
|
bool P3DSession::
|
|
get_env(string &value, const string &varname) {
|
|
#ifdef _WIN32
|
|
wstring varname_w;
|
|
string_to_wstring(varname_w, varname);
|
|
const wchar_t *vc = _wgetenv(varname_w.c_str());
|
|
if (vc == nullptr) {
|
|
return false;
|
|
}
|
|
wstring_to_string(value, vc);
|
|
return true;
|
|
#else // _WIN32
|
|
const char *vc = getenv(varname.c_str());
|
|
if (vc == nullptr) {
|
|
return false;
|
|
}
|
|
value = vc;
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Writes _env, which is formatted as a string containing zero-byte-terminated
|
|
* environment defintions, to the nout stream, one definition per line.
|
|
*/
|
|
void P3DSession::
|
|
write_env() const {
|
|
size_t p = 0;
|
|
size_t zero = _env.find('\0', p);
|
|
while (zero != string::npos) {
|
|
nout << " ";
|
|
nout.write(_env.data() + p, zero - p);
|
|
nout << "\n";
|
|
p = zero + 1;
|
|
zero = _env.find('\0', p);
|
|
}
|
|
}
|