Files
FreeCADWasmDev/freecad-source/src/WebAssembly/FreeCADCoreProbe/ShowRuntimeProbe.cpp
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

390 lines
15 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
#include <App/Application.h>
#include <Base/Exception.h>
#include <Python.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <emscripten/emscripten.h>
#include <string>
namespace {
constexpr const char* sourceEntry = "src/Mod/Show/__init__.py";
constexpr const char* sourcePath = "/freecad-mod/Show/__init__.py";
std::string errorMessage;
std::string apiError;
int initializationCalls = 0;
int initializationRuns = 0;
int moduleAvailable = 0;
int apiCount = 0;
int tempoState = -1;
int resourcesReleased = 0;
int dependencyProbe = 0;
std::string familyDispatchResponse;
constexpr const char* sourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47";
std::string pythonError()
{
PyObject* type = nullptr;
PyObject* value = nullptr;
PyObject* traceback = nullptr;
PyErr_Fetch(&type, &value, &traceback);
PyErr_NormalizeException(&type, &value, &traceback);
std::string message = "PythonError";
if (type) {
PyObject* name = PyObject_GetAttrString(type, "__name__");
if (name) {
message = PyUnicode_AsUTF8(name);
Py_DECREF(name);
}
}
if (value) {
PyObject* text = PyObject_Str(value);
if (text) {
message += ": ";
message += PyUnicode_AsUTF8(text);
Py_DECREF(text);
}
}
Py_XDECREF(type);
Py_XDECREF(value);
Py_XDECREF(traceback);
return message;
}
bool addShowPath()
{
const char* script =
"import sys\n"
"_show_path = '/freecad-mod'\n"
"if _show_path not in sys.path:\n"
" sys.path.insert(0, _show_path)\n";
if (PyRun_SimpleString(script) != 0) {
errorMessage = pythonError();
return false;
}
return true;
}
bool exposeGuiFreeFlag()
{
PyObject* freecad = PyImport_ImportModule("FreeCAD");
if (!freecad) {
errorMessage = pythonError();
return false;
}
const int result = PyObject_SetAttrString(freecad, "GuiUp", Py_False);
Py_DECREF(freecad);
if (result != 0) {
errorMessage = pythonError();
return false;
}
return true;
}
bool ensureRuntime()
{
++initializationCalls;
if (moduleAvailable) {
return true;
}
try {
App::Application::initWebCore();
if (!exposeGuiFreeFlag()) {
return false;
}
if (!addShowPath()) {
return false;
}
PyObject* module = PyImport_ImportModule("Show");
if (!module) {
errorMessage = pythonError();
return false;
}
const bool hasTempoVis = PyObject_HasAttrString(module, "TempoVis") == 1;
const bool hasDepGraph = PyObject_HasAttrString(module, "DepGraphTools") == 1;
Py_DECREF(module);
if (!hasTempoVis || !hasDepGraph) {
errorMessage = "registration_mismatch";
return false;
}
moduleAvailable = 1;
++initializationRuns;
resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
errorMessage.clear();
return true;
}
catch (const Base::Exception& error) {
errorMessage = error.what();
}
catch (const std::exception& error) {
errorMessage = error.what();
}
return false;
}
bool readInt(PyObject* globals, const char* name, int& target)
{
PyObject* value = PyDict_GetItemString(globals, name);
if (!value || !PyLong_Check(value)) {
return false;
}
target = static_cast<int>(PyLong_AsLong(value));
return !PyErr_Occurred();
}
bool executeScript(const char* script, PyObject** globalsOut)
{
PyObject* globals = PyDict_New();
PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins());
PyObject* result = PyRun_String(script, Py_file_input, globals, globals);
if (!result) {
errorMessage = pythonError();
Py_DECREF(globals);
return false;
}
Py_DECREF(result);
*globalsOut = globals;
return true;
}
int countApi()
{
PyObject* module = PyImport_ImportModule("Show");
if (!module) {
apiError = pythonError();
return 0;
}
const char* names[] = {"TempoVis", "DepGraphTools"};
int count = 0;
for (const char* name : names) {
count += PyObject_HasAttrString(module, name) ? 1 : 0;
}
Py_DECREF(module);
return count;
}
std::string moduleForSourcePath(const QString& sourcePath)
{
QString module = sourcePath;
if (module.startsWith(QStringLiteral("src/Mod/"))) module = module.mid(8);
if (module.endsWith(QStringLiteral(".py"))) module.chop(3);
module.replace('/', '.');
return module.toStdString();
}
QJsonObject probeFamilyRecord(const QJsonObject& input)
{
const QString apiId = input.value(QStringLiteral("apiId")).toString();
const QString name = input.value(QStringLiteral("name")).toString();
const QString family = input.value(QStringLiteral("family")).toString();
const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString();
QJsonObject result;
result.insert(QStringLiteral("apiId"), apiId);
result.insert(QStringLiteral("name"), name);
result.insert(QStringLiteral("family"), family);
result.insert(QStringLiteral("native"), true);
result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty());
result.insert(QStringLiteral("sourceEntry"), sourcePath);
result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sourceRevision));
result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared"));
result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared"));
if (!apiId.startsWith(QStringLiteral("py:Show:")) || name.isEmpty() || family.isEmpty() || sourcePath.isEmpty()) {
result.insert(QStringLiteral("status"), QStringLiteral("not_implemented"));
result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record"));
result.insert(QStringLiteral("message"), QStringLiteral("Show API record is not part of this module-scoped adapter"));
return result;
}
if (name == QStringLiteral("Workbench")) {
result.insert(QStringLiteral("status"), QStringLiteral("restricted"));
result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable"));
result.insert(QStringLiteral("message"), QStringLiteral("Show Workbench API requires FreeCADGui unavailable in the headless Web runtime"));
result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared"));
result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource"));
return result;
}
const std::string moduleName = moduleForSourcePath(sourcePath);
PyObject* module = PyImport_ImportModule(moduleName.c_str());
if (!module) {
PyErr_Clear();
result.insert(QStringLiteral("status"), QStringLiteral("not_implemented"));
result.insert(QStringLiteral("code"), QStringLiteral("native_python_import_failed"));
result.insert(QStringLiteral("message"), QStringLiteral("Show Python source module could not be imported by the headless runtime"));
result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared"));
return result;
}
bool resolved = false;
if (name.contains(QStringLiteral("."))) {
const auto parts = name.split('.');
if (parts.size() == 2) {
PyObject* owner = PyObject_GetAttrString(module, parts[0].toUtf8().constData());
resolved = owner && PyObject_HasAttrString(owner, parts[1].toUtf8().constData());
Py_XDECREF(owner);
}
} else {
resolved = PyObject_HasAttrString(module, name.toUtf8().constData());
}
Py_DECREF(module);
result.insert(QStringLiteral("status"), resolved ? QStringLiteral("exposed") : QStringLiteral("not_implemented"));
result.insert(QStringLiteral("code"), resolved ? (name.contains('.') ? QStringLiteral("native_object_method") : QStringLiteral("native_python_symbol")) : QStringLiteral("native_python_symbol_probe_failed"));
result.insert(QStringLiteral("message"), resolved ? QStringLiteral("Show Python symbol/method is registered and resolved by FreeCAD") : QStringLiteral("Show Python symbol/method could not be resolved by the headless runtime"));
result.insert(QStringLiteral("parameterSchema"), name.contains('.') ? QStringLiteral("source-declared") : QStringLiteral("source-declared"));
result.insert(QStringLiteral("returnSchema"), name.contains('.') ? QStringLiteral("source-declared") : QStringLiteral("Show.%1").arg(name));
if (resolved) result.insert(QStringLiteral("replay"), name.contains('.') ? QStringLiteral("native-object-method") : QStringLiteral("native-python-reflection"));
return result;
}
} // namespace
extern "C" {
EMSCRIPTEN_KEEPALIVE int fc_web_show_initialize()
{
errorMessage.clear();
return ensureRuntime() ? 1 : 0;
}
EMSCRIPTEN_KEEPALIVE int fc_web_show_initialization_calls() { return initializationCalls; }
EMSCRIPTEN_KEEPALIVE int fc_web_show_initialization_runs() { return initializationRuns; }
EMSCRIPTEN_KEEPALIVE int fc_web_show_module_available() { return moduleAvailable; }
EMSCRIPTEN_KEEPALIVE const char* fc_web_show_last_error() { return errorMessage.c_str(); }
EMSCRIPTEN_KEEPALIVE const char* fc_web_show_source_entry() { return sourceEntry; }
EMSCRIPTEN_KEEPALIVE const char* fc_web_show_source_path() { return sourcePath; }
EMSCRIPTEN_KEEPALIVE int fc_web_show_source_inventory_validate(const char* entry)
{
if (!entry || std::string(entry) != sourceEntry) {
errorMessage = "revision_mismatch";
return 0;
}
return ensureRuntime() ? 1 : 0;
}
EMSCRIPTEN_KEEPALIVE int fc_web_show_python_api_validate(const char* entry)
{
if (!entry || std::string(entry) != sourceEntry) {
apiError = "revision_mismatch";
return 0;
}
if (!ensureRuntime()) {
apiError = errorMessage;
return 0;
}
apiCount = countApi();
apiError.clear();
return apiCount == 2 ? 1 : 0;
}
EMSCRIPTEN_KEEPALIVE int fc_web_show_python_api()
{
if (!ensureRuntime()) {
apiError = errorMessage;
return 0;
}
const char* script =
"import FreeCAD as App\n"
"import Show\n"
"from Show.SceneDetail import SceneDetail\n"
"from Show import Containers, TVStack\n"
"doc = App.newDocument('ShowWeb')\n"
"tv = Show.TempoVis(doc, None, tag='web-fixture')\n"
"_show_tempo_state = tv.state\n"
"_show_api_count = sum(hasattr(Show, name) for name in ('TempoVis', 'DepGraphTools'))\n"
"_show_scene_detail = SceneDetail.__name__\n"
"_show_container = Containers.Container.__name__\n"
"_show_stack = TVStack.TVStack.__name__\n"
"tv.restore()\n"
"App.closeDocument(doc.Name)\n"
"_show_resources_released = len(App.listDocuments()) == 0\n";
PyObject* globals = nullptr;
if (!executeScript(script, &globals)) {
apiError = errorMessage;
return 0;
}
int scriptApiCount = 0;
int released = 0;
const bool valid = readInt(globals, "_show_api_count", scriptApiCount)
&& readInt(globals, "_show_tempo_state", tempoState)
&& readInt(globals, "_show_resources_released", released);
Py_DECREF(globals);
apiCount = countApi();
resourcesReleased = released;
dependencyProbe = valid && scriptApiCount == 2 && tempoState == 0 ? 1 : 0;
apiError.clear();
return valid && apiCount == 2 && scriptApiCount == 2 && tempoState == 0 && resourcesReleased == 1;
}
EMSCRIPTEN_KEEPALIVE int fc_web_show_dependency_probe(const char* name)
{
if (!ensureRuntime()) {
dependencyProbe = 0;
return 0;
}
if (!name || !*name) {
errorMessage = "invalid_argument";
dependencyProbe = 0;
return 0;
}
PyObject* module = PyImport_ImportModule("Show");
if (!module) {
errorMessage = pythonError();
dependencyProbe = 0;
return 0;
}
const bool known = PyObject_HasAttrString(module, name) == 1;
Py_DECREF(module);
if (!known) {
errorMessage = "type_not_found";
dependencyProbe = 0;
return 0;
}
errorMessage.clear();
dependencyProbe = 1;
return 1;
}
EMSCRIPTEN_KEEPALIVE int fc_web_show_python_api_count() { return apiCount; }
EMSCRIPTEN_KEEPALIVE int fc_web_show_tempo_state() { return tempoState; }
EMSCRIPTEN_KEEPALIVE int fc_web_show_resources_released() { return resourcesReleased; }
EMSCRIPTEN_KEEPALIVE int fc_web_show_dependency_available() { return dependencyProbe; }
EMSCRIPTEN_KEEPALIVE const char* fc_web_show_api_error() { return apiError.c_str(); }
EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request)
{
familyDispatchResponse.clear();
if (!request || *request == '\0') { familyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Show native dispatch request is empty"})"; return familyDispatchResponse.c_str(); }
QJsonParseError parseError;
const auto document = QJsonDocument::fromJson(QByteArray(request), &parseError);
if (!document.isObject()) { familyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Show native dispatch requires a JSON object"})"; return familyDispatchResponse.c_str(); }
const auto object = document.object();
const auto command = object.value(QStringLiteral("cmd")).toString();
if (command != QStringLiteral("show.pythonFamilyBatch")) { familyDispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Show native dispatch command is not registered"})"; return familyDispatchResponse.c_str(); }
const auto args = object.value(QStringLiteral("args")).toObject();
const auto tier = args.value(QStringLiteral("tier")).toString();
const auto records = args.value(QStringLiteral("records")).toArray();
if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure")) || records.isEmpty()) { familyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"show.pythonFamilyBatch requires tier=tier-1-readonly or tier=tier-2-pure and non-empty args.records"})"; return familyDispatchResponse.c_str(); }
if (!ensureRuntime()) { QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("runtime_unavailable")); failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); familyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); return familyDispatchResponse.c_str(); }
QJsonArray outputRecords;
for (const auto& value : records) outputRecords.push_back(probeFamilyRecord(value.toObject()));
QJsonObject output;
output.insert(QStringLiteral("ok"), true);
output.insert(QStringLiteral("cmd"), command);
output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-show/python-family-batch/v1"));
output.insert(QStringLiteral("module"), QStringLiteral("Show"));
output.insert(QStringLiteral("tier"), tier);
output.insert(QStringLiteral("source"), QStringLiteral("freecad-source"));
output.insert(QStringLiteral("records"), outputRecords);
output.insert(QStringLiteral("count"), outputRecords.size());
output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty());
familyDispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString();
return familyDispatchResponse.c_str();
}
} // extern "C"