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

This commit is contained in:
2026-08-21 06:51:31 +08:00
parent b5a086fda5
commit c87b2871be
23 changed files with 2865 additions and 15 deletions

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -34,9 +34,11 @@
#include <openssl/hmac.h>
#include <openssl/md5.h>
#include <openssl/pem.h>
#include <openssl/sha.h>
#include <curl/curl.h>
#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);
}
return (true);
}

View File

@@ -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);

View File

@@ -52,9 +52,17 @@ IF(WIN32)
SET( CURL_FOUND False)
message( FATAL_ERROR "curl not found!" )
endif(EXISTS ${FREECAD_LIBPACK_DIR}/lib/libcurl.lib)
ELSE()
find_package(CURL REQUIRED)
ENDIF(WIN32)
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)

View 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; }
}

View File

@@ -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()

View 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; }
}

View 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;
}
}

View 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;
});

View 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; });

View 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;
});

View 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;
});

View File

@@ -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;
});

View File

@@ -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;
});

View 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;
});

View 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;
});

View File

@@ -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;
});

View 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;
});

View 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;
});

View 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"