panda3d/direct/src/plugin/p3dSession.cxx

1457 lines
46 KiB
C++

// Filename: p3dSession.cxx
// Created by: drose (03Jun09)
//
////////////////////////////////////////////////////////////////////
//
// 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."
//
////////////////////////////////////////////////////////////////////
#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 "run_p3dpython.h"
#include <ctype.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
////////////////////////////////////////////////////////////////////
// Function: P3DSession::Constructor
// Access: Public
// Description: 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();
_start_dir = inst_mgr->get_root_dir() + "/start";
_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);
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::Destructor
// Access: Public
// Description:
////////////////////////////////////////////////////////////////////
P3DSession::
~P3DSession() {
assert(!_p3dpython_running);
DESTROY_LOCK(_instances_lock);
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::shutdown
// Access: Public
// Description: Terminates the session by shutting down Python and
// stopping the subprocess.
////////////////////////////////////////////////////////////////////
void P3DSession::
shutdown() {
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.
if (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);
}
CloseHandle(_p3dpython_handle);
#else // _WIN32
// Wait for a certain amount of time for the process to stop by
// itself.
struct timeval start;
gettimeofday(&start, NULL);
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, NULL);
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, NULL, NULL, NULL, &tv);
result = waitpid(_p3dpython_pid, &status, WNOHANG);
}
nout << "Python process has successfully stopped.\n";
if (WIFEXITED(status)) {
nout << " exited normally, status = "
<< WEXITSTATUS(status) << "\n";
} else if (WIFSIGNALED(status)) {
nout << " signalled by " << WTERMSIG(status) << ", core = "
<< WCOREDUMP(status) << "\n";
} 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();
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::start_instance
// Access: Public
// Description: 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 == NULL);
assert(inst->get_session_key() == _session_key);
inst->ref();
ACQUIRE_LOCK(_instances_lock);
inst->_session = this;
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);
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::terminate_instance
// Access: Public
// Description: 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) {
inst->_session = NULL;
_instances.erase(inst->get_instance_id());
}
RELEASE_LOCK(_instances_lock);
unref_delete(inst);
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::send_command
// Access: Public
// Description: 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);
}
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::command_and_response
// Access: Public
// Description: 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 NULL;
}
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 != NULL);
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 NULL;
}
// 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;
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, NULL, 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;
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::xml_to_p3dobj
// Access: Public
// Description: 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 != NULL) {
return new P3DStringObject(*value);
}
} else if (strcmp(type, "concrete_sequence") == 0) {
P3DConcreteSequence *obj = new P3DConcreteSequence;
const TiXmlElement *xitem = xvalue->FirstChildElement("value");
while (xitem != NULL) {
P3D_object *item = xml_to_p3dobj(xitem);
if (item != NULL) {
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 != NULL) {
const char *key = xitem->Attribute("key");
if (key != NULL) {
P3D_object *item = xml_to_p3dobj(xitem);
if (item != NULL) {
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();
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::p3dobj_to_xml
// Access: Public
// Description: 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, NULL, 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 = NULL;
if (obj->_class == &P3DObject::_object_class) {
p3dobj = (P3DObject *)obj;
}
if (p3dobj != NULL && 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;
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::signal_request_ready
// Access: Public
// Description: 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();
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::drop_pyobj
// Access: Public
// Description: 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);
}
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::drop_p3dobj
// Access: Public
// Description: 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);
}
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::start_p3dpython
// Access: Private
// Description: Starts Python running in a child process.
////////////////////////////////////////////////////////////////////
void P3DSession::
start_p3dpython(P3DInstance *inst) {
if (_p3dpython_started) {
// Already started.
return;
}
if (inst->_panda3d == NULL) {
nout << "Couldn't start Python: no panda3d dependency.\n";
return;
}
P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
_python_root_dir = inst->_panda3d->get_package_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->_keep_user_env) {
_keep_user_env = true;
}
if (!_keep_user_env) {
mkdir_complete(_start_dir, nout);
}
// Also make sure the prc directory is present.
string prc_root = inst_mgr->get_root_dir() + "/prc";
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();
}
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 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()) {
// Add the prc_name to the path too, even if this directory doesn't
// actually exist.
prc_path = inst_mgr->get_root_dir() + "/" + prc_name + 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.
const char *pypath = getenv("PYTHONPATH");
if (pypath != (char *)NULL) {
python_path = pypath;
python_path += sep;
python_path += search_path;
}
// We also preserve PRC_PATH.
const char *prcpath = getenv("PRC_PATH");
if (prcpath == NULL) {
prcpath = getenv("PANDA_PRC_PATH");
}
if (prcpath != (char *)NULL) {
prc_path = prcpath;
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;
if (_p3dpython_exe.empty()) {
_p3dpython_exe = _python_root_dir + "/p3dpython";
#ifdef _WIN32
_p3dpython_exe += ".exe";
#endif
}
// 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",
#endif
#ifdef HAVE_X11
"DISPLAY",
#endif
NULL
};
for (int ki = 0; keep[ki] != NULL; ++ki) {
char *value = getenv(keep[ki]);
if (value != NULL) {
_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",
NULL
};
#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 != NULL; ++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] != NULL && !found; ++i) {
found = (strcmp(dont_keep[i], varc) == 0);
}
if (!found) {
// This variable is OK, keep it.
_env += env;
_env += '\0';
}
}
}
}
// Define some new environment variables.
_env += "PATH=";
_env += search_path;
_env += '\0';
_env += "LD_LIBRARY_PATH=";
_env += search_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=";
_env += inst_mgr->get_temp_directory();
_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';
}
// 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 p3d_info.xml file.
string log_basename = inst->_log_basename;
// But we also let it be overridden by the tokens.
if (inst->get_fparams().has_token("log_basename")) {
log_basename = inst->get_fparams().lookup_token("log_basename");
}
#ifdef P3D_PLUGIN_LOG_BASENAME3
if (log_basename.empty()) {
// No log_basename specified for the app; use the compiled-in
// default.
log_basename = P3D_PLUGIN_LOG_BASENAME3;
}
#endif
// 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
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, NULL, 0)) {
nout << "failed to create pipe\n";
} 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, NULL, 0)) {
nout << "failed to create pipe\n";
} 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");
}
int from_fd[2];
if (pipe(from_fd) < 0) {
perror("failed to create pipe");
}
_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
// Get the filename of the Panda3D multifile. We need to pass this
// to p3dpython.
_mf_filename = inst->_panda3d->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();
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::spawn_read_thread
// Access: Private
// Description: 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(!_read_thread_continue);
_read_thread_continue = true;
SPAWN_THREAD(_read_thread, rt_thread_run, this);
_started_read_thread = true;
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::join_read_thread
// Access: Private
// Description: 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;
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::rt_thread_run
// Access: Private
// Description: 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 == NULL) {
// 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";
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::rt_handle_request
// Access: Private
// Description: Processes a single request or notification received
// from an instance.
////////////////////////////////////////////////////////////////////
void P3DSession::
rt_handle_request(TiXmlDocument *doc) {
TiXmlElement *xresponse = doc->FirstChildElement("response");
if (xresponse != (TiXmlElement *)NULL) {
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 != (TiXmlElement *)NULL) {
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 = NULL;
}
RELEASE_LOCK(_instances_lock);
}
}
if (doc != NULL) {
delete doc;
}
}
////////////////////////////////////////////////////////////////////
// Function: P3DSession::rt_terminate
// Access: Private
// Description: 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();
}
}
#ifdef _WIN32
////////////////////////////////////////////////////////////////////
// Function: P3DSession::win_create_process
// Access: Private
// Description: 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()) {
HANDLE handle = CreateFile
(_log_pathname.c_str(), GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, CREATE_ALWAYS, 0, NULL);
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";
}
}
STARTUPINFO startup_info;
ZeroMemory(&startup_info, sizeof(STARTUPINFO));
startup_info.cb = sizeof(startup_info);
// Set up the I/O 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;
// Make sure the "python" console window is hidden.
startup_info.wShowWindow = SW_HIDE;
startup_info.dwFlags |= STARTF_USESHOWWINDOW;
// If _keep_user_env, meaning not to change the current directory,
// then pass NULL in to CreateProcess().
const char *start_dir_cstr = NULL;
if (!_keep_user_env) {
start_dir_cstr = _start_dir.c_str();
}
// Construct the command-line string, containing the quoted
// command-line arguments.
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.
string command_line_str = stream.str();
char *command_line = new char[command_line_str.size() + 1];
strcpy(command_line, command_line_str.c_str());
PROCESS_INFORMATION process_info;
BOOL result = CreateProcess
(_p3dpython_exe.c_str(), command_line, NULL, NULL, TRUE, 0,
(void *)_env.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
////////////////////////////////////////////////////////////////////
// Function: P3DSession::posix_create_process
// Access: Private
// Description: 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() {
// 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) {
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.
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((char *)NULL);
stringstream input_handle_stream;
input_handle_stream << _input_handle;
string input_handle_str = input_handle_stream.str();
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);
return child;
}
#endif // _WIN32
////////////////////////////////////////////////////////////////////
// Function: P3DSession::p3dpython_thread_run
// Access: Private
// Description: 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 != NULL) {
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 == NULL) {
// 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 == NULL) {
// 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 == NULL) {
nout << "Couldn't find run_p3dpython\n";
return;
}
if (!run_p3dpython(libp3dpython.c_str(), _mf_filename.c_str(),
_input_handle, _output_handle, _log_pathname.c_str(),
_interactive_console)) {
nout << "Failure on startup.\n";
}
}