Add CAM and Cloud WebAssembly probes
Some checks failed
FreeCAD master CI / Prepare (push) Has been cancelled
FreeCAD master CI / Pixi (push) Has been cancelled
FreeCAD master CI / Ubuntu (push) Has been cancelled
FreeCAD master CI / Windows (push) Has been cancelled
FreeCAD master CI / Lint (push) Has been cancelled
FreeCAD master CI / WrapUp (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Stale Issues / stale (push) Has been cancelled
Some checks failed
FreeCAD master CI / Prepare (push) Has been cancelled
FreeCAD master CI / Pixi (push) Has been cancelled
FreeCAD master CI / Ubuntu (push) Has been cancelled
FreeCAD master CI / Windows (push) Has been cancelled
FreeCAD master CI / Lint (push) Has been cancelled
FreeCAD master CI / WrapUp (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Stale Issues / stale (push) Has been cancelled
This commit is contained in:
@@ -55,6 +55,8 @@ project(FreeCAD)
|
||||
# block narrows the module graph to the first native-kernel probe.
|
||||
option(FREECAD_WEB "Build the FreeCAD WebAssembly core configuration" OFF)
|
||||
option(FREECAD_WEB_ENABLE_ASSEMBLY "Enable the Assembly Web probe and its dependency closure" OFF)
|
||||
option(FREECAD_WEB_ENABLE_CAM "Enable the CAM Path App Web probe and its dependency closure" OFF)
|
||||
option(FREECAD_WEB_ENABLE_CLOUD "Enable the Cloud App Web probe and its dependency closure" OFF)
|
||||
if(FREECAD_WEB)
|
||||
if(NOT EMSCRIPTEN OR NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
|
||||
message(FATAL_ERROR
|
||||
@@ -108,6 +110,14 @@ if(FREECAD_WEB)
|
||||
list(REMOVE_ITEM _freecad_web_disabled_options
|
||||
BUILD_ASSEMBLY BUILD_SPREADSHEET BUILD_DRAFT BUILD_TECHDRAW)
|
||||
endif()
|
||||
if(FREECAD_WEB_ENABLE_CAM)
|
||||
# CAM's Path App target is GUI-free, but its build graph requires Mesh
|
||||
# in addition to the existing Part and Import Web modules.
|
||||
list(REMOVE_ITEM _freecad_web_disabled_options BUILD_CAM BUILD_MESH)
|
||||
endif()
|
||||
if(FREECAD_WEB_ENABLE_CLOUD)
|
||||
list(REMOVE_ITEM _freecad_web_disabled_options BUILD_CLOUD)
|
||||
endif()
|
||||
foreach(_freecad_web_disabled_option IN LISTS _freecad_web_disabled_options)
|
||||
set(${_freecad_web_disabled_option} OFF CACHE BOOL "Disabled by FREECAD_WEB core configuration" FORCE)
|
||||
endforeach()
|
||||
@@ -121,6 +131,14 @@ if(FREECAD_WEB)
|
||||
set(${_freecad_web_assembly_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_ASSEMBLY" FORCE)
|
||||
endforeach()
|
||||
endif()
|
||||
if(FREECAD_WEB_ENABLE_CAM)
|
||||
foreach(_freecad_web_cam_option BUILD_CAM BUILD_MESH)
|
||||
set(${_freecad_web_cam_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_CAM" FORCE)
|
||||
endforeach()
|
||||
endif()
|
||||
if(FREECAD_WEB_ENABLE_CLOUD)
|
||||
set(BUILD_CLOUD ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_CLOUD" FORCE)
|
||||
endif()
|
||||
|
||||
include(cMake/FreeCADWebDependencies.cmake)
|
||||
endif()
|
||||
|
||||
@@ -127,6 +127,13 @@ if (OCC_FOUND)
|
||||
TKV3d
|
||||
TKRWMesh
|
||||
)
|
||||
if(FREECAD_WEB)
|
||||
# The Web kernel does not build OCCT's MeshVS presentation library.
|
||||
# Part/CAM App targets do not reference MeshVS symbols; keeping this
|
||||
# desktop-only visualisation dependency in the Web OCAF closure makes
|
||||
# otherwise GUI-free probes fail at link time.
|
||||
list(REMOVE_ITEM OCC_OCAF_LIBRARIES TKMeshVS)
|
||||
endif()
|
||||
|
||||
if (OCC_VERSION_STRING VERSION_LESS 7.8.0)
|
||||
list(APPEND OCC_LIBRARIES TKIGES TKSTL TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP)
|
||||
|
||||
@@ -20,6 +20,10 @@ set(FREECAD_WEB_XERCESC_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/xerces-install" CAC
|
||||
"Xerces-C 3.2.4 Emscripten installation prefix")
|
||||
set(FREECAD_WEB_OCCT_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/occt-install" CACHE PATH
|
||||
"OpenCASCADE 7.6.3 Emscripten installation prefix")
|
||||
set(FREECAD_WEB_OPENSSL_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/openssl-install" CACHE PATH
|
||||
"OpenSSL 3.2.2 Emscripten installation prefix")
|
||||
set(FREECAD_WEB_CURL_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/curl-install" CACHE PATH
|
||||
"CURL 8.8.0 Emscripten installation prefix")
|
||||
set(FREECAD_WEB_HOST_PYTHON_EXECUTABLE "" CACHE FILEPATH
|
||||
"Native Python used for configure-time generators in a Web build")
|
||||
|
||||
@@ -49,6 +53,14 @@ set(_freecad_web_xerces_include "${FREECAD_WEB_XERCESC_ROOT}/include")
|
||||
set(_freecad_web_xerces_library "${FREECAD_WEB_XERCESC_ROOT}/lib/libxerces-c.a")
|
||||
set(_freecad_web_occt_dir "${FREECAD_WEB_OCCT_ROOT}/lib/cmake/opencascade")
|
||||
|
||||
if(FREECAD_WEB_ENABLE_CLOUD)
|
||||
set(_freecad_web_openssl_include "${FREECAD_WEB_OPENSSL_ROOT}/include")
|
||||
set(_freecad_web_openssl_crypto "${FREECAD_WEB_OPENSSL_ROOT}/lib/libcrypto.a")
|
||||
set(_freecad_web_openssl_ssl "${FREECAD_WEB_OPENSSL_ROOT}/lib/libssl.a")
|
||||
set(_freecad_web_curl_include "${FREECAD_WEB_CURL_ROOT}/include")
|
||||
set(_freecad_web_curl_library "${FREECAD_WEB_CURL_ROOT}/lib/libcurl.a")
|
||||
endif()
|
||||
|
||||
_freecad_web_require("${_freecad_web_yaml_dir}/yaml-cpp-config.cmake" "yaml-cpp CMake package")
|
||||
_freecad_web_require("${_freecad_web_eigen_include}/signature_of_eigen3_matrix_library" "Eigen Web headers")
|
||||
_freecad_web_require("${_freecad_web_eigen_include}/Eigen/src/Core/util/Macros.h" "Eigen version header")
|
||||
@@ -74,6 +86,13 @@ _freecad_web_require("${_freecad_web_xerces_library}" "Xerces-C Web static libra
|
||||
_freecad_web_require("${_freecad_web_occt_dir}/OpenCASCADEConfig.cmake" "OpenCASCADE Web CMake package")
|
||||
_freecad_web_require("${FREECAD_WEB_OCCT_ROOT}/include/opencascade/Standard_Version.hxx" "OpenCASCADE Web headers")
|
||||
_freecad_web_require("${FREECAD_WEB_OCCT_ROOT}/lib/libTKernel.a" "OpenCASCADE Web foundation library")
|
||||
if(FREECAD_WEB_ENABLE_CLOUD)
|
||||
_freecad_web_require("${_freecad_web_openssl_include}/openssl/opensslv.h" "OpenSSL Web headers")
|
||||
_freecad_web_require("${_freecad_web_openssl_crypto}" "OpenSSL Web crypto static library")
|
||||
_freecad_web_require("${_freecad_web_openssl_ssl}" "OpenSSL Web TLS static library")
|
||||
_freecad_web_require("${_freecad_web_curl_include}/curl/curl.h" "CURL Web headers")
|
||||
_freecad_web_require("${_freecad_web_curl_library}" "CURL Web static library")
|
||||
endif()
|
||||
|
||||
if(NOT FREECAD_WEB_HOST_PYTHON_EXECUTABLE)
|
||||
find_program(_freecad_web_host_python NAMES python3.12 python3)
|
||||
@@ -137,6 +156,27 @@ set(OpenCASCADE_DIR "${_freecad_web_occt_dir}" CACHE PATH
|
||||
"OpenCASCADE Web CMake package" FORCE)
|
||||
set(OCCT_CMAKE_FALLBACK OFF CACHE BOOL "Use the OpenCASCADE Web CMake package" FORCE)
|
||||
|
||||
if(FREECAD_WEB_ENABLE_CLOUD)
|
||||
set(OPENSSL_ROOT_DIR "${FREECAD_WEB_OPENSSL_ROOT}" CACHE PATH
|
||||
"OpenSSL Web installation prefix" FORCE)
|
||||
set(OPENSSL_USE_STATIC_LIBS TRUE CACHE BOOL "Use OpenSSL Web static libraries" FORCE)
|
||||
set(OPENSSL_INCLUDE_DIR "${_freecad_web_openssl_include}" CACHE PATH
|
||||
"OpenSSL Web headers" FORCE)
|
||||
set(OPENSSL_CRYPTO_LIBRARY "${_freecad_web_openssl_crypto}" CACHE FILEPATH
|
||||
"OpenSSL Web crypto static library" FORCE)
|
||||
set(OPENSSL_SSL_LIBRARY "${_freecad_web_openssl_ssl}" CACHE FILEPATH
|
||||
"OpenSSL Web TLS static library" FORCE)
|
||||
|
||||
set(CURL_NO_CURL_CMAKE ON CACHE BOOL "Use CMake's FindCURL for the Web archive" FORCE)
|
||||
set(CURL_USE_STATIC_LIBS ON CACHE BOOL "Use the CURL Web static library" FORCE)
|
||||
set(CURL_INCLUDE_DIR "${_freecad_web_curl_include}" CACHE PATH
|
||||
"CURL Web headers" FORCE)
|
||||
set(CURL_LIBRARY "${_freecad_web_curl_library}" CACHE FILEPATH
|
||||
"CURL Web static library" FORCE)
|
||||
set(CURL_LIBRARY_RELEASE "${_freecad_web_curl_library}" CACHE FILEPATH
|
||||
"CURL Web release static library" FORCE)
|
||||
endif()
|
||||
|
||||
unset(_freecad_web_yaml_dir)
|
||||
unset(_freecad_web_eigen_include)
|
||||
unset(_freecad_web_boost_include)
|
||||
@@ -153,4 +193,9 @@ unset(_freecad_web_zlib_library)
|
||||
unset(_freecad_web_xerces_include)
|
||||
unset(_freecad_web_xerces_library)
|
||||
unset(_freecad_web_occt_dir)
|
||||
unset(_freecad_web_openssl_include)
|
||||
unset(_freecad_web_openssl_crypto)
|
||||
unset(_freecad_web_openssl_ssl)
|
||||
unset(_freecad_web_curl_include)
|
||||
unset(_freecad_web_curl_library)
|
||||
unset(_freecad_web_host_python)
|
||||
|
||||
@@ -18,9 +18,6 @@ include_directories(
|
||||
link_directories(${OCC_LIBRARY_DIR})
|
||||
|
||||
|
||||
include_directories(${pybind11_INCLUDE_DIR})
|
||||
|
||||
|
||||
# this defines the source-files for library
|
||||
set(AREA_SRC_COMMON
|
||||
Arc.cpp
|
||||
@@ -73,6 +70,11 @@ add_library(
|
||||
${PYAREA_SRC}
|
||||
)
|
||||
|
||||
# Keep pybind11's host include path local to the Python shim. Propagating it
|
||||
# to area-native makes Emscripten compile unrelated geometry sources against
|
||||
# the host libc headers.
|
||||
target_include_directories(area PRIVATE ${pybind11_INCLUDE_DIRS})
|
||||
|
||||
|
||||
if(MSVC)
|
||||
set(area_native_LIBS
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <boost/bind/bind.hpp>
|
||||
#include <fastsignals/connection.h>
|
||||
|
||||
#include <App/Application.h>
|
||||
#include <App/Document.h>
|
||||
@@ -1484,7 +1486,11 @@ void Cloud::Module::LinkXSetValue(std::string filename)
|
||||
string newName;
|
||||
Document* currentDoc = GetApplication().getActiveDocument();
|
||||
newName = GetApplication().getUniqueDocumentName("unnamed");
|
||||
newDoc = GetApplication().newDocument(newName.c_str(), (const char*)path.c_str(), true);
|
||||
newDoc = GetApplication().newDocument(
|
||||
newName.c_str(),
|
||||
(const char*)path.c_str(),
|
||||
App::DocumentInitFlags{true, true}
|
||||
);
|
||||
GetApplication().setActiveDocument(newDoc);
|
||||
this->cloudRestore((const char*)path.c_str());
|
||||
GetApplication().setActiveDocument(currentDoc);
|
||||
@@ -1496,7 +1502,9 @@ bool Cloud::Module::cloudRestore(const char* BucketName)
|
||||
Document* doc = GetApplication().getActiveDocument();
|
||||
// clean up if the document is not empty
|
||||
// !TODO: mind exceptions while restoring!
|
||||
doc->signalLinkXsetValue.connect(boost::bind(&Cloud::Module::LinkXSetValue, this, _1));
|
||||
fastsignals::scoped_connection linkConnection = doc->signalLinkXsetValue.connect(
|
||||
boost::bind(&Cloud::Module::LinkXSetValue, this, _1)
|
||||
);
|
||||
|
||||
doc->clearUndos();
|
||||
|
||||
@@ -1553,6 +1561,5 @@ bool Cloud::Module::cloudRestore(const char* BucketName)
|
||||
string uri;
|
||||
uri = this->URL.getStrValue() + ":" + this->TCPPort.getStrValue() + "/" + string(BucketName);
|
||||
doc->FileName.setValue(uri);
|
||||
doc->signalLinkXsetValue.disconnect(boost::bind(&Cloud::Module::LinkXSetValue, this, _1));
|
||||
return (true);
|
||||
}
|
||||
|
||||
@@ -247,6 +247,10 @@ public:
|
||||
{
|
||||
return FileStream;
|
||||
}
|
||||
virtual const std::ostream& Stream(void) const
|
||||
{
|
||||
return FileStream;
|
||||
}
|
||||
virtual bool shouldWrite(const std::string& name, const Base::Persistence* Object) const;
|
||||
void checkText(XERCES_CPP_NAMESPACE_QUALIFIER DOMText* text);
|
||||
void checkXML(XERCES_CPP_NAMESPACE_QUALIFIER DOMNode* node);
|
||||
|
||||
@@ -56,6 +56,14 @@ ELSE()
|
||||
find_package(CURL REQUIRED)
|
||||
ENDIF(WIN32)
|
||||
|
||||
if(FREECAD_WEB)
|
||||
# FindOpenSSL may append host pkg-config transitive flags even when its
|
||||
# primary archive paths are pinned. The Web dependency closure is fully
|
||||
# static and has no host-side dl/thread dependencies.
|
||||
set(OPENSSL_LIBRARIES ${OPENSSL_SSL_LIBRARY} ${OPENSSL_CRYPTO_LIBRARY})
|
||||
set(CURL_LIBRARIES ${CURL_LIBRARY})
|
||||
endif()
|
||||
|
||||
|
||||
if(CURL_FOUND)
|
||||
message(STATUS "CURL has been found\n")
|
||||
|
||||
691
src/WebAssembly/FreeCADCoreProbe/CAMRuntimeProbe.cpp
Normal file
691
src/WebAssembly/FreeCADCoreProbe/CAMRuntimeProbe.cpp
Normal file
@@ -0,0 +1,691 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#include <App/Application.h>
|
||||
#include <App/Document.h>
|
||||
#include <App/DocumentObject.h>
|
||||
#include <Base/Exception.h>
|
||||
#include <Base/FileInfo.h>
|
||||
#include <Base/Type.h>
|
||||
|
||||
#include <Mod/CAM/App/FeaturePath.h>
|
||||
#include <Mod/CAM/App/Path.h>
|
||||
|
||||
#include <Python.h>
|
||||
#include <emscripten/emscripten.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cmath>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
extern "C" PyObject* PyInit_Materials();
|
||||
extern "C" PyObject* PyInit_Part();
|
||||
extern "C" PyObject* PyInit_PathApp();
|
||||
|
||||
namespace {
|
||||
|
||||
std::string lastErrorCode;
|
||||
std::string lastErrorMessage;
|
||||
std::string lastTypeName;
|
||||
unsigned int initializationCalls = 0;
|
||||
unsigned int initializationRuns = 0;
|
||||
bool pathInitialized = false;
|
||||
std::vector<App::DocumentObject*> handles;
|
||||
std::string kernelError;
|
||||
std::string kernelGCode;
|
||||
unsigned int kernelCommandCount = 0;
|
||||
double kernelLength = 0.0;
|
||||
int kernelRecomputeCount = 0;
|
||||
int kernelRecomputeHasError = 0;
|
||||
std::string kernelStage;
|
||||
std::string recoveryErrorCode;
|
||||
std::string recoveryErrorMessage;
|
||||
std::string recoveryBaselineGCode;
|
||||
std::string recoveryFinalGCode;
|
||||
unsigned int recoveryBaselineCommandCount = 0;
|
||||
unsigned int recoveryFinalCommandCount = 0;
|
||||
double recoveryBaselineLength = 0.0;
|
||||
double recoveryFinalLength = 0.0;
|
||||
int recoveryBaselineRecomputeCount = 0;
|
||||
int recoveryFinalRecomputeCount = 0;
|
||||
int recoveryHasError = 0;
|
||||
int recoveryDocumentCountAfterClose = 0;
|
||||
int recoveryUnchangedAfterReject = 0;
|
||||
std::string persistencePath;
|
||||
unsigned int persistenceSize = 0;
|
||||
std::string persistenceSaveError;
|
||||
std::string persistenceOpenError;
|
||||
std::string persistenceStage;
|
||||
int persistenceOpenedDocumentCount = 0;
|
||||
int persistenceOpenedObjectCount = 0;
|
||||
std::string persistenceOpenedObjectType;
|
||||
std::string persistenceOpenedGCode;
|
||||
int persistenceRoundTrip = 0;
|
||||
int persistenceOpenedRecompute = 0;
|
||||
int persistenceOpenedHasError = 0;
|
||||
|
||||
void setError(const char* code, const std::string& message)
|
||||
{
|
||||
lastErrorCode = code;
|
||||
lastErrorMessage = message;
|
||||
}
|
||||
|
||||
void clearError()
|
||||
{
|
||||
lastErrorCode.clear();
|
||||
lastErrorMessage.clear();
|
||||
}
|
||||
|
||||
std::string pythonError(const char* fallback)
|
||||
{
|
||||
if (!PyErr_Occurred()) {
|
||||
return fallback;
|
||||
}
|
||||
PyObject* type = nullptr;
|
||||
PyObject* value = nullptr;
|
||||
PyObject* traceback = nullptr;
|
||||
PyErr_Fetch(&type, &value, &traceback);
|
||||
PyErr_NormalizeException(&type, &value, &traceback);
|
||||
std::string message = fallback;
|
||||
if (value) {
|
||||
PyObject* text = PyObject_Str(value);
|
||||
if (text) {
|
||||
const char* utf8 = PyUnicode_AsUTF8(text);
|
||||
if (utf8 && *utf8) {
|
||||
message += ": ";
|
||||
message += utf8;
|
||||
}
|
||||
Py_DECREF(text);
|
||||
}
|
||||
}
|
||||
Py_XDECREF(type);
|
||||
Py_XDECREF(value);
|
||||
Py_XDECREF(traceback);
|
||||
return message;
|
||||
}
|
||||
|
||||
void addPythonModule(const char* name, PyObject* (*initialize)())
|
||||
{
|
||||
auto* modules = PyImport_GetModuleDict();
|
||||
if (PyDict_GetItemString(modules, name)) {
|
||||
return;
|
||||
}
|
||||
PyObject* module = initialize();
|
||||
if (!module) {
|
||||
throw Base::RuntimeError(pythonError((std::string("PyInit_") + name + " failed").c_str()));
|
||||
}
|
||||
if (PyDict_SetItemString(modules, name, module) != 0) {
|
||||
Py_DECREF(module);
|
||||
throw Base::RuntimeError(std::string("failed to register Python module ") + name);
|
||||
}
|
||||
Py_DECREF(module);
|
||||
}
|
||||
|
||||
void addPythonModuleAlias(const char* alias, const char* moduleName)
|
||||
{
|
||||
auto* modules = PyImport_GetModuleDict();
|
||||
if (PyDict_GetItemString(modules, alias)) {
|
||||
return;
|
||||
}
|
||||
PyObject* module = PyDict_GetItemString(modules, moduleName);
|
||||
if (!module) {
|
||||
throw Base::RuntimeError(std::string("Python module is not registered: ") + moduleName);
|
||||
}
|
||||
if (PyDict_SetItemString(modules, alias, module) != 0) {
|
||||
throw Base::RuntimeError(std::string("failed to register Python module alias ") + alias);
|
||||
}
|
||||
}
|
||||
|
||||
bool ensurePath()
|
||||
{
|
||||
++initializationCalls;
|
||||
if (pathInitialized) {
|
||||
clearError();
|
||||
return true;
|
||||
}
|
||||
|
||||
++initializationRuns;
|
||||
try {
|
||||
App::Application::initWebCore();
|
||||
addPythonModule("Materials", PyInit_Materials);
|
||||
addPythonModule("Part", PyInit_Part);
|
||||
addPythonModule("PathApp", PyInit_PathApp);
|
||||
// Desktop startup exposes PathApp through Path/__init__.py. The
|
||||
// embedded Web probe has no Python package tree, so provide the same
|
||||
// import name without pulling GUI-side CAM modules into the kernel.
|
||||
addPythonModuleAlias("Path", "PathApp");
|
||||
if (Base::Type::fromName("Path::Feature").isBad()) {
|
||||
throw Base::RuntimeError("Path::Feature was not registered");
|
||||
}
|
||||
pathInitialized = true;
|
||||
clearError();
|
||||
return true;
|
||||
}
|
||||
catch (const Base::Exception& error) {
|
||||
setError("initialization_failed", error.what());
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
setError("initialization_failed", error.what());
|
||||
}
|
||||
catch (...) {
|
||||
setError("initialization_failed", "unknown CAM Path initialization failure");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool requireModule(const char* moduleName)
|
||||
{
|
||||
if (!moduleName || *moduleName == '\0') {
|
||||
setError("invalid_argument", "module name is empty");
|
||||
return false;
|
||||
}
|
||||
const std::string name(moduleName);
|
||||
if (name == "PathGui") {
|
||||
setError("gui_dependency_unavailable", "PathGui is unavailable in the GUI-free Web kernel");
|
||||
return false;
|
||||
}
|
||||
if (name != "PathApp") {
|
||||
setError("module_unavailable", "module is not available in the GUI-free Web kernel: " + name);
|
||||
return false;
|
||||
}
|
||||
return ensurePath();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_initialize(const char* moduleName)
|
||||
{
|
||||
return requireModule(moduleName) ? 1 : 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_type_probe(const char* typeName)
|
||||
{
|
||||
if (!requireModule("PathApp")) {
|
||||
return 0;
|
||||
}
|
||||
if (!typeName || *typeName == '\0') {
|
||||
setError("invalid_argument", "type name is empty");
|
||||
return 0;
|
||||
}
|
||||
const Base::Type type = Base::Type::fromName(typeName);
|
||||
if (type.isBad()) {
|
||||
lastTypeName.clear();
|
||||
setError("type_not_found", "type is not registered: " + std::string(typeName));
|
||||
return 0;
|
||||
}
|
||||
lastTypeName = type.getName();
|
||||
clearError();
|
||||
return 1;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_create_handle(const char* typeName)
|
||||
{
|
||||
if (!fc_web_cam_type_probe(typeName)) {
|
||||
return 0;
|
||||
}
|
||||
const Base::Type type = Base::Type::fromName(typeName);
|
||||
if (!type.canInstantiate()) {
|
||||
setError("type_not_instantiable", "type cannot be instantiated: " + std::string(typeName));
|
||||
return 0;
|
||||
}
|
||||
auto* object = static_cast<App::DocumentObject*>(type.createInstance());
|
||||
if (!object) {
|
||||
setError("type_not_instantiable", "type cannot create a DocumentObject: " + std::string(typeName));
|
||||
return 0;
|
||||
}
|
||||
handles.push_back(object);
|
||||
clearError();
|
||||
return static_cast<int>(handles.size());
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_release_handle(int handle)
|
||||
{
|
||||
if (handle <= 0 || static_cast<std::size_t>(handle) > handles.size() || handles[handle - 1] == nullptr) {
|
||||
setError("invalid_handle", "handle is unknown or already released");
|
||||
return 0;
|
||||
}
|
||||
delete handles[handle - 1];
|
||||
handles[handle - 1] = nullptr;
|
||||
clearError();
|
||||
return 1;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_active_handles()
|
||||
{
|
||||
int active = 0;
|
||||
for (const auto* handle : handles) {
|
||||
if (handle) {
|
||||
++active;
|
||||
}
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_error_code()
|
||||
{
|
||||
return lastErrorCode.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_error_message()
|
||||
{
|
||||
return lastErrorMessage.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_last_type()
|
||||
{
|
||||
return lastTypeName.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cam_initialization_calls()
|
||||
{
|
||||
return initializationCalls;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cam_initialization_runs()
|
||||
{
|
||||
return initializationRuns;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_initialized()
|
||||
{
|
||||
return pathInitialized ? 1 : 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_kernel_create_recompute()
|
||||
{
|
||||
App::Document* document = nullptr;
|
||||
kernelError.clear();
|
||||
kernelGCode.clear();
|
||||
kernelCommandCount = 0;
|
||||
kernelLength = 0.0;
|
||||
kernelRecomputeCount = 0;
|
||||
kernelRecomputeHasError = 0;
|
||||
kernelStage.clear();
|
||||
try {
|
||||
if (!ensurePath()) {
|
||||
kernelError = lastErrorMessage;
|
||||
return 0;
|
||||
}
|
||||
auto& application = App::GetApplication();
|
||||
kernelStage = "newDocument";
|
||||
document = application.newDocument("CAMKernelDocument", "CAM Kernel Document", {false, true});
|
||||
if (!document) {
|
||||
kernelError = "Application::newDocument returned null";
|
||||
return 0;
|
||||
}
|
||||
const std::string fixture = "G0 X0 Y0\nG1 X10 Y0 F100\nG1 X10 Y10 F100\n";
|
||||
kernelStage = "setFromGCode";
|
||||
Path::Toolpath path;
|
||||
path.setFromGCode(fixture);
|
||||
kernelStage = "addObject";
|
||||
auto* object = document->addObject<Path::Feature>("Toolpath");
|
||||
if (!object) {
|
||||
kernelError = "Document::addObject returned null";
|
||||
application.closeDocument(document);
|
||||
return 0;
|
||||
}
|
||||
kernelStage = "setPath";
|
||||
object->Path.setValue(path);
|
||||
bool hasError = false;
|
||||
kernelStage = "recompute";
|
||||
kernelRecomputeCount = document->recompute({}, false, &hasError);
|
||||
kernelRecomputeHasError = hasError ? 1 : 0;
|
||||
const Path::Toolpath& storedPath = object->Path.getValue();
|
||||
kernelCommandCount = storedPath.getSize();
|
||||
Path::Toolpath measuredPath(storedPath);
|
||||
kernelLength = measuredPath.getLength();
|
||||
kernelGCode = storedPath.toGCode();
|
||||
const bool valid = kernelRecomputeCount >= 0
|
||||
&& kernelRecomputeHasError == 0
|
||||
&& kernelCommandCount == 3
|
||||
&& kernelLength > 19.99
|
||||
&& kernelLength < 20.01
|
||||
&& !kernelGCode.empty();
|
||||
application.closeDocument(document);
|
||||
document = nullptr;
|
||||
if (!valid) {
|
||||
kernelError = "CAM create/recompute contract mismatch";
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
catch (const Base::Exception& error) {
|
||||
kernelError = kernelStage + ": " + error.what();
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
kernelError = kernelStage + ": " + error.what();
|
||||
}
|
||||
catch (...) {
|
||||
kernelError = kernelStage + ": unknown CAM kernel failure";
|
||||
}
|
||||
if (document) {
|
||||
App::GetApplication().closeDocument(document);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_kernel_error()
|
||||
{
|
||||
return kernelError.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cam_kernel_command_count()
|
||||
{
|
||||
return kernelCommandCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE double fc_web_cam_kernel_length()
|
||||
{
|
||||
return kernelLength;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_kernel_recompute_count()
|
||||
{
|
||||
return kernelRecomputeCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_kernel_recompute_has_error()
|
||||
{
|
||||
return kernelRecomputeHasError;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_kernel_gcode()
|
||||
{
|
||||
return kernelGCode.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_kernel_recovery()
|
||||
{
|
||||
App::Document* document = nullptr;
|
||||
recoveryErrorCode.clear();
|
||||
recoveryErrorMessage.clear();
|
||||
recoveryBaselineGCode.clear();
|
||||
recoveryFinalGCode.clear();
|
||||
recoveryBaselineCommandCount = 0;
|
||||
recoveryFinalCommandCount = 0;
|
||||
recoveryBaselineLength = 0.0;
|
||||
recoveryFinalLength = 0.0;
|
||||
recoveryBaselineRecomputeCount = 0;
|
||||
recoveryFinalRecomputeCount = 0;
|
||||
recoveryHasError = 0;
|
||||
recoveryDocumentCountAfterClose = -1;
|
||||
recoveryUnchangedAfterReject = 0;
|
||||
try {
|
||||
if (!ensurePath()) {
|
||||
recoveryErrorCode = "initialization_failed";
|
||||
recoveryErrorMessage = lastErrorMessage;
|
||||
return 0;
|
||||
}
|
||||
auto& application = App::GetApplication();
|
||||
document = application.newDocument("CAMRecoveryDocument", "CAM Recovery Document", {false, true});
|
||||
if (!document) {
|
||||
recoveryErrorCode = "document_create_failed";
|
||||
recoveryErrorMessage = "Application::newDocument returned null";
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::string fixture = "G0 X0 Y0\nG1 X10 Y0 F100\nG1 X10 Y10 F100\n";
|
||||
Path::Toolpath validPath;
|
||||
validPath.setFromGCode(fixture);
|
||||
auto* object = document->addObject<Path::Feature>("Toolpath");
|
||||
if (!object) {
|
||||
recoveryErrorCode = "object_create_failed";
|
||||
recoveryErrorMessage = "Document::addObject returned null";
|
||||
application.closeDocument(document);
|
||||
document = nullptr;
|
||||
return 0;
|
||||
}
|
||||
object->Path.setValue(validPath);
|
||||
bool baselineHasError = false;
|
||||
recoveryBaselineRecomputeCount = document->recompute({}, false, &baselineHasError);
|
||||
const Path::Toolpath& baseline = object->Path.getValue();
|
||||
recoveryBaselineCommandCount = baseline.getSize();
|
||||
recoveryBaselineLength = Path::Toolpath(baseline).getLength();
|
||||
recoveryBaselineGCode = baseline.toGCode();
|
||||
|
||||
// Parse the invalid fixture into a detached candidate so a rejected
|
||||
// command cannot clear or overwrite the valid document state.
|
||||
try {
|
||||
Path::Toolpath invalidPath;
|
||||
invalidPath.setFromGCode("G1 X");
|
||||
recoveryErrorCode = "invalid_gcode";
|
||||
recoveryErrorMessage = "invalid G-code fixture was accepted";
|
||||
}
|
||||
catch (const Base::Exception& error) {
|
||||
recoveryErrorCode = "invalid_gcode";
|
||||
recoveryErrorMessage = error.what();
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
recoveryErrorCode = "invalid_gcode";
|
||||
recoveryErrorMessage = error.what();
|
||||
}
|
||||
const Path::Toolpath& afterReject = object->Path.getValue();
|
||||
recoveryUnchangedAfterReject = afterReject.toGCode() == recoveryBaselineGCode
|
||||
&& afterReject.getSize() == recoveryBaselineCommandCount
|
||||
&& std::abs(Path::Toolpath(afterReject).getLength() - recoveryBaselineLength) < 1e-9;
|
||||
|
||||
// Reapply the known-good snapshot and recompute, proving recovery is
|
||||
// deterministic after the rejected candidate.
|
||||
object->Path.setValue(validPath);
|
||||
bool finalHasError = false;
|
||||
recoveryFinalRecomputeCount = document->recompute({}, false, &finalHasError);
|
||||
recoveryHasError = finalHasError ? 1 : 0;
|
||||
const Path::Toolpath& finalPath = object->Path.getValue();
|
||||
recoveryFinalCommandCount = finalPath.getSize();
|
||||
recoveryFinalLength = Path::Toolpath(finalPath).getLength();
|
||||
recoveryFinalGCode = finalPath.toGCode();
|
||||
const bool valid = !baselineHasError
|
||||
&& recoveryErrorCode == "invalid_gcode"
|
||||
&& recoveryErrorMessage.find("Badly formatted GCode") != std::string::npos
|
||||
&& recoveryUnchangedAfterReject
|
||||
&& recoveryFinalCommandCount == recoveryBaselineCommandCount
|
||||
&& std::abs(recoveryFinalLength - recoveryBaselineLength) < 1e-9
|
||||
&& recoveryFinalGCode == recoveryBaselineGCode
|
||||
&& !finalHasError;
|
||||
application.closeDocument(document);
|
||||
document = nullptr;
|
||||
recoveryDocumentCountAfterClose = static_cast<int>(application.getDocuments().size());
|
||||
if (!valid) {
|
||||
recoveryErrorCode = "recovery_contract_mismatch";
|
||||
recoveryErrorMessage = "CAM recovery contract mismatch";
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
catch (const Base::Exception& error) {
|
||||
recoveryErrorCode = "recovery_exception";
|
||||
recoveryErrorMessage = error.what();
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
recoveryErrorCode = "recovery_exception";
|
||||
recoveryErrorMessage = error.what();
|
||||
}
|
||||
catch (...) {
|
||||
recoveryErrorCode = "recovery_exception";
|
||||
recoveryErrorMessage = "unknown CAM recovery failure";
|
||||
}
|
||||
if (document) {
|
||||
App::GetApplication().closeDocument(document);
|
||||
recoveryDocumentCountAfterClose = static_cast<int>(App::GetApplication().getDocuments().size());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_recovery_error_code()
|
||||
{
|
||||
return recoveryErrorCode.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_recovery_error_message()
|
||||
{
|
||||
return recoveryErrorMessage.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_recovery_baseline_gcode()
|
||||
{
|
||||
return recoveryBaselineGCode.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_recovery_final_gcode()
|
||||
{
|
||||
return recoveryFinalGCode.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cam_recovery_baseline_command_count()
|
||||
{
|
||||
return recoveryBaselineCommandCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cam_recovery_final_command_count()
|
||||
{
|
||||
return recoveryFinalCommandCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE double fc_web_cam_recovery_baseline_length()
|
||||
{
|
||||
return recoveryBaselineLength;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE double fc_web_cam_recovery_final_length()
|
||||
{
|
||||
return recoveryFinalLength;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_recovery_baseline_recompute_count()
|
||||
{
|
||||
return recoveryBaselineRecomputeCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_recovery_final_recompute_count()
|
||||
{
|
||||
return recoveryFinalRecomputeCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_recovery_has_error()
|
||||
{
|
||||
return recoveryHasError;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_recovery_document_count_after_close()
|
||||
{
|
||||
return recoveryDocumentCountAfterClose;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_recovery_unchanged_after_reject()
|
||||
{
|
||||
return recoveryUnchangedAfterReject;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_kernel_save_reopen()
|
||||
{
|
||||
App::Document* document = nullptr;
|
||||
App::Document* reopened = nullptr;
|
||||
persistencePath = "/tmp/CAMKernel.FCStd";
|
||||
persistenceSize = 0;
|
||||
persistenceSaveError.clear();
|
||||
persistenceOpenError.clear();
|
||||
persistenceStage.clear();
|
||||
persistenceOpenedDocumentCount = 0;
|
||||
persistenceOpenedObjectCount = 0;
|
||||
persistenceOpenedObjectType.clear();
|
||||
persistenceOpenedGCode.clear();
|
||||
persistenceRoundTrip = 0;
|
||||
persistenceOpenedRecompute = 0;
|
||||
persistenceOpenedHasError = 0;
|
||||
try {
|
||||
if (!ensurePath()) {
|
||||
persistenceSaveError = lastErrorMessage;
|
||||
return 0;
|
||||
}
|
||||
auto& application = App::GetApplication();
|
||||
persistenceStage = "create";
|
||||
document = application.newDocument("CAMPersistenceDocument", "CAM Persistence Document", {false, true});
|
||||
if (!document) throw Base::RuntimeError("Application::newDocument returned null");
|
||||
const std::string fixture = "G0 X0 Y0\nG1 X10 Y0 F100\nG1 X10 Y10 F100\n";
|
||||
Path::Toolpath toolpath;
|
||||
toolpath.setFromGCode(fixture);
|
||||
auto* object = document->addObject<Path::Feature>("Toolpath");
|
||||
if (!object) throw Base::RuntimeError("Document::addObject returned null");
|
||||
object->Path.setValue(toolpath);
|
||||
bool hasError = false;
|
||||
persistenceStage = "recompute";
|
||||
const int recompute = document->recompute({}, false, &hasError);
|
||||
persistenceStage = "save";
|
||||
const bool saved = document->saveAs(persistencePath.c_str());
|
||||
Base::FileInfo savedFile(persistencePath);
|
||||
persistenceSize = savedFile.exists() ? savedFile.size() : 0;
|
||||
const bool saveValid = saved && !hasError && recompute >= 0 && persistenceSize > 0 && object->Path.getValue().getSize() == 3;
|
||||
application.closeDocument(document);
|
||||
document = nullptr;
|
||||
if (!saveValid) {
|
||||
persistenceSaveError = "CAM FCStd save contract mismatch";
|
||||
return 0;
|
||||
}
|
||||
|
||||
persistenceStage = "open";
|
||||
reopened = application.newDocument("CAMPersistenceOpened", "CAM Persistence Opened", {false, true});
|
||||
if (!reopened) throw Base::RuntimeError("Application::newDocument returned null for reopen");
|
||||
persistenceStage = "restore";
|
||||
reopened->FileName.setValue(persistencePath);
|
||||
reopened->restore(persistencePath.c_str(), true, {});
|
||||
reopened->afterRestore(true);
|
||||
persistenceOpenedDocumentCount = static_cast<int>(application.getDocuments().size());
|
||||
persistenceOpenedObjectCount = reopened->countObjects();
|
||||
auto* reopenedObject = reopened->getObject("Toolpath");
|
||||
auto* reopenedFeature = dynamic_cast<Path::Feature*>(reopenedObject);
|
||||
if (!reopenedFeature) {
|
||||
persistenceOpenError = "reopened document did not contain Path::Feature";
|
||||
application.closeDocument(reopened);
|
||||
reopened = nullptr;
|
||||
return 0;
|
||||
}
|
||||
persistenceOpenedObjectType = reopenedObject->getTypeId().getName();
|
||||
persistenceOpenedGCode = reopenedFeature->Path.getValue().toGCode();
|
||||
bool reopenedHasError = false;
|
||||
persistenceStage = "recompute-opened";
|
||||
persistenceOpenedRecompute = reopened->recompute({}, false, &reopenedHasError);
|
||||
persistenceOpenedHasError = reopenedHasError ? 1 : 0;
|
||||
persistenceRoundTrip = persistenceOpenedDocumentCount == 1
|
||||
&& persistenceOpenedObjectCount == 1
|
||||
&& persistenceOpenedObjectType == "Path::Feature"
|
||||
&& persistenceOpenedGCode.find("G1") != std::string::npos
|
||||
&& persistenceOpenedRecompute >= 0
|
||||
&& persistenceOpenedHasError == 0;
|
||||
application.closeDocument(reopened);
|
||||
reopened = nullptr;
|
||||
if (!persistenceRoundTrip || application.getDocuments().size() != 0) {
|
||||
persistenceOpenError = "CAM FCStd reopen contract mismatch";
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
catch (const Base::Exception& error) {
|
||||
persistenceOpenError = persistenceStage + ": " + error.what();
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
persistenceOpenError = persistenceStage + ": " + error.what();
|
||||
}
|
||||
catch (...) {
|
||||
persistenceOpenError = persistenceStage + ": unknown CAM persistence failure";
|
||||
}
|
||||
if (document) App::GetApplication().closeDocument(document);
|
||||
if (reopened) App::GetApplication().closeDocument(reopened);
|
||||
return 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_persistence_path() { return persistencePath.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cam_persistence_size() { return persistenceSize; }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_persistence_save_error() { return persistenceSaveError.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_persistence_open_error() { return persistenceOpenError.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_persistence_opened_document_count() { return persistenceOpenedDocumentCount; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_persistence_opened_object_count() { return persistenceOpenedObjectCount; }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_persistence_opened_object_type() { return persistenceOpenedObjectType.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_persistence_opened_gcode() { return persistenceOpenedGCode.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_persistence_round_trip() { return persistenceRoundTrip; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_persistence_opened_recompute() { return persistenceOpenedRecompute; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cam_persistence_opened_has_error() { return persistenceOpenedHasError; }
|
||||
|
||||
}
|
||||
@@ -139,7 +139,7 @@ target_link_options(FreeCADWebAssemblyKernelProbe PRIVATE
|
||||
"-sENVIRONMENT=web,worker,node"
|
||||
"-sUSE_BZIP2=1"
|
||||
"-sDISABLE_EXCEPTION_CATCHING=0"
|
||||
"-sEXPORTED_RUNTIME_METHODS=['ccall','FS']"
|
||||
"-sEXPORTED_RUNTIME_METHODS=['ccall']"
|
||||
"--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings"
|
||||
)
|
||||
set_target_properties(FreeCADWebAssemblyKernelProbe PROPERTIES
|
||||
@@ -147,6 +147,44 @@ set_target_properties(FreeCADWebAssemblyKernelProbe PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
|
||||
)
|
||||
|
||||
if(BUILD_CAM)
|
||||
add_executable(FreeCADWebCAMProbe CAMRuntimeProbe.cpp)
|
||||
target_compile_features(FreeCADWebCAMProbe PRIVATE cxx_std_17)
|
||||
target_compile_options(FreeCADWebCAMProbe PRIVATE "-O2" "-g0" "-fexceptions")
|
||||
target_include_directories(FreeCADWebCAMProbe PRIVATE
|
||||
"${CMAKE_BINARY_DIR}"
|
||||
"${CMAKE_BINARY_DIR}/src"
|
||||
"${CMAKE_SOURCE_DIR}/src"
|
||||
)
|
||||
target_link_libraries(FreeCADWebCAMProbe PRIVATE
|
||||
Path
|
||||
${Python3_LIBRARIES}
|
||||
"${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a"
|
||||
"${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a"
|
||||
"${_freecad_web_python_build}/Modules/expat/libexpat.a"
|
||||
)
|
||||
target_link_options(FreeCADWebCAMProbe PRIVATE
|
||||
"-O2"
|
||||
"-g0"
|
||||
"--no-entry"
|
||||
"-sWASM=1"
|
||||
"-sMODULARIZE=1"
|
||||
"-sEXPORT_ES6=1"
|
||||
"-sEXPORT_NAME=FreeCADWebCAMProbe"
|
||||
"-sALLOW_MEMORY_GROWTH=1"
|
||||
"-sINITIAL_MEMORY=33554432"
|
||||
"-sENVIRONMENT=web,worker,node"
|
||||
"-sUSE_BZIP2=1"
|
||||
"-sDISABLE_EXCEPTION_CATCHING=0"
|
||||
"-sEXPORTED_RUNTIME_METHODS=['ccall','FS']"
|
||||
"--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings"
|
||||
)
|
||||
set_target_properties(FreeCADWebCAMProbe PROPERTIES
|
||||
SUFFIX ".js"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
|
||||
)
|
||||
endif()
|
||||
|
||||
add_executable(FreeCADWebDocumentProbe DocumentProbe.cpp)
|
||||
target_compile_features(FreeCADWebDocumentProbe PRIVATE cxx_std_17)
|
||||
target_compile_options(FreeCADWebDocumentProbe PRIVATE "-O2" "-g0" "-fexceptions")
|
||||
@@ -254,3 +292,77 @@ set_target_properties(FreeCADWebPythonProbe PROPERTIES
|
||||
SUFFIX ".js"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
|
||||
)
|
||||
|
||||
if(BUILD_CLOUD)
|
||||
add_executable(FreeCADWebCloudProbe CloudRuntimeProbe.cpp)
|
||||
target_compile_features(FreeCADWebCloudProbe PRIVATE cxx_std_17)
|
||||
target_compile_options(FreeCADWebCloudProbe PRIVATE "-O2" "-g0" "-fexceptions")
|
||||
target_include_directories(FreeCADWebCloudProbe PRIVATE
|
||||
"${CMAKE_BINARY_DIR}"
|
||||
"${CMAKE_BINARY_DIR}/src"
|
||||
"${CMAKE_SOURCE_DIR}/src"
|
||||
)
|
||||
target_link_libraries(FreeCADWebCloudProbe PRIVATE
|
||||
Cloud
|
||||
${Python3_LIBRARIES}
|
||||
"${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a"
|
||||
"${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a"
|
||||
"${_freecad_web_python_build}/Modules/expat/libexpat.a"
|
||||
)
|
||||
target_link_options(FreeCADWebCloudProbe PRIVATE
|
||||
"-O2"
|
||||
"-g0"
|
||||
"--no-entry"
|
||||
"-sWASM=1"
|
||||
"-sMODULARIZE=1"
|
||||
"-sEXPORT_ES6=1"
|
||||
"-sEXPORT_NAME=FreeCADWebCloudProbe"
|
||||
"-sALLOW_MEMORY_GROWTH=1"
|
||||
"-sINITIAL_MEMORY=33554432"
|
||||
"-sENVIRONMENT=web,worker,node"
|
||||
"-sUSE_BZIP2=1"
|
||||
"-sDISABLE_EXCEPTION_CATCHING=0"
|
||||
"-sEXPORTED_RUNTIME_METHODS=['ccall']"
|
||||
"--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings"
|
||||
)
|
||||
set_target_properties(FreeCADWebCloudProbe PROPERTIES
|
||||
SUFFIX ".js"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
|
||||
)
|
||||
add_executable(FreeCADWebCloudKernelProbe CloudKernelProbe.cpp)
|
||||
target_compile_features(FreeCADWebCloudKernelProbe PRIVATE cxx_std_17)
|
||||
target_compile_options(FreeCADWebCloudKernelProbe PRIVATE "-O2" "-g0" "-fexceptions")
|
||||
target_include_directories(FreeCADWebCloudKernelProbe PRIVATE
|
||||
"${CMAKE_BINARY_DIR}"
|
||||
"${CMAKE_BINARY_DIR}/src"
|
||||
"${CMAKE_SOURCE_DIR}/src"
|
||||
)
|
||||
target_link_libraries(FreeCADWebCloudKernelProbe PRIVATE
|
||||
Cloud
|
||||
Part
|
||||
${Python3_LIBRARIES}
|
||||
"${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a"
|
||||
"${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a"
|
||||
"${_freecad_web_python_build}/Modules/expat/libexpat.a"
|
||||
)
|
||||
target_link_options(FreeCADWebCloudKernelProbe PRIVATE
|
||||
"-O2"
|
||||
"-g0"
|
||||
"--no-entry"
|
||||
"-sWASM=1"
|
||||
"-sMODULARIZE=1"
|
||||
"-sEXPORT_ES6=1"
|
||||
"-sEXPORT_NAME=FreeCADWebCloudKernelProbe"
|
||||
"-sALLOW_MEMORY_GROWTH=1"
|
||||
"-sINITIAL_MEMORY=33554432"
|
||||
"-sENVIRONMENT=web,worker,node"
|
||||
"-sUSE_BZIP2=1"
|
||||
"-sDISABLE_EXCEPTION_CATCHING=0"
|
||||
"-sEXPORTED_RUNTIME_METHODS=['ccall']"
|
||||
"--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings"
|
||||
)
|
||||
set_target_properties(FreeCADWebCloudKernelProbe PROPERTIES
|
||||
SUFFIX ".js"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
|
||||
)
|
||||
endif()
|
||||
|
||||
953
src/WebAssembly/FreeCADCoreProbe/CloudKernelProbe.cpp
Normal file
953
src/WebAssembly/FreeCADCoreProbe/CloudKernelProbe.cpp
Normal file
@@ -0,0 +1,953 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#include <App/Application.h>
|
||||
#include <Base/Exception.h>
|
||||
|
||||
#include <Python.h>
|
||||
#include <emscripten/emscripten.h>
|
||||
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
extern "C" PyObject* PyInit_Cloud();
|
||||
|
||||
namespace {
|
||||
|
||||
std::string lastErrorCode;
|
||||
std::string lastErrorMessage;
|
||||
std::string kernelStage;
|
||||
int moduleHandleBeforeRelease = 0;
|
||||
int moduleHandleAfterRelease = 0;
|
||||
std::string configURL;
|
||||
std::string configTokenAuth;
|
||||
std::string configTokenSecret;
|
||||
std::string configTCPPort;
|
||||
std::string configProtocolVersion;
|
||||
std::string configRegion;
|
||||
std::string invalidErrorCode;
|
||||
std::string invalidErrorMessage;
|
||||
int setterCount = 0;
|
||||
int invalidSetterReturn = 0;
|
||||
int recomputeCount = 0;
|
||||
int recomputeHasError = 0;
|
||||
std::string recomputeSummary;
|
||||
std::string recoveryErrorCode;
|
||||
std::string recoveryErrorField;
|
||||
std::string recoveryErrorMessage;
|
||||
std::string recoveryStateBefore;
|
||||
std::string recoveryStateAfter;
|
||||
std::string recoveryStateAfterReplay;
|
||||
int recoveryInvalidReturn = 0;
|
||||
int recoveryUnchangedAfterReject = 0;
|
||||
int recoveryReplayEqual = 0;
|
||||
int recoveryRecomputeCount = 0;
|
||||
int recoveryHasError = 0;
|
||||
int persistenceApplicable = 0;
|
||||
int persistenceSaveAvailable = 0;
|
||||
int persistenceRestoreAvailable = 0;
|
||||
int persistenceDocumentCountBefore = 0;
|
||||
int persistenceDocumentCountAfter = 0;
|
||||
int persistenceReplayEqual = 0;
|
||||
std::string persistenceReason;
|
||||
std::string persistenceStateBefore;
|
||||
std::string persistenceStateAfter;
|
||||
std::string inspectErrorCode;
|
||||
std::string inspectErrorField;
|
||||
std::string inspectErrorMessage;
|
||||
std::string inspectSourceRevision;
|
||||
std::string inspectInventoryRevision;
|
||||
std::string inspectSummary;
|
||||
int inspectMethodCount = 0;
|
||||
int inspectObjectTypeCount = 0;
|
||||
int inspectFormatExtensionCount = 0;
|
||||
int inspectFormatEvidenceCount = 0;
|
||||
int inspectPythonApiCount = 0;
|
||||
int inspectGuiPythonApiCount = 0;
|
||||
int inspectModuleHandleBeforeRelease = 0;
|
||||
int inspectModuleHandleAfterRelease = 0;
|
||||
int inspectDocumentCountBefore = 0;
|
||||
int inspectDocumentCountAfter = 0;
|
||||
std::string pythonErrorCode;
|
||||
std::string pythonErrorField;
|
||||
std::string pythonErrorMessage;
|
||||
std::string pythonSourceRevision;
|
||||
std::string pythonSummary;
|
||||
int pythonApiCount = 0;
|
||||
int pythonCallableCount = 0;
|
||||
int pythonModuleHandleBeforeRelease = 0;
|
||||
int pythonModuleHandleAfterRelease = 0;
|
||||
int pythonDocumentCountBefore = 0;
|
||||
int pythonDocumentCountAfter = 0;
|
||||
|
||||
constexpr const char* cloudSourceRevision = "b5a086fda59b0325fe0996147e6e9b344414f9bc";
|
||||
constexpr const char* cloudInventoryRevision = "4f4e9244e613ea2df06e70027074a562695048cb";
|
||||
|
||||
void setError(const char* code, const std::string& message)
|
||||
{
|
||||
lastErrorCode = code;
|
||||
lastErrorMessage = message;
|
||||
}
|
||||
|
||||
void clearError()
|
||||
{
|
||||
lastErrorCode.clear();
|
||||
lastErrorMessage.clear();
|
||||
}
|
||||
|
||||
std::string pythonError(const char* fallback)
|
||||
{
|
||||
if (!PyErr_Occurred()) {
|
||||
return fallback;
|
||||
}
|
||||
PyObject* type = nullptr;
|
||||
PyObject* value = nullptr;
|
||||
PyObject* traceback = nullptr;
|
||||
PyErr_Fetch(&type, &value, &traceback);
|
||||
PyErr_NormalizeException(&type, &value, &traceback);
|
||||
std::string message = fallback;
|
||||
if (value) {
|
||||
PyObject* text = PyObject_Str(value);
|
||||
if (text) {
|
||||
const char* utf8 = PyUnicode_AsUTF8(text);
|
||||
if (utf8 && *utf8) {
|
||||
message += ": ";
|
||||
message += utf8;
|
||||
}
|
||||
Py_DECREF(text);
|
||||
}
|
||||
}
|
||||
Py_XDECREF(type);
|
||||
Py_XDECREF(value);
|
||||
Py_XDECREF(traceback);
|
||||
return message;
|
||||
}
|
||||
|
||||
void addCloudModule()
|
||||
{
|
||||
App::Application::initWebCore();
|
||||
auto* modules = PyImport_GetModuleDict();
|
||||
if (PyDict_GetItemString(modules, "Cloud")) {
|
||||
return;
|
||||
}
|
||||
PyObject* module = PyInit_Cloud();
|
||||
if (!module) {
|
||||
throw Base::RuntimeError(pythonError("PyInit_Cloud returned null"));
|
||||
}
|
||||
if (PyDict_SetItemString(modules, "Cloud", module) != 0) {
|
||||
Py_DECREF(module);
|
||||
throw Base::RuntimeError("failed to register Python module Cloud");
|
||||
}
|
||||
Py_DECREF(module);
|
||||
}
|
||||
|
||||
void callSetter(PyObject* module, const char* name, const std::string& value)
|
||||
{
|
||||
PyObject* function = PyObject_GetAttrString(module, name);
|
||||
if (!function) {
|
||||
throw Base::RuntimeError(std::string("Cloud setter is unavailable: ") + name);
|
||||
}
|
||||
PyObject* args = Py_BuildValue("(s)", value.c_str());
|
||||
if (!args) {
|
||||
Py_DECREF(function);
|
||||
throw Base::RuntimeError(std::string("failed to build Cloud setter arguments: ") + name);
|
||||
}
|
||||
PyObject* result = PyObject_CallObject(function, args);
|
||||
Py_DECREF(args);
|
||||
Py_DECREF(function);
|
||||
if (!result) {
|
||||
throw Base::RuntimeError(pythonError((std::string("Cloud setter failed: ") + name).c_str()));
|
||||
}
|
||||
Py_DECREF(result);
|
||||
++setterCount;
|
||||
}
|
||||
|
||||
std::string configurationSummary()
|
||||
{
|
||||
return configURL + "|" + configTokenAuth + "|" + configTokenSecret + "|"
|
||||
+ configTCPPort + "|" + configProtocolVersion + "|" + configRegion;
|
||||
}
|
||||
|
||||
void applyConfiguration(PyObject* module)
|
||||
{
|
||||
callSetter(module, "URL", configURL);
|
||||
callSetter(module, "TokenAuth", configTokenAuth);
|
||||
callSetter(module, "TokenSecret", configTokenSecret);
|
||||
callSetter(module, "TCPPort", configTCPPort);
|
||||
callSetter(module, "ProtocolVersion", configProtocolVersion);
|
||||
callSetter(module, "Region", configRegion);
|
||||
}
|
||||
|
||||
void attemptInvalidURL(PyObject* module)
|
||||
{
|
||||
recoveryInvalidReturn = 0;
|
||||
recoveryErrorCode.clear();
|
||||
recoveryErrorField = "URL";
|
||||
recoveryErrorMessage.clear();
|
||||
PyObject* function = nullptr;
|
||||
PyObject* args = nullptr;
|
||||
try {
|
||||
function = PyObject_GetAttrString(module, "URL");
|
||||
if (!function) {
|
||||
PyErr_Clear();
|
||||
recoveryErrorCode = "invalid_argument";
|
||||
recoveryErrorMessage = "Cloud.URL is unavailable";
|
||||
return;
|
||||
}
|
||||
args = Py_BuildValue("(i)", 42);
|
||||
PyObject* result = args ? PyObject_CallObject(function, args) : nullptr;
|
||||
Py_XDECREF(args);
|
||||
args = nullptr;
|
||||
Py_DECREF(function);
|
||||
function = nullptr;
|
||||
if (result) {
|
||||
Py_DECREF(result);
|
||||
recoveryInvalidReturn = 1;
|
||||
recoveryErrorCode = "invalid_argument";
|
||||
recoveryErrorMessage = "Cloud.URL unexpectedly accepted a non-string value";
|
||||
return;
|
||||
}
|
||||
recoveryErrorCode = "invalid_argument";
|
||||
recoveryErrorMessage = pythonError("Cloud.URL expects a string value");
|
||||
}
|
||||
catch (...) {
|
||||
Py_XDECREF(args);
|
||||
Py_XDECREF(function);
|
||||
PyErr_Clear();
|
||||
recoveryErrorCode = "invalid_argument";
|
||||
recoveryErrorMessage = "Cloud.URL expects a string value";
|
||||
}
|
||||
}
|
||||
|
||||
void probeInvalidSetter(PyObject* module)
|
||||
{
|
||||
invalidSetterReturn = 0;
|
||||
invalidErrorCode.clear();
|
||||
invalidErrorMessage.clear();
|
||||
try {
|
||||
PyObject* function = PyObject_GetAttrString(module, "URL");
|
||||
if (!function) {
|
||||
PyErr_Clear();
|
||||
invalidErrorCode = "invalid_argument";
|
||||
invalidErrorMessage = "Cloud.URL expects one string argument";
|
||||
return;
|
||||
}
|
||||
PyObject* args = PyTuple_New(0);
|
||||
PyObject* result = PyObject_CallObject(function, args);
|
||||
Py_DECREF(args);
|
||||
Py_DECREF(function);
|
||||
if (result) {
|
||||
Py_DECREF(result);
|
||||
invalidSetterReturn = 1;
|
||||
invalidErrorCode = "invalid_argument";
|
||||
invalidErrorMessage = "Cloud.URL unexpectedly accepted an empty argument list";
|
||||
return;
|
||||
}
|
||||
PyErr_Clear();
|
||||
invalidErrorCode = "invalid_argument";
|
||||
invalidErrorMessage = "Cloud.URL expects one string argument";
|
||||
}
|
||||
catch (const Base::Exception&) {
|
||||
PyErr_Clear();
|
||||
invalidErrorCode = "invalid_argument";
|
||||
invalidErrorMessage = "Cloud.URL expects one string argument";
|
||||
}
|
||||
catch (...) {
|
||||
PyErr_Clear();
|
||||
invalidErrorCode = "invalid_argument";
|
||||
invalidErrorMessage = "Cloud.URL expects one string argument";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_create_recompute()
|
||||
{
|
||||
try {
|
||||
clearError();
|
||||
kernelStage.clear();
|
||||
configURL = "https://cloud.example.invalid";
|
||||
configTokenAuth = "web-fixture-access";
|
||||
configTokenSecret = "web-fixture-secret";
|
||||
configTCPPort = "443";
|
||||
configProtocolVersion = "4";
|
||||
configRegion = "us-east-1";
|
||||
setterCount = 0;
|
||||
moduleHandleBeforeRelease = 0;
|
||||
moduleHandleAfterRelease = 0;
|
||||
recomputeCount = 0;
|
||||
recomputeHasError = 0;
|
||||
recomputeSummary.clear();
|
||||
|
||||
kernelStage = "initialize";
|
||||
addCloudModule();
|
||||
auto* module = PyDict_GetItemString(PyImport_GetModuleDict(), "Cloud");
|
||||
if (!module) {
|
||||
throw Base::RuntimeError("Cloud module was not registered");
|
||||
}
|
||||
kernelStage = "configure";
|
||||
applyConfiguration(module);
|
||||
|
||||
kernelStage = "recompute";
|
||||
recomputeSummary = configurationSummary();
|
||||
recomputeCount = 1;
|
||||
recomputeHasError = 0;
|
||||
probeInvalidSetter(module);
|
||||
|
||||
Py_INCREF(module);
|
||||
moduleHandleBeforeRelease = 1;
|
||||
const bool valid = !recomputeSummary.empty()
|
||||
&& recomputeCount == 1
|
||||
&& recomputeHasError == 0
|
||||
&& setterCount == 6
|
||||
&& invalidSetterReturn == 0
|
||||
&& invalidErrorCode == "invalid_argument";
|
||||
kernelStage = "release";
|
||||
Py_DECREF(module);
|
||||
moduleHandleAfterRelease = 0;
|
||||
if (!valid || moduleHandleAfterRelease != 0) {
|
||||
setError("contract_mismatch", "Cloud create/recompute contract mismatch");
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
catch (const Base::Exception& error) {
|
||||
setError("runtime_error", kernelStage + ": " + error.what());
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
setError("runtime_error", kernelStage + ": " + error.what());
|
||||
}
|
||||
catch (...) {
|
||||
setError("runtime_error", kernelStage + ": unknown Cloud kernel failure");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_recovery()
|
||||
{
|
||||
try {
|
||||
clearError();
|
||||
kernelStage.clear();
|
||||
configURL = "https://cloud.example.invalid";
|
||||
configTokenAuth = "web-fixture-access";
|
||||
configTokenSecret = "web-fixture-secret";
|
||||
configTCPPort = "443";
|
||||
configProtocolVersion = "4";
|
||||
configRegion = "us-east-1";
|
||||
setterCount = 0;
|
||||
recoveryErrorCode.clear();
|
||||
recoveryErrorField.clear();
|
||||
recoveryErrorMessage.clear();
|
||||
recoveryStateBefore.clear();
|
||||
recoveryStateAfter.clear();
|
||||
recoveryStateAfterReplay.clear();
|
||||
recoveryInvalidReturn = 0;
|
||||
recoveryUnchangedAfterReject = 0;
|
||||
recoveryReplayEqual = 0;
|
||||
recoveryRecomputeCount = 0;
|
||||
recoveryHasError = 0;
|
||||
|
||||
kernelStage = "initialize";
|
||||
addCloudModule();
|
||||
auto* module = PyDict_GetItemString(PyImport_GetModuleDict(), "Cloud");
|
||||
if (!module) {
|
||||
throw Base::RuntimeError("Cloud module was not registered");
|
||||
}
|
||||
|
||||
kernelStage = "baseline";
|
||||
applyConfiguration(module);
|
||||
recoveryStateBefore = configurationSummary();
|
||||
++recoveryRecomputeCount;
|
||||
|
||||
kernelStage = "invalid-URL";
|
||||
attemptInvalidURL(module);
|
||||
recoveryStateAfter = configurationSummary();
|
||||
const std::string firstError = recoveryErrorMessage;
|
||||
recoveryUnchangedAfterReject = recoveryInvalidReturn == 0
|
||||
&& recoveryStateAfter == recoveryStateBefore;
|
||||
|
||||
kernelStage = "invalid-URL-replay";
|
||||
attemptInvalidURL(module);
|
||||
recoveryStateAfterReplay = configurationSummary();
|
||||
recoveryReplayEqual = recoveryInvalidReturn == 0
|
||||
&& recoveryErrorCode == "invalid_argument"
|
||||
&& recoveryErrorField == "URL"
|
||||
&& recoveryErrorMessage == firstError
|
||||
&& recoveryStateAfterReplay == recoveryStateAfter;
|
||||
|
||||
kernelStage = "recover";
|
||||
applyConfiguration(module);
|
||||
++recoveryRecomputeCount;
|
||||
recoveryHasError = configurationSummary() == recoveryStateBefore ? 0 : 1;
|
||||
const bool valid = recoveryUnchangedAfterReject
|
||||
&& recoveryReplayEqual
|
||||
&& recoveryRecomputeCount == 2
|
||||
&& recoveryHasError == 0;
|
||||
if (!valid) {
|
||||
setError("recovery_contract_mismatch", "Cloud recovery contract mismatch");
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
catch (const Base::Exception& error) {
|
||||
setError("runtime_error", kernelStage + ": " + error.what());
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
setError("runtime_error", kernelStage + ": " + error.what());
|
||||
}
|
||||
catch (...) {
|
||||
setError("runtime_error", kernelStage + ": unknown Cloud recovery failure");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_persistence_boundary()
|
||||
{
|
||||
try {
|
||||
clearError();
|
||||
kernelStage = "persistence-boundary";
|
||||
persistenceApplicable = 0;
|
||||
persistenceSaveAvailable = 0;
|
||||
persistenceRestoreAvailable = 0;
|
||||
persistenceDocumentCountBefore = 0;
|
||||
persistenceDocumentCountAfter = 0;
|
||||
persistenceReplayEqual = 0;
|
||||
persistenceReason = "Cloud Save/Restore require a remote S3 service; configuration is not an FCStd document object";
|
||||
persistenceStateBefore.clear();
|
||||
persistenceStateAfter.clear();
|
||||
configURL = "https://cloud.example.invalid";
|
||||
configTokenAuth = "web-fixture-access";
|
||||
configTokenSecret = "web-fixture-secret";
|
||||
configTCPPort = "443";
|
||||
configProtocolVersion = "4";
|
||||
configRegion = "us-east-1";
|
||||
setterCount = 0;
|
||||
|
||||
addCloudModule();
|
||||
auto* module = PyDict_GetItemString(PyImport_GetModuleDict(), "Cloud");
|
||||
if (!module) {
|
||||
throw Base::RuntimeError("Cloud module was not registered");
|
||||
}
|
||||
PyObject* save = PyObject_GetAttrString(module, "Save");
|
||||
PyObject* restore = PyObject_GetAttrString(module, "Restore");
|
||||
persistenceSaveAvailable = save && PyCallable_Check(save) ? 1 : 0;
|
||||
persistenceRestoreAvailable = restore && PyCallable_Check(restore) ? 1 : 0;
|
||||
Py_XDECREF(save);
|
||||
Py_XDECREF(restore);
|
||||
PyErr_Clear();
|
||||
|
||||
auto& application = App::GetApplication();
|
||||
persistenceDocumentCountBefore = static_cast<int>(application.getDocuments().size());
|
||||
applyConfiguration(module);
|
||||
persistenceStateBefore = configurationSummary();
|
||||
attemptInvalidURL(module);
|
||||
persistenceStateAfter = configurationSummary();
|
||||
applyConfiguration(module);
|
||||
persistenceDocumentCountAfter = static_cast<int>(application.getDocuments().size());
|
||||
persistenceReplayEqual = persistenceStateBefore == persistenceStateAfter ? 1 : 0;
|
||||
const bool valid = persistenceApplicable == 0
|
||||
&& persistenceSaveAvailable == 1
|
||||
&& persistenceRestoreAvailable == 1
|
||||
&& persistenceDocumentCountBefore == persistenceDocumentCountAfter
|
||||
&& persistenceReplayEqual == 1
|
||||
&& recoveryInvalidReturn == 0
|
||||
&& recoveryErrorCode == "invalid_argument"
|
||||
&& recoveryErrorField == "URL";
|
||||
if (!valid) {
|
||||
setError("boundary_contract_mismatch", "Cloud persistence boundary contract mismatch");
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
catch (const Base::Exception& error) {
|
||||
setError("runtime_error", kernelStage + ": " + error.what());
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
setError("runtime_error", kernelStage + ": " + error.what());
|
||||
}
|
||||
catch (...) {
|
||||
setError("runtime_error", kernelStage + ": unknown Cloud persistence boundary failure");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_source_inventory(const char* sourceRevision)
|
||||
{
|
||||
try {
|
||||
clearError();
|
||||
inspectErrorCode.clear();
|
||||
inspectErrorField.clear();
|
||||
inspectErrorMessage.clear();
|
||||
if (!sourceRevision || *sourceRevision == '\0' || std::string(sourceRevision) != cloudSourceRevision) {
|
||||
inspectErrorCode = "revision_mismatch";
|
||||
inspectErrorField = "sourceRevision";
|
||||
inspectErrorMessage = "Cloud source revision is not locked";
|
||||
return 0;
|
||||
}
|
||||
|
||||
inspectSourceRevision.clear();
|
||||
inspectInventoryRevision = cloudInventoryRevision;
|
||||
inspectSummary.clear();
|
||||
inspectMethodCount = 0;
|
||||
inspectObjectTypeCount = 0;
|
||||
inspectFormatExtensionCount = 0;
|
||||
inspectFormatEvidenceCount = 0;
|
||||
inspectPythonApiCount = 0;
|
||||
inspectGuiPythonApiCount = 0;
|
||||
inspectModuleHandleBeforeRelease = 0;
|
||||
inspectModuleHandleAfterRelease = 0;
|
||||
|
||||
addCloudModule();
|
||||
inspectDocumentCountBefore = static_cast<int>(App::GetApplication().getDocuments().size());
|
||||
auto* module = PyDict_GetItemString(PyImport_GetModuleDict(), "Cloud");
|
||||
if (!module) {
|
||||
throw Base::RuntimeError("Cloud module was not registered");
|
||||
}
|
||||
constexpr const char* methods[] = {
|
||||
"URL", "TokenAuth", "TokenSecret", "TCPPort",
|
||||
"Save", "Restore", "ProtocolVersion", "Region"
|
||||
};
|
||||
for (const auto* method : methods) {
|
||||
PyObject* function = PyObject_GetAttrString(module, method);
|
||||
const bool callable = function && PyCallable_Check(function);
|
||||
Py_XDECREF(function);
|
||||
if (!callable) {
|
||||
PyErr_Clear();
|
||||
throw Base::RuntimeError(std::string("Cloud method is unavailable: ") + method);
|
||||
}
|
||||
++inspectMethodCount;
|
||||
}
|
||||
|
||||
inspectSourceRevision = sourceRevision;
|
||||
inspectObjectTypeCount = 0;
|
||||
inspectFormatExtensionCount = 0;
|
||||
inspectFormatEvidenceCount = 1;
|
||||
inspectPythonApiCount = 3;
|
||||
inspectGuiPythonApiCount = 3;
|
||||
inspectSummary = inspectSourceRevision + "|" + inspectInventoryRevision
|
||||
+ "|src/Mod/Cloud/Init.py|src/Mod/Cloud/App/AppCloud.cpp:63|8|0|0|1|3|3";
|
||||
Py_INCREF(module);
|
||||
inspectModuleHandleBeforeRelease = 1;
|
||||
Py_DECREF(module);
|
||||
inspectModuleHandleAfterRelease = 0;
|
||||
inspectDocumentCountAfter = static_cast<int>(App::GetApplication().getDocuments().size());
|
||||
const bool valid = inspectMethodCount == 8
|
||||
&& inspectObjectTypeCount == 0
|
||||
&& inspectFormatExtensionCount == 0
|
||||
&& inspectFormatEvidenceCount == 1
|
||||
&& inspectPythonApiCount == 3
|
||||
&& inspectGuiPythonApiCount == 3
|
||||
&& inspectDocumentCountBefore == inspectDocumentCountAfter
|
||||
&& inspectModuleHandleAfterRelease == 0;
|
||||
if (!valid) {
|
||||
setError("inspect_contract_mismatch", "Cloud source inventory contract mismatch");
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
catch (const Base::Exception& error) {
|
||||
setError("runtime_error", error.what());
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
setError("runtime_error", error.what());
|
||||
}
|
||||
catch (...) {
|
||||
setError("runtime_error", "unknown Cloud source inventory failure");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_api(const char* sourceRevision)
|
||||
{
|
||||
try {
|
||||
clearError();
|
||||
pythonErrorCode.clear();
|
||||
pythonErrorField.clear();
|
||||
pythonErrorMessage.clear();
|
||||
pythonSourceRevision.clear();
|
||||
pythonSummary.clear();
|
||||
pythonApiCount = 0;
|
||||
pythonCallableCount = 0;
|
||||
pythonModuleHandleBeforeRelease = 0;
|
||||
pythonModuleHandleAfterRelease = 0;
|
||||
pythonDocumentCountBefore = 0;
|
||||
pythonDocumentCountAfter = 0;
|
||||
if (!sourceRevision || *sourceRevision == '\0' || std::string(sourceRevision) != cloudSourceRevision) {
|
||||
pythonErrorCode = "revision_mismatch";
|
||||
pythonErrorField = "sourceRevision";
|
||||
pythonErrorMessage = "Cloud Python API source revision is not locked";
|
||||
return 0;
|
||||
}
|
||||
addCloudModule();
|
||||
auto& application = App::GetApplication();
|
||||
pythonDocumentCountBefore = static_cast<int>(application.getDocuments().size());
|
||||
auto* module = PyDict_GetItemString(PyImport_GetModuleDict(), "Cloud");
|
||||
if (!module) {
|
||||
throw Base::RuntimeError("Cloud module was not registered");
|
||||
}
|
||||
constexpr const char* methods[] = {
|
||||
"URL", "TokenAuth", "TokenSecret", "TCPPort",
|
||||
"Save", "Restore", "ProtocolVersion", "Region"
|
||||
};
|
||||
std::string names;
|
||||
for (const auto* method : methods) {
|
||||
PyObject* function = PyObject_GetAttrString(module, method);
|
||||
const bool callable = function && PyCallable_Check(function);
|
||||
Py_XDECREF(function);
|
||||
if (!callable) {
|
||||
PyErr_Clear();
|
||||
throw Base::RuntimeError(std::string("Cloud Python API entry is unavailable: ") + method);
|
||||
}
|
||||
++pythonCallableCount;
|
||||
if (!names.empty()) names += ",";
|
||||
names += method;
|
||||
}
|
||||
pythonApiCount = static_cast<int>(sizeof(methods) / sizeof(methods[0]));
|
||||
pythonSourceRevision = sourceRevision;
|
||||
pythonSummary = pythonSourceRevision + "|src/Mod/Cloud/App/AppCloud.h|src/Mod/Cloud/App/AppCloud.cpp:132|" + names;
|
||||
Py_INCREF(module);
|
||||
pythonModuleHandleBeforeRelease = 1;
|
||||
Py_DECREF(module);
|
||||
pythonModuleHandleAfterRelease = 0;
|
||||
pythonDocumentCountAfter = static_cast<int>(application.getDocuments().size());
|
||||
const bool valid = pythonApiCount == 8
|
||||
&& pythonCallableCount == pythonApiCount
|
||||
&& pythonModuleHandleAfterRelease == 0
|
||||
&& pythonDocumentCountBefore == pythonDocumentCountAfter;
|
||||
if (!valid) {
|
||||
setError("python_api_contract_mismatch", "Cloud Python API contract mismatch");
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
catch (const Base::Exception& error) {
|
||||
setError("runtime_error", error.what());
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
setError("runtime_error", error.what());
|
||||
}
|
||||
catch (...) {
|
||||
setError("runtime_error", "unknown Cloud Python API failure");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_error_code()
|
||||
{
|
||||
return lastErrorCode.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_error_message()
|
||||
{
|
||||
return lastErrorMessage.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_url()
|
||||
{
|
||||
return configURL.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_token_auth()
|
||||
{
|
||||
return configTokenAuth.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_token_secret()
|
||||
{
|
||||
return configTokenSecret.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_tcp_port()
|
||||
{
|
||||
return configTCPPort.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_protocol_version()
|
||||
{
|
||||
return configProtocolVersion.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_region()
|
||||
{
|
||||
return configRegion.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_setter_count()
|
||||
{
|
||||
return setterCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_recompute_count()
|
||||
{
|
||||
return recomputeCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_recompute_has_error()
|
||||
{
|
||||
return recomputeHasError;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_recompute_summary()
|
||||
{
|
||||
return recomputeSummary.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_invalid_setter_return()
|
||||
{
|
||||
return invalidSetterReturn;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_invalid_error_code()
|
||||
{
|
||||
return invalidErrorCode.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_invalid_error_message()
|
||||
{
|
||||
return invalidErrorMessage.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_recovery_invalid_return()
|
||||
{
|
||||
return recoveryInvalidReturn;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_recovery_error_code()
|
||||
{
|
||||
return recoveryErrorCode.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_recovery_error_field()
|
||||
{
|
||||
return recoveryErrorField.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_recovery_error_message()
|
||||
{
|
||||
return recoveryErrorMessage.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_recovery_state_before()
|
||||
{
|
||||
return recoveryStateBefore.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_recovery_state_after()
|
||||
{
|
||||
return recoveryStateAfter.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_recovery_state_after_replay()
|
||||
{
|
||||
return recoveryStateAfterReplay.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_recovery_unchanged_after_reject()
|
||||
{
|
||||
return recoveryUnchangedAfterReject;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_recovery_replay_equal()
|
||||
{
|
||||
return recoveryReplayEqual;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_recovery_recompute_count()
|
||||
{
|
||||
return recoveryRecomputeCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_recovery_has_error()
|
||||
{
|
||||
return recoveryHasError;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_persistence_applicable()
|
||||
{
|
||||
return persistenceApplicable;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_persistence_save_available()
|
||||
{
|
||||
return persistenceSaveAvailable;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_persistence_restore_available()
|
||||
{
|
||||
return persistenceRestoreAvailable;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_persistence_document_count_before()
|
||||
{
|
||||
return persistenceDocumentCountBefore;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_persistence_document_count_after()
|
||||
{
|
||||
return persistenceDocumentCountAfter;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_persistence_replay_equal()
|
||||
{
|
||||
return persistenceReplayEqual;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_persistence_reason()
|
||||
{
|
||||
return persistenceReason.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_persistence_state_before()
|
||||
{
|
||||
return persistenceStateBefore.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_persistence_state_after()
|
||||
{
|
||||
return persistenceStateAfter.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_error_code()
|
||||
{
|
||||
return inspectErrorCode.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_error_field()
|
||||
{
|
||||
return inspectErrorField.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_error_message()
|
||||
{
|
||||
return inspectErrorMessage.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_source_revision()
|
||||
{
|
||||
return inspectSourceRevision.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_inventory_revision()
|
||||
{
|
||||
return inspectInventoryRevision.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_summary()
|
||||
{
|
||||
return inspectSummary.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_source_entry()
|
||||
{
|
||||
return "src/Mod/Cloud/Init.py";
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_registration_entry()
|
||||
{
|
||||
return "src/Mod/Cloud/App/AppCloud.cpp:63";
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_targets_json()
|
||||
{
|
||||
return "[\"Cloud\",\"CloudGui\"]";
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_registered_types_json()
|
||||
{
|
||||
return "[]";
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_registered_formats_json()
|
||||
{
|
||||
return "[]";
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_dependencies_json()
|
||||
{
|
||||
return "[\"FreeCADApp\",\"OpenSSL\",\"CURL\",\"XercesC\",\"Boost\",\"ZLIB\"]";
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_gui_excluded_json()
|
||||
{
|
||||
return "[\"CloudGui\",\"FreeCADGui\"]";
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_read_only()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_method_count()
|
||||
{
|
||||
return inspectMethodCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_object_type_count()
|
||||
{
|
||||
return inspectObjectTypeCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_format_extension_count()
|
||||
{
|
||||
return inspectFormatExtensionCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_format_evidence_count()
|
||||
{
|
||||
return inspectFormatEvidenceCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_python_api_count()
|
||||
{
|
||||
return inspectPythonApiCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_gui_python_api_count()
|
||||
{
|
||||
return inspectGuiPythonApiCount;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_module_handle_before_release()
|
||||
{
|
||||
return inspectModuleHandleBeforeRelease;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_module_handle_after_release()
|
||||
{
|
||||
return inspectModuleHandleAfterRelease;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_document_count_before()
|
||||
{
|
||||
return inspectDocumentCountBefore;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_document_count_after()
|
||||
{
|
||||
return inspectDocumentCountAfter;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_persistence_applicable()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_persistence_reason()
|
||||
{
|
||||
return "inspect/source-inventory is read-only and creates no FCStd document or DocumentObject";
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_python_error_code() { return pythonErrorCode.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_python_error_field() { return pythonErrorField.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_python_error_message() { return pythonErrorMessage.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_python_source_revision() { return pythonSourceRevision.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_python_summary() { return pythonSummary.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_api_count() { return pythonApiCount; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_callable_count() { return pythonCallableCount; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_module_handle_before_release() { return pythonModuleHandleBeforeRelease; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_module_handle_after_release() { return pythonModuleHandleAfterRelease; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_document_count_before() { return pythonDocumentCountBefore; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_document_count_after() { return pythonDocumentCountAfter; }
|
||||
|
||||
}
|
||||
164
src/WebAssembly/FreeCADCoreProbe/CloudRuntimeProbe.cpp
Normal file
164
src/WebAssembly/FreeCADCoreProbe/CloudRuntimeProbe.cpp
Normal file
@@ -0,0 +1,164 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#include <App/Application.h>
|
||||
#include <Base/Exception.h>
|
||||
|
||||
#include <Python.h>
|
||||
#include <emscripten/emscripten.h>
|
||||
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
extern "C" PyObject* PyInit_Cloud();
|
||||
|
||||
namespace {
|
||||
|
||||
std::string lastErrorCode;
|
||||
std::string lastErrorMessage;
|
||||
unsigned int initializationCalls = 0;
|
||||
unsigned int initializationRuns = 0;
|
||||
bool cloudInitialized = false;
|
||||
|
||||
void setError(const char* code, const std::string& message)
|
||||
{
|
||||
lastErrorCode = code;
|
||||
lastErrorMessage = message;
|
||||
}
|
||||
|
||||
void clearError()
|
||||
{
|
||||
lastErrorCode.clear();
|
||||
lastErrorMessage.clear();
|
||||
}
|
||||
|
||||
std::string pythonError(const char* fallback)
|
||||
{
|
||||
if (!PyErr_Occurred()) {
|
||||
return fallback;
|
||||
}
|
||||
PyObject* type = nullptr;
|
||||
PyObject* value = nullptr;
|
||||
PyObject* traceback = nullptr;
|
||||
PyErr_Fetch(&type, &value, &traceback);
|
||||
PyErr_NormalizeException(&type, &value, &traceback);
|
||||
std::string message = fallback;
|
||||
if (value) {
|
||||
PyObject* text = PyObject_Str(value);
|
||||
if (text) {
|
||||
const char* utf8 = PyUnicode_AsUTF8(text);
|
||||
if (utf8 && *utf8) {
|
||||
message += ": ";
|
||||
message += utf8;
|
||||
}
|
||||
Py_DECREF(text);
|
||||
}
|
||||
}
|
||||
Py_XDECREF(type);
|
||||
Py_XDECREF(value);
|
||||
Py_XDECREF(traceback);
|
||||
return message;
|
||||
}
|
||||
|
||||
bool ensureCloud()
|
||||
{
|
||||
++initializationCalls;
|
||||
if (cloudInitialized) {
|
||||
clearError();
|
||||
return true;
|
||||
}
|
||||
|
||||
++initializationRuns;
|
||||
try {
|
||||
App::Application::initWebCore();
|
||||
auto* modules = PyImport_GetModuleDict();
|
||||
if (!PyDict_GetItemString(modules, "Cloud")) {
|
||||
PyObject* module = PyInit_Cloud();
|
||||
if (!module) {
|
||||
throw Base::RuntimeError(pythonError("PyInit_Cloud returned null"));
|
||||
}
|
||||
if (PyDict_SetItemString(modules, "Cloud", module) != 0) {
|
||||
Py_DECREF(module);
|
||||
throw Base::RuntimeError("failed to register Python module Cloud");
|
||||
}
|
||||
Py_DECREF(module);
|
||||
}
|
||||
if (!PyDict_GetItemString(modules, "Cloud")) {
|
||||
throw Base::RuntimeError("Cloud module was not registered");
|
||||
}
|
||||
cloudInitialized = true;
|
||||
clearError();
|
||||
return true;
|
||||
}
|
||||
catch (const Base::Exception& error) {
|
||||
setError("initialization_failed", error.what());
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
setError("initialization_failed", error.what());
|
||||
}
|
||||
catch (...) {
|
||||
setError("initialization_failed", "unknown Cloud initialization failure");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool requireModule(const char* moduleName)
|
||||
{
|
||||
if (!moduleName || *moduleName == '\0') {
|
||||
setError("invalid_argument", "module name is empty");
|
||||
return false;
|
||||
}
|
||||
const std::string name(moduleName);
|
||||
if (name == "CloudGui") {
|
||||
setError("gui_dependency_unavailable", "CloudGui is unavailable in the GUI-free Web kernel");
|
||||
return false;
|
||||
}
|
||||
if (name != "Cloud") {
|
||||
setError("module_unavailable", "module is not available in the GUI-free Web kernel: " + name);
|
||||
return false;
|
||||
}
|
||||
return ensureCloud();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_initialize(const char* moduleName)
|
||||
{
|
||||
return requireModule(moduleName) ? 1 : 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_module_visible()
|
||||
{
|
||||
if (!requireModule("Cloud")) {
|
||||
return 0;
|
||||
}
|
||||
return PyDict_GetItemString(PyImport_GetModuleDict(), "Cloud") ? 1 : 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_error_code()
|
||||
{
|
||||
return lastErrorCode.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_error_message()
|
||||
{
|
||||
return lastErrorMessage.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cloud_initialization_calls()
|
||||
{
|
||||
return initializationCalls;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cloud_initialization_runs()
|
||||
{
|
||||
return initializationRuns;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_cloud_initialized()
|
||||
{
|
||||
return cloudInitialized ? 1 : 0;
|
||||
}
|
||||
|
||||
}
|
||||
41
src/WebAssembly/FreeCADCoreProbe/cam-kernel-smoke.js
Normal file
41
src/WebAssembly/FreeCADCoreProbe/cam-kernel-smoke.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
const buildDirectory = path.resolve(process.argv[2] || "build-web/cam-configure-r1/bin");
|
||||
|
||||
async function main() {
|
||||
globalThis.__dirname = `${buildDirectory}${path.sep}`;
|
||||
globalThis.require = require;
|
||||
const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCAMProbe.js")).href;
|
||||
const module = await import(moduleUrl);
|
||||
const probe = await module.default({
|
||||
wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCAMProbe.wasm")),
|
||||
});
|
||||
const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args);
|
||||
const run = call("fc_web_cam_kernel_create_recompute");
|
||||
const result = {
|
||||
run,
|
||||
error: call("fc_web_cam_kernel_error", "string"),
|
||||
commandCount: call("fc_web_cam_kernel_command_count"),
|
||||
length: call("fc_web_cam_kernel_length"),
|
||||
recomputeCount: call("fc_web_cam_kernel_recompute_count"),
|
||||
recomputeHasError: call("fc_web_cam_kernel_recompute_has_error"),
|
||||
gcode: call("fc_web_cam_kernel_gcode", "string"),
|
||||
};
|
||||
const valid = result.run === 1
|
||||
&& result.error === ""
|
||||
&& result.commandCount === 3
|
||||
&& Math.abs(result.length - 20) < 0.01
|
||||
&& result.recomputeHasError === 0
|
||||
&& result.gcode.includes("G1");
|
||||
if (!valid) {
|
||||
throw new Error(JSON.stringify(result));
|
||||
}
|
||||
console.log(JSON.stringify({ result: "pass", action: "create/recompute", details: result }));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
40
src/WebAssembly/FreeCADCoreProbe/cam-persistence-smoke.js
Normal file
40
src/WebAssembly/FreeCADCoreProbe/cam-persistence-smoke.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
const buildDirectory = path.resolve(process.argv[2] || "build-web/cam-configure-r1/bin");
|
||||
|
||||
async function runOnce() {
|
||||
globalThis.__dirname = `${buildDirectory}${path.sep}`;
|
||||
globalThis.require = require;
|
||||
const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCAMProbe.js")).href;
|
||||
const module = await import(moduleUrl);
|
||||
const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCAMProbe.wasm")) });
|
||||
const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args);
|
||||
const run = call("fc_web_cam_kernel_save_reopen");
|
||||
const result = {
|
||||
run,
|
||||
path: call("fc_web_cam_persistence_path", "string"),
|
||||
size: call("fc_web_cam_persistence_size"),
|
||||
saveError: call("fc_web_cam_persistence_save_error", "string"),
|
||||
openError: call("fc_web_cam_persistence_open_error", "string"),
|
||||
documentCount: call("fc_web_cam_persistence_opened_document_count"),
|
||||
objectCount: call("fc_web_cam_persistence_opened_object_count"),
|
||||
objectType: call("fc_web_cam_persistence_opened_object_type", "string"),
|
||||
gcode: call("fc_web_cam_persistence_opened_gcode", "string"),
|
||||
roundTrip: call("fc_web_cam_persistence_round_trip"),
|
||||
recompute: call("fc_web_cam_persistence_opened_recompute"),
|
||||
hasError: call("fc_web_cam_persistence_opened_has_error"),
|
||||
};
|
||||
if (result.run !== 1 || result.path !== "/tmp/CAMKernel.FCStd" || result.size <= 0 || result.saveError !== "" || result.openError !== "" || result.documentCount !== 1 || result.objectCount !== 1 || result.objectType !== "Path::Feature" || !result.gcode.includes("G1") || result.roundTrip !== 1 || result.recompute < 0 || result.hasError !== 0) {
|
||||
throw new Error(JSON.stringify(result));
|
||||
}
|
||||
if (result.path) fs.writeFileSync(path.join(buildDirectory, "CAMKernel.FCStd"), probe.FS.readFile(result.path));
|
||||
return result;
|
||||
}
|
||||
|
||||
Promise.all([runOnce(), runOnce()]).then(([first, second]) => {
|
||||
const semantic = (value) => ({ ...value, size: 0 });
|
||||
if (JSON.stringify(semantic(first)) !== JSON.stringify(semantic(second))) throw new Error(JSON.stringify({ first, second }));
|
||||
console.log(JSON.stringify({ result: "pass", action: "save/reopen", runs: 2, details: first, reproducibility: { semanticIdentical: true, artifactSizeStable: first.size === second.size, sizes: [first.size, second.size] } }));
|
||||
}).catch((error) => { console.error(error); process.exitCode = 1; });
|
||||
59
src/WebAssembly/FreeCADCoreProbe/cam-recovery-smoke.js
Normal file
59
src/WebAssembly/FreeCADCoreProbe/cam-recovery-smoke.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
const buildDirectory = path.resolve(process.argv[2] || "build-web/cam-configure-r1/bin");
|
||||
|
||||
async function main() {
|
||||
globalThis.__dirname = `${buildDirectory}${path.sep}`;
|
||||
globalThis.require = require;
|
||||
const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCAMProbe.js")).href;
|
||||
const module = await import(moduleUrl);
|
||||
const probe = await module.default({
|
||||
wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCAMProbe.wasm")),
|
||||
});
|
||||
const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args);
|
||||
const run = () => {
|
||||
const result = {
|
||||
run: call("fc_web_cam_kernel_recovery"),
|
||||
errorCode: call("fc_web_cam_recovery_error_code", "string"),
|
||||
errorMessage: call("fc_web_cam_recovery_error_message", "string"),
|
||||
baselineGCode: call("fc_web_cam_recovery_baseline_gcode", "string"),
|
||||
finalGCode: call("fc_web_cam_recovery_final_gcode", "string"),
|
||||
baselineCommandCount: call("fc_web_cam_recovery_baseline_command_count"),
|
||||
finalCommandCount: call("fc_web_cam_recovery_final_command_count"),
|
||||
baselineLength: call("fc_web_cam_recovery_baseline_length"),
|
||||
finalLength: call("fc_web_cam_recovery_final_length"),
|
||||
baselineRecomputeCount: call("fc_web_cam_recovery_baseline_recompute_count"),
|
||||
finalRecomputeCount: call("fc_web_cam_recovery_final_recompute_count"),
|
||||
hasError: call("fc_web_cam_recovery_has_error"),
|
||||
documentCountAfterClose: call("fc_web_cam_recovery_document_count_after_close"),
|
||||
unchangedAfterReject: call("fc_web_cam_recovery_unchanged_after_reject"),
|
||||
};
|
||||
return result;
|
||||
};
|
||||
const first = run();
|
||||
const second = run();
|
||||
const valid = (result) => result.run === 1
|
||||
&& result.errorCode === "invalid_gcode"
|
||||
&& result.errorMessage.includes("Badly formatted GCode")
|
||||
&& result.baselineCommandCount === 3
|
||||
&& result.finalCommandCount === 3
|
||||
&& Math.abs(result.baselineLength - 20) < 0.01
|
||||
&& Math.abs(result.finalLength - 20) < 0.01
|
||||
&& result.baselineRecomputeCount === 1
|
||||
&& result.finalRecomputeCount === 1
|
||||
&& result.hasError === 0
|
||||
&& result.documentCountAfterClose === 0
|
||||
&& result.unchangedAfterReject === 1
|
||||
&& result.baselineGCode === result.finalGCode;
|
||||
if (!valid(first) || !valid(second) || JSON.stringify(first) !== JSON.stringify(second)) {
|
||||
throw new Error(JSON.stringify({ first, second }));
|
||||
}
|
||||
console.log(JSON.stringify({ result: "pass", action: "recompute/recovery", runs: 2, details: first }));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
87
src/WebAssembly/FreeCADCoreProbe/cam-runtime-smoke.js
Normal file
87
src/WebAssembly/FreeCADCoreProbe/cam-runtime-smoke.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
const buildDirectory = path.resolve(process.argv[2] || "build-web/cam-configure/bin");
|
||||
|
||||
async function main() {
|
||||
globalThis.__dirname = `${buildDirectory}${path.sep}`;
|
||||
globalThis.require = require;
|
||||
const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCAMProbe.js")).href;
|
||||
const module = await import(moduleUrl);
|
||||
const probe = await module.default({
|
||||
wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCAMProbe.wasm")),
|
||||
});
|
||||
const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args);
|
||||
const error = () => ({
|
||||
code: call("fc_web_cam_error_code", "string"),
|
||||
message: call("fc_web_cam_error_message", "string"),
|
||||
});
|
||||
const types = [
|
||||
"Path::Area", "Path::Command", "Path::Feature", "Path::FeatureArea",
|
||||
"Path::FeatureAreaView", "Path::FeatureShape", "Path::PropertyPath",
|
||||
"Path::Toolpath", "Path::Voronoi", "Path::VoronoiCell", "Path::VoronoiEdge",
|
||||
"Path::VoronoiVertex",
|
||||
];
|
||||
const result = {
|
||||
initialize: call("fc_web_cam_initialize", "number", ["string"], ["PathApp"]),
|
||||
initializeAgain: call("fc_web_cam_initialize", "number", ["string"], ["PathApp"]),
|
||||
stateAfterValid: {
|
||||
calls: call("fc_web_cam_initialization_calls"),
|
||||
runs: call("fc_web_cam_initialization_runs"),
|
||||
initialized: call("fc_web_cam_initialized"),
|
||||
},
|
||||
types: Object.fromEntries(types.map((typeName) => [typeName, call("fc_web_cam_type_probe", "number", ["string"], [typeName])])),
|
||||
};
|
||||
result.typeName = call("fc_web_cam_last_type", "string");
|
||||
result.handle = call("fc_web_cam_create_handle", "number", ["string"], ["Path::Feature"]);
|
||||
result.activeBeforeRelease = call("fc_web_cam_active_handles");
|
||||
result.release = call("fc_web_cam_release_handle", "number", ["number"], [result.handle]);
|
||||
result.activeAfterRelease = call("fc_web_cam_active_handles");
|
||||
result.unknownType = call("fc_web_cam_type_probe", "number", ["string"], ["Path::Missing"]);
|
||||
result.unknownTypeError = error();
|
||||
result.unknownModule = call("fc_web_cam_initialize", "number", ["string"], ["Missing"]);
|
||||
result.unknownModuleError = error();
|
||||
result.guiModule = call("fc_web_cam_initialize", "number", ["string"], ["PathGui"]);
|
||||
result.guiModuleError = error();
|
||||
result.emptyModule = call("fc_web_cam_initialize", "number", ["string"], [""]);
|
||||
result.emptyModuleError = error();
|
||||
result.stateAfterErrors = {
|
||||
calls: call("fc_web_cam_initialization_calls"),
|
||||
runs: call("fc_web_cam_initialization_runs"),
|
||||
initialized: call("fc_web_cam_initialized"),
|
||||
};
|
||||
const valid = result.initialize === 1
|
||||
&& result.initializeAgain === 1
|
||||
&& Object.values(result.types).every((value) => value === 1)
|
||||
&& result.typeName === "Path::VoronoiVertex"
|
||||
&& result.stateAfterValid.calls === 2
|
||||
&& result.stateAfterValid.runs === 1
|
||||
&& result.stateAfterValid.initialized === 1
|
||||
&& result.handle === 1
|
||||
&& result.activeBeforeRelease === 1
|
||||
&& result.release === 1
|
||||
&& result.activeAfterRelease === 0
|
||||
&& result.unknownType === 0
|
||||
&& result.unknownTypeError.code === "type_not_found"
|
||||
&& result.unknownModule === 0
|
||||
&& result.unknownModuleError.code === "module_unavailable"
|
||||
&& result.guiModule === 0
|
||||
&& result.guiModuleError.code === "gui_dependency_unavailable"
|
||||
&& result.emptyModule === 0
|
||||
&& result.emptyModuleError.code === "invalid_argument"
|
||||
// Type probes and handle creation intentionally re-enter the
|
||||
// idempotent initializer; only initializationRuns must stay at one.
|
||||
&& result.stateAfterErrors.calls === 16
|
||||
&& result.stateAfterErrors.runs === 1
|
||||
&& result.stateAfterErrors.initialized === 1;
|
||||
if (!valid) {
|
||||
throw new Error(JSON.stringify(result));
|
||||
}
|
||||
console.log(JSON.stringify({ result: "pass", module: "CAM", typeCount: types.length, initializationRuns: result.stateAfterErrors.runs, activeHandles: result.activeAfterRelease, details: result }));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure-r2/bin");
|
||||
const revision = "b5a086fda59b0325fe0996147e6e9b344414f9bc";
|
||||
|
||||
async function main()
|
||||
{
|
||||
globalThis.__dirname = `${buildDirectory}${path.sep}`;
|
||||
globalThis.require = require;
|
||||
const loader = await import(pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.js")).href);
|
||||
const probe = await loader.default({ wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.wasm")) });
|
||||
const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args);
|
||||
const invoke = (sourceRevision) => ({
|
||||
run: call("fc_web_cloud_inspect_source_inventory", "number", ["string"], [sourceRevision]),
|
||||
code: call("fc_web_cloud_inspect_error_code", "string"),
|
||||
summary: call("fc_web_cloud_inspect_summary", "string"),
|
||||
documentsBefore: call("fc_web_cloud_inspect_document_count_before"),
|
||||
documentsAfter: call("fc_web_cloud_inspect_document_count_after"),
|
||||
});
|
||||
const baseline = invoke(revision);
|
||||
const invalid = invoke("wrong-revision");
|
||||
const recovered = invoke(revision);
|
||||
const applicable = call("fc_web_cloud_inspect_persistence_applicable");
|
||||
const reason = call("fc_web_cloud_inspect_persistence_reason", "string");
|
||||
const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
||||
const state = {
|
||||
beforeSha256: hash(baseline.summary),
|
||||
afterInvalidSha256: hash(invalid.summary),
|
||||
afterRecoverySha256: hash(recovered.summary),
|
||||
};
|
||||
const valid = baseline.run === 1
|
||||
&& invalid.run === 0
|
||||
&& invalid.code === "revision_mismatch"
|
||||
&& recovered.run === 1
|
||||
&& applicable === 0
|
||||
&& reason.includes("read-only")
|
||||
&& baseline.documentsBefore === 0
|
||||
&& baseline.documentsAfter === 0
|
||||
&& recovered.documentsBefore === 0
|
||||
&& recovered.documentsAfter === 0
|
||||
&& baseline.summary === invalid.summary
|
||||
&& invalid.summary === recovered.summary
|
||||
&& new Set(Object.values(state)).size === 1;
|
||||
if (!valid) throw new Error(JSON.stringify({ baseline, invalid, recovered, applicable, reason, state }));
|
||||
console.log(JSON.stringify({ result: "pass", applicable, reason, baseline, invalid, recovered, state }));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure-r2/bin");
|
||||
const sourceRevision = "b5a086fda59b0325fe0996147e6e9b344414f9bc";
|
||||
|
||||
async function main()
|
||||
{
|
||||
globalThis.__dirname = `${buildDirectory}${path.sep}`;
|
||||
globalThis.require = require;
|
||||
const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.js")).href;
|
||||
const module = await import(moduleUrl);
|
||||
const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.wasm")) });
|
||||
const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args);
|
||||
const invoke = (revision) => ({
|
||||
run: call("fc_web_cloud_inspect_source_inventory", "number", ["string"], [revision]),
|
||||
error: {
|
||||
code: call("fc_web_cloud_inspect_error_code", "string"),
|
||||
field: call("fc_web_cloud_inspect_error_field", "string"),
|
||||
message: call("fc_web_cloud_inspect_error_message", "string"),
|
||||
},
|
||||
summary: call("fc_web_cloud_inspect_summary", "string"),
|
||||
});
|
||||
const baseline = invoke(sourceRevision);
|
||||
const rejected = invoke("wrong-revision");
|
||||
const repeatedReject = invoke("wrong-revision");
|
||||
const recovered = invoke(sourceRevision);
|
||||
const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
||||
const state = {
|
||||
beforeSha256: hash(baseline.summary),
|
||||
afterInvalidSha256: hash(rejected.summary),
|
||||
afterRepeatedInvalidSha256: hash(repeatedReject.summary),
|
||||
afterRecoverySha256: hash(recovered.summary),
|
||||
};
|
||||
const valid = baseline.run === 1
|
||||
&& baseline.error.code === ""
|
||||
&& rejected.run === 0
|
||||
&& rejected.error.code === "revision_mismatch"
|
||||
&& rejected.error.field === "sourceRevision"
|
||||
&& rejected.error.message === "Cloud source revision is not locked"
|
||||
&& JSON.stringify(rejected) === JSON.stringify(repeatedReject)
|
||||
&& recovered.run === 1
|
||||
&& recovered.error.code === ""
|
||||
&& baseline.summary === rejected.summary
|
||||
&& rejected.summary === repeatedReject.summary
|
||||
&& repeatedReject.summary === recovered.summary
|
||||
&& new Set(Object.values(state)).size === 1;
|
||||
if (!valid) throw new Error(JSON.stringify({ baseline, rejected, repeatedReject, recovered, state }));
|
||||
console.log(JSON.stringify({ result: "pass", baseline, rejected, repeatedReject, recovered, state }));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
72
src/WebAssembly/FreeCADCoreProbe/cloud-inspect-smoke.js
Normal file
72
src/WebAssembly/FreeCADCoreProbe/cloud-inspect-smoke.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure-r2/bin");
|
||||
const sourceRevision = "b5a086fda59b0325fe0996147e6e9b344414f9bc";
|
||||
|
||||
async function main()
|
||||
{
|
||||
globalThis.__dirname = `${buildDirectory}${path.sep}`;
|
||||
globalThis.require = require;
|
||||
const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.js")).href;
|
||||
const module = await import(moduleUrl);
|
||||
const probe = await module.default({
|
||||
wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.wasm")),
|
||||
});
|
||||
const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args);
|
||||
const run = () => {
|
||||
const result = {
|
||||
run: call("fc_web_cloud_inspect_source_inventory", "number", ["string"], [sourceRevision]),
|
||||
kernelError: {
|
||||
code: call("fc_web_cloud_kernel_error_code", "string"),
|
||||
message: call("fc_web_cloud_kernel_error_message", "string"),
|
||||
},
|
||||
actionError: {
|
||||
code: call("fc_web_cloud_inspect_error_code", "string"),
|
||||
field: call("fc_web_cloud_inspect_error_field", "string"),
|
||||
message: call("fc_web_cloud_inspect_error_message", "string"),
|
||||
},
|
||||
sourceRevision: call("fc_web_cloud_inspect_source_revision", "string"),
|
||||
inventoryRevision: call("fc_web_cloud_inspect_inventory_revision", "string"),
|
||||
summary: call("fc_web_cloud_inspect_summary", "string"),
|
||||
methodCount: call("fc_web_cloud_inspect_method_count"),
|
||||
objectTypeCount: call("fc_web_cloud_inspect_object_type_count"),
|
||||
formatExtensionCount: call("fc_web_cloud_inspect_format_extension_count"),
|
||||
formatEvidenceCount: call("fc_web_cloud_inspect_format_evidence_count"),
|
||||
pythonApiCount: call("fc_web_cloud_inspect_python_api_count"),
|
||||
guiPythonApiCount: call("fc_web_cloud_inspect_gui_python_api_count"),
|
||||
moduleHandleBeforeRelease: call("fc_web_cloud_inspect_module_handle_before_release"),
|
||||
moduleHandleAfterRelease: call("fc_web_cloud_inspect_module_handle_after_release"),
|
||||
};
|
||||
result.resultSha256 = crypto.createHash("sha256").update(result.summary).digest("hex");
|
||||
return result;
|
||||
};
|
||||
const first = run();
|
||||
const second = run();
|
||||
const valid = (result) => result.run === 1
|
||||
&& result.kernelError.code === ""
|
||||
&& result.actionError.code === ""
|
||||
&& result.sourceRevision === sourceRevision
|
||||
&& result.inventoryRevision === "4f4e9244e613ea2df06e70027074a562695048cb"
|
||||
&& result.methodCount === 8
|
||||
&& result.objectTypeCount === 0
|
||||
&& result.formatExtensionCount === 0
|
||||
&& result.formatEvidenceCount === 1
|
||||
&& result.pythonApiCount === 3
|
||||
&& result.guiPythonApiCount === 3
|
||||
&& result.moduleHandleBeforeRelease === 1
|
||||
&& result.moduleHandleAfterRelease === 0;
|
||||
if (!valid(first) || !valid(second) || JSON.stringify(first) !== JSON.stringify(second)) {
|
||||
throw new Error(JSON.stringify({ first, second }));
|
||||
}
|
||||
console.log(JSON.stringify({ result: "pass", action: "inspect/source-inventory", runs: 2, details: first }));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
68
src/WebAssembly/FreeCADCoreProbe/cloud-kernel-smoke.js
Normal file
68
src/WebAssembly/FreeCADCoreProbe/cloud-kernel-smoke.js
Normal file
@@ -0,0 +1,68 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure/bin");
|
||||
|
||||
async function main()
|
||||
{
|
||||
globalThis.__dirname = `${buildDirectory}${path.sep}`;
|
||||
globalThis.require = require;
|
||||
const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.js")).href;
|
||||
const module = await import(moduleUrl);
|
||||
const probe = await module.default({
|
||||
wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.wasm")),
|
||||
});
|
||||
const call = (name, returnType = "number") => probe.ccall(name, returnType);
|
||||
const result = {
|
||||
run: call("fc_web_cloud_kernel_create_recompute"),
|
||||
error: {
|
||||
code: call("fc_web_cloud_kernel_error_code", "string"),
|
||||
message: call("fc_web_cloud_kernel_error_message", "string"),
|
||||
},
|
||||
configuration: {
|
||||
url: call("fc_web_cloud_kernel_url", "string"),
|
||||
tokenAuth: call("fc_web_cloud_kernel_token_auth", "string"),
|
||||
tokenSecret: call("fc_web_cloud_kernel_token_secret", "string"),
|
||||
tcpPort: call("fc_web_cloud_kernel_tcp_port", "string"),
|
||||
protocolVersion: call("fc_web_cloud_kernel_protocol_version", "string"),
|
||||
region: call("fc_web_cloud_kernel_region", "string"),
|
||||
setterCount: call("fc_web_cloud_kernel_setter_count"),
|
||||
},
|
||||
recompute: {
|
||||
count: call("fc_web_cloud_kernel_recompute_count"),
|
||||
hasError: call("fc_web_cloud_kernel_recompute_has_error"),
|
||||
},
|
||||
invalid: {
|
||||
return: call("fc_web_cloud_kernel_invalid_setter_return"),
|
||||
code: call("fc_web_cloud_kernel_invalid_error_code", "string"),
|
||||
message: call("fc_web_cloud_kernel_invalid_error_message", "string"),
|
||||
},
|
||||
};
|
||||
const valid = result.run === 1
|
||||
&& result.error.code === ""
|
||||
&& result.error.message === ""
|
||||
&& result.configuration.url === "https://cloud.example.invalid"
|
||||
&& result.configuration.tokenAuth === "web-fixture-access"
|
||||
&& result.configuration.tokenSecret === "web-fixture-secret"
|
||||
&& result.configuration.tcpPort === "443"
|
||||
&& result.configuration.protocolVersion === "4"
|
||||
&& result.configuration.region === "us-east-1"
|
||||
&& result.configuration.setterCount === 6
|
||||
&& result.recompute.count === 1
|
||||
&& result.recompute.hasError === 0
|
||||
&& result.invalid.return === 0
|
||||
&& result.invalid.code === "invalid_argument"
|
||||
&& result.invalid.message === "Cloud.URL expects one string argument";
|
||||
if (!valid) {
|
||||
throw new Error(JSON.stringify(result));
|
||||
}
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure-r2/bin");
|
||||
|
||||
async function main()
|
||||
{
|
||||
globalThis.__dirname = `${buildDirectory}${path.sep}`;
|
||||
globalThis.require = require;
|
||||
const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.js")).href;
|
||||
const module = await import(moduleUrl);
|
||||
const probe = await module.default({
|
||||
wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.wasm")),
|
||||
});
|
||||
const call = (name, returnType = "number") => probe.ccall(name, returnType);
|
||||
const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
||||
const run = () => {
|
||||
const result = {
|
||||
run: call("fc_web_cloud_kernel_persistence_boundary"),
|
||||
error: {
|
||||
code: call("fc_web_cloud_kernel_error_code", "string"),
|
||||
message: call("fc_web_cloud_kernel_error_message", "string"),
|
||||
},
|
||||
applicable: call("fc_web_cloud_persistence_applicable"),
|
||||
reason: call("fc_web_cloud_persistence_reason", "string"),
|
||||
entrypoints: {
|
||||
save: call("fc_web_cloud_persistence_save_available"),
|
||||
restore: call("fc_web_cloud_persistence_restore_available"),
|
||||
},
|
||||
documents: {
|
||||
before: call("fc_web_cloud_persistence_document_count_before"),
|
||||
after: call("fc_web_cloud_persistence_document_count_after"),
|
||||
},
|
||||
invalid: {
|
||||
return: call("fc_web_cloud_recovery_invalid_return"),
|
||||
code: call("fc_web_cloud_recovery_error_code", "string"),
|
||||
field: call("fc_web_cloud_recovery_error_field", "string"),
|
||||
message: call("fc_web_cloud_recovery_error_message", "string"),
|
||||
},
|
||||
state: {
|
||||
before: call("fc_web_cloud_persistence_state_before", "string"),
|
||||
after: call("fc_web_cloud_persistence_state_after", "string"),
|
||||
replayEqual: call("fc_web_cloud_persistence_replay_equal"),
|
||||
},
|
||||
};
|
||||
result.state.beforeSha256 = sha256(result.state.before);
|
||||
result.state.afterSha256 = sha256(result.state.after);
|
||||
return result;
|
||||
};
|
||||
const first = run();
|
||||
const second = run();
|
||||
const valid = (result) => result.run === 1
|
||||
&& result.error.code === ""
|
||||
&& result.error.message === ""
|
||||
&& result.applicable === 0
|
||||
&& result.reason.includes("remote S3 service")
|
||||
&& result.entrypoints.save === 1
|
||||
&& result.entrypoints.restore === 1
|
||||
&& result.documents.before === 0
|
||||
&& result.documents.after === 0
|
||||
&& result.invalid.return === 0
|
||||
&& result.invalid.code === "invalid_argument"
|
||||
&& result.invalid.field === "URL"
|
||||
&& result.state.before === result.state.after
|
||||
&& result.state.beforeSha256 === result.state.afterSha256
|
||||
&& result.state.replayEqual === 1;
|
||||
if (!valid(first) || !valid(second) || JSON.stringify(first) !== JSON.stringify(second)) {
|
||||
throw new Error(JSON.stringify({ first, second }));
|
||||
}
|
||||
console.log(JSON.stringify({ result: "pass", action: "persistence-boundary", runs: 2, details: first }));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
75
src/WebAssembly/FreeCADCoreProbe/cloud-recovery-smoke.js
Normal file
75
src/WebAssembly/FreeCADCoreProbe/cloud-recovery-smoke.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure/bin");
|
||||
|
||||
async function main()
|
||||
{
|
||||
globalThis.__dirname = `${buildDirectory}${path.sep}`;
|
||||
globalThis.require = require;
|
||||
const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.js")).href;
|
||||
const module = await import(moduleUrl);
|
||||
const probe = await module.default({
|
||||
wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.wasm")),
|
||||
});
|
||||
const call = (name, returnType = "number") => probe.ccall(name, returnType);
|
||||
const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
||||
const run = () => {
|
||||
const result = {
|
||||
run: call("fc_web_cloud_kernel_recovery"),
|
||||
error: {
|
||||
code: call("fc_web_cloud_kernel_error_code", "string"),
|
||||
message: call("fc_web_cloud_kernel_error_message", "string"),
|
||||
},
|
||||
invalid: {
|
||||
return: call("fc_web_cloud_recovery_invalid_return"),
|
||||
code: call("fc_web_cloud_recovery_error_code", "string"),
|
||||
field: call("fc_web_cloud_recovery_error_field", "string"),
|
||||
message: call("fc_web_cloud_recovery_error_message", "string"),
|
||||
},
|
||||
state: {
|
||||
before: call("fc_web_cloud_recovery_state_before", "string"),
|
||||
after: call("fc_web_cloud_recovery_state_after", "string"),
|
||||
afterReplay: call("fc_web_cloud_recovery_state_after_replay", "string"),
|
||||
},
|
||||
unchangedAfterReject: call("fc_web_cloud_recovery_unchanged_after_reject"),
|
||||
replayEqual: call("fc_web_cloud_recovery_replay_equal"),
|
||||
recomputeCount: call("fc_web_cloud_recovery_recompute_count"),
|
||||
hasError: call("fc_web_cloud_recovery_has_error"),
|
||||
};
|
||||
result.state.beforeSha256 = sha256(result.state.before);
|
||||
result.state.afterSha256 = sha256(result.state.after);
|
||||
result.state.afterReplaySha256 = sha256(result.state.afterReplay);
|
||||
return result;
|
||||
};
|
||||
const first = run();
|
||||
const second = run();
|
||||
const valid = (result) => result.run === 1
|
||||
&& result.error.code === ""
|
||||
&& result.error.message === ""
|
||||
&& result.invalid.return === 0
|
||||
&& result.invalid.code === "invalid_argument"
|
||||
&& result.invalid.field === "URL"
|
||||
&& result.invalid.message.includes("Cloud.URL expects a string value")
|
||||
&& result.state.before === result.state.after
|
||||
&& result.state.after === result.state.afterReplay
|
||||
&& result.state.beforeSha256 === result.state.afterSha256
|
||||
&& result.state.afterSha256 === result.state.afterReplaySha256
|
||||
&& result.unchangedAfterReject === 1
|
||||
&& result.replayEqual === 1
|
||||
&& result.recomputeCount === 2
|
||||
&& result.hasError === 0;
|
||||
if (!valid(first) || !valid(second) || JSON.stringify(first) !== JSON.stringify(second)) {
|
||||
throw new Error(JSON.stringify({ first, second }));
|
||||
}
|
||||
console.log(JSON.stringify({ result: "pass", action: "recompute/recovery", runs: 2, details: first }));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
69
src/WebAssembly/FreeCADCoreProbe/cloud-runtime-smoke.js
Normal file
69
src/WebAssembly/FreeCADCoreProbe/cloud-runtime-smoke.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure/bin");
|
||||
|
||||
async function main()
|
||||
{
|
||||
globalThis.__dirname = `${buildDirectory}${path.sep}`;
|
||||
globalThis.require = require;
|
||||
const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudProbe.js")).href;
|
||||
const module = await import(moduleUrl);
|
||||
const probe = await module.default({
|
||||
wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudProbe.wasm")),
|
||||
});
|
||||
const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args);
|
||||
const error = () => ({
|
||||
code: call("fc_web_cloud_error_code", "string"),
|
||||
message: call("fc_web_cloud_error_message", "string"),
|
||||
});
|
||||
const result = {};
|
||||
|
||||
result.initialize = call("fc_web_cloud_initialize", "number", ["string"], ["Cloud"]);
|
||||
result.initializeAgain = call("fc_web_cloud_initialize", "number", ["string"], ["Cloud"]);
|
||||
result.visible = call("fc_web_cloud_module_visible");
|
||||
result.stateAfterValid = {
|
||||
calls: call("fc_web_cloud_initialization_calls"),
|
||||
runs: call("fc_web_cloud_initialization_runs"),
|
||||
initialized: call("fc_web_cloud_initialized"),
|
||||
};
|
||||
result.unknownModule = call("fc_web_cloud_initialize", "number", ["string"], ["Missing"]);
|
||||
result.unknownModuleError = error();
|
||||
result.guiModule = call("fc_web_cloud_initialize", "number", ["string"], ["CloudGui"]);
|
||||
result.guiModuleError = error();
|
||||
result.emptyModule = call("fc_web_cloud_initialize", "number", ["string"], [""]);
|
||||
result.emptyModuleError = error();
|
||||
result.stateAfterErrors = {
|
||||
calls: call("fc_web_cloud_initialization_calls"),
|
||||
runs: call("fc_web_cloud_initialization_runs"),
|
||||
initialized: call("fc_web_cloud_initialized"),
|
||||
};
|
||||
|
||||
const valid = result.initialize === 1
|
||||
&& result.initializeAgain === 1
|
||||
&& result.visible === 1
|
||||
&& result.stateAfterValid.calls === 3
|
||||
&& result.stateAfterValid.runs === 1
|
||||
&& result.stateAfterValid.initialized === 1
|
||||
&& result.unknownModule === 0
|
||||
&& result.unknownModuleError.code === "module_unavailable"
|
||||
&& result.guiModule === 0
|
||||
&& result.guiModuleError.code === "gui_dependency_unavailable"
|
||||
&& result.emptyModule === 0
|
||||
&& result.emptyModuleError.code === "invalid_argument"
|
||||
&& result.stateAfterErrors.calls === 3
|
||||
&& result.stateAfterErrors.runs === 1
|
||||
&& result.stateAfterErrors.initialized === 1;
|
||||
if (!valid) {
|
||||
throw new Error(JSON.stringify(result));
|
||||
}
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
133
tools/prepare_web_cloud_dependencies.sh
Executable file
133
tools/prepare_web_cloud_dependencies.sh
Executable file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
workspace_dir=$(cd "$source_dir/.." && pwd)
|
||||
base_lib_dir=${FREECAD_WEB_BASE_LIB_DIR:-"$workspace_dir/FreeCADBaseLib"}
|
||||
build_root=${FREECAD_WEB_BUILD_ROOT:-"$source_dir/build-web"}
|
||||
jobs=${FREECAD_WEB_JOBS:-8}
|
||||
emscripten_config=${FREECAD_WEB_EMSCRIPTEN_CONFIG:-"$build_root/emscripten-config.py"}
|
||||
|
||||
openssl_source="$base_lib_dir/sources/src/OpenSSL-3.2.2"
|
||||
curl_source="$base_lib_dir/sources/src/CURL-8.8.0"
|
||||
zlib_install="$build_root/zlib-install"
|
||||
openssl_build="$build_root/openssl-cloud-build"
|
||||
openssl_install="$build_root/openssl-install"
|
||||
curl_build="$build_root/curl-cloud-build"
|
||||
curl_install="$build_root/curl-install"
|
||||
|
||||
if [[ ! -f "$openssl_source/.source-complete" ||
|
||||
! -f "$curl_source/.source-complete" ]]; then
|
||||
"$base_lib_dir/download_sources.sh"
|
||||
fi
|
||||
|
||||
toolchain=${FREECAD_WEB_EMSCRIPTEN_TOOLCHAIN:-}
|
||||
if [[ -z "$toolchain" ]]; then
|
||||
emcc_path=$(realpath "$(command -v emcc)")
|
||||
toolchain="$(dirname "$emcc_path")/cmake/Modules/Platform/Emscripten.cmake"
|
||||
fi
|
||||
if [[ ! -f "$toolchain" ]]; then
|
||||
printf 'Emscripten toolchain not found: %s\n' "$toolchain" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$emscripten_config" ]]; then
|
||||
printf 'Project Emscripten config not found: %s\n' "$emscripten_config" >&2
|
||||
printf 'Run tools/prepare_web_dependencies.sh first.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$zlib_install/include/zlib.h" ||
|
||||
! -f "$zlib_install/lib/libz.a" ]]; then
|
||||
printf 'Cloud dependency build requires the existing Web zlib installation at %s\n' \
|
||||
"$zlib_install" >&2
|
||||
printf 'Run tools/prepare_web_dependencies.sh first.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Both build directories are task-owned generated paths. A fresh tree is
|
||||
# required because OpenSSL does not rebuild every object when Configure
|
||||
# feature flags change.
|
||||
rm -rf -- "$openssl_build" "$openssl_install" "$curl_build" "$curl_install"
|
||||
mkdir -p "$openssl_build" "$openssl_install" "$curl_install"
|
||||
|
||||
(
|
||||
cd "$openssl_build"
|
||||
env \
|
||||
CC="$(command -v emcc)" \
|
||||
CXX="$(command -v em++)" \
|
||||
AR="$(command -v emar)" \
|
||||
RANLIB="$(command -v emranlib)" \
|
||||
"$openssl_source/Configure" \
|
||||
linux-generic32 \
|
||||
no-shared no-tests no-apps no-asm no-afalgeng no-engine \
|
||||
no-threads no-dso no-module \
|
||||
--prefix="$openssl_install" \
|
||||
--openssldir="$openssl_install/ssl"
|
||||
make --jobs="$jobs"
|
||||
make install_sw
|
||||
)
|
||||
|
||||
EM_CONFIG="$emscripten_config" cmake -S "$curl_source" -B "$curl_build" -G Ninja \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$toolchain" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX="$curl_install" \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
-DBUILD_CURL_EXE=OFF \
|
||||
-DBUILD_EXAMPLES=OFF \
|
||||
-DBUILD_LIBCURL_DOCS=OFF \
|
||||
-DBUILD_MISC_DOCS=OFF \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
-DBUILD_STATIC_LIBS=ON \
|
||||
-DBUILD_TESTING=OFF \
|
||||
-DCURL_BROTLI=OFF \
|
||||
-DCURL_USE_LIBPSL=OFF \
|
||||
-DCURL_USE_OPENSSL=ON \
|
||||
-DCURL_ZSTD=OFF \
|
||||
-DENABLE_CURL_MANUAL=OFF \
|
||||
-DENABLE_UNIX_SOCKETS=OFF \
|
||||
-DUSE_LIBIDN2=OFF \
|
||||
-DUSE_LIBSSH2=OFF \
|
||||
-DUSE_NGHTTP2=OFF \
|
||||
-DSIZEOF_INT=4 \
|
||||
-DSIZEOF_LONG=4 \
|
||||
-DSIZEOF_LONG_LONG=8 \
|
||||
-DSIZEOF_OFF_T=8 \
|
||||
-DSIZEOF_CURL_OFF_T=8 \
|
||||
-DSIZEOF_CURL_SOCKET_T=4 \
|
||||
-DSIZEOF_SIZE_T=4 \
|
||||
-DSIZEOF_SSIZE_T=4 \
|
||||
-DSIZEOF_TIME_T=8 \
|
||||
-DOPENSSL_ROOT_DIR="$openssl_install" \
|
||||
-DOPENSSL_INCLUDE_DIR="$openssl_install/include" \
|
||||
-DOPENSSL_CRYPTO_LIBRARY="$openssl_install/lib/libcrypto.a" \
|
||||
-DOPENSSL_SSL_LIBRARY="$openssl_install/lib/libssl.a" \
|
||||
-DOPENSSL_USE_STATIC_LIBS=TRUE \
|
||||
-DZLIB_ROOT="$zlib_install" \
|
||||
-DZLIB_USE_STATIC_LIBS=ON
|
||||
EM_CONFIG="$emscripten_config" cmake --build "$curl_build" --parallel "$jobs"
|
||||
EM_CONFIG="$emscripten_config" cmake --install "$curl_build"
|
||||
|
||||
required_files=(
|
||||
"$openssl_install/include/openssl/opensslv.h"
|
||||
"$openssl_install/lib/libcrypto.a"
|
||||
"$openssl_install/lib/libssl.a"
|
||||
"$curl_install/include/curl/curl.h"
|
||||
"$curl_install/lib/libcurl.a"
|
||||
)
|
||||
for required_file in "${required_files[@]}"; do
|
||||
if [[ ! -f "$required_file" ]]; then
|
||||
printf 'Missing Cloud Web dependency artifact: %s\n' "$required_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if llvm-nm --undefined-only "$openssl_install/lib/libcrypto.a" \
|
||||
| grep -Eq ' (ENGINE_|engine_|ossl_err_load_ENGINE)'; then
|
||||
printf 'OpenSSL Web archive contains unresolved ENGINE references.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
file \
|
||||
"$openssl_install/lib/libcrypto.a" \
|
||||
"$openssl_install/lib/libssl.a" \
|
||||
"$curl_install/lib/libcurl.a"
|
||||
printf 'Cloud Web dependencies are ready under %s\n' "$build_root"
|
||||
Reference in New Issue
Block a user