Commit 5986b111 authored by jan.koester's avatar jan.koester
Browse files

test

parent cc544ebe
Loading
Loading
Loading
Loading
+0 −5
Original line number Diff line number Diff line
@@ -50,11 +50,6 @@ static void signalHandler(int sig) {
}
#endif
int main(int argc, char *argv[]) {
    if (int bridgeRc = blogi::webedit::nodebridge::runNodeRunnerFromArgs(argc, argv);
        bridgeRc >= 0) {
        return bridgeRc;
    }

    cmdplus::CmdController cmdCtl(cmdplus::CmdController::getInstance());

    cmdCtl.registerCmd("help", 'h', false, "", "Show this help");
+185 −219
Original line number Diff line number Diff line
#pragma once

#include <iostream>
#include <cstring>
#include <memory>
#include <string>
#include <vector>

#ifdef _WIN32
#include <windows.h>
#else
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#endif
#include <mutex>

#ifndef BLOGI_HAVE_LIBNODE
#error "BLOGI_HAVE_LIBNODE must be defined: this build supports only embedded libnode"
@@ -19,9 +12,7 @@

#include <node.h>

namespace blogi::webedit::nodebridge {

inline constexpr const char *kBridgeRunnerArg = "--node-bridge-run";
namespace blogi::webedit::embedded {

inline std::string getEnvOrDefault(const char *key, const char *fallback) {
    const char *v = std::getenv(key);
@@ -31,234 +22,209 @@ inline std::string getEnvOrDefault(const char *key, const char *fallback) {
    return std::string(fallback);
}

inline std::string getSelfExecutablePath() {
#ifdef _WIN32
    char buf[MAX_PATH];
    DWORD len = GetModuleFileNameA(nullptr, buf, static_cast<DWORD>(sizeof(buf)));
    if (len == 0 || len >= sizeof(buf)) {
        return std::string();
    }
    return std::string(buf, len);
#else
    char buf[4096];
    ssize_t len = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
    if (len <= 0) {
        return std::string();
inline constexpr const char *kEmbeddedBootstrapScript = R"JS(
'use strict';
const fs = require('fs');
const path = require('path');
const loaded = new Map();

function resolveHook(mod, hook) {
  if (hook === 'render') {
    if (typeof mod === 'function') return mod;
    if (typeof mod.render === 'function') return mod.render;
    return null;
  }
    buf[len] = '\0';
    return std::string(buf);
#endif
  if (typeof mod[hook] === 'function') return mod[hook];
  if (mod.hooks && typeof mod.hooks[hook] === 'function') return mod.hooks[hook];
  return null;
}

inline int runNodeRunnerInProcess(const std::string &reqPath,
                                  const std::string &resPath) {
    static const std::string bridgeScript =
        "'use strict';"
        "const fs=require('fs');"
        "const path=require('path');"
        "(async()=>{"
        "const argv=process.argv;"
        "const requestFile=argv[1];"
        "const responseFile=argv[2];"
        "if(!requestFile||!responseFile)throw new Error('Usage: node -e <bridge> <request.json> <response.json>');"
        "const req=JSON.parse(fs.readFileSync(requestFile,'utf8'));"
        "const hook=(typeof req.hook==='string'&&req.hook)?req.hook:'render';"
        "if(!req.script_path)throw new Error('request.script_path is required');"
        "const scriptPath=path.resolve(req.script_path);"
        "const mod=require(scriptPath);"
        "const resolveHook=(m,h)=>{"
        "if(h==='render'){if(typeof m==='function')return m;if(typeof m.render==='function')return m.render;return null;}"
        "if(typeof m[h]==='function')return m[h];"
        "if(m.hooks&&typeof m.hooks[h]==='function')return m.hooks[h];"
        "return null;"
        "};"
        "const fn=resolveHook(mod,hook);"
        "if(!fn){fs.writeFileSync(responseFile,JSON.stringify({status:'success',missing_hook:true,result:null}),'utf8');return;}"
        "const result=await Promise.resolve(fn(req));"
        "if(hook==='metadata'&&result&&typeof result==='object'){"
        "if(!result.type_id||typeof result.type_id!=='string'||!result.type_id.trim()){"
        "const scriptBase=path.basename(scriptPath,path.extname(scriptPath));"
        "const widgetName=(typeof result.name==='string'&&result.name.trim())?result.name.trim():scriptBase;"
        "if(typeof mod.getUUID==='function')result.type_id=String(mod.getUUID());"
        "if(!result.type_id||!result.type_id.trim())throw new Error('Widget '+widgetName+' has no hardcoded UUID (type_id/getUUID missing)');"
        "}"
        "}"
        "if(hook==='render'){"
        "let html='';"
        "if(typeof result==='string')html=result;"
        "else if(result&&typeof result==='object'&&typeof result.html==='string')html=result.html;"
        "else throw new Error('render() must return a string or { html: string }');"
        "fs.writeFileSync(responseFile,JSON.stringify({status:'success',html,result:{html}}),'utf8');"
        "return;"
        "}"
        "fs.writeFileSync(responseFile,JSON.stringify({status:'success',missing_hook:false,result}),'utf8');"
        "})().catch((err)=>{"
        "const responseFile=process.argv[2];"
        "const payload=JSON.stringify({status:'error',message:(err&&err.message)?err.message:'Unknown Node runner error'});"
        "if(responseFile){try{fs.writeFileSync(responseFile,payload,'utf8');}catch(_){process.stderr.write(payload+'\\n');}}"
        "else{process.stderr.write(payload+'\\n');}"
        "process.exitCode=1;"
        "});";

    std::vector<std::string> args;
    args.emplace_back("blogi-editor-node");
    args.emplace_back(reqPath);
    args.emplace_back(resPath);

    bool initialized = false;
    auto teardown = [&initialized]() {
        if (initialized) {
            node::TearDownOncePerProcess();
            initialized = false;
function getMod(scriptPath) {
  const p = path.resolve(scriptPath);
  if (loaded.has(p)) return { mod: loaded.get(p), p };
  const mod = require(p);
  loaded.set(p, mod);
  return { mod, p };
}
    };

    auto init = node::InitializeOncePerProcess(args);
    if (!init) {
        std::cerr << "Node bridge init failed: InitializeOncePerProcess returned null" << std::endl;
        return 1;
globalThis.__blogiEmbeddedRun = async function(reqPath, resPath) {
  try {
    const req = JSON.parse(fs.readFileSync(reqPath, 'utf8'));
    const hook = (typeof req.hook === 'string' && req.hook) ? req.hook : 'render';
    if (!req.script_path) throw new Error('request.script_path is required');

    const r = getMod(req.script_path);
    const fn = resolveHook(r.mod, hook);
    if (!fn) {
      fs.writeFileSync(resPath, JSON.stringify({ status: 'success', missing_hook: true, result: null }), 'utf8');
      return true;
    }
    initialized = true;

    int rc = init->exit_code();
    if (!init->errors().empty()) {
        for (const auto &err : init->errors()) {
            std::cerr << "Node bridge init warning/error: " << err << std::endl;
    const result = await Promise.resolve(fn(req));

    if (hook === 'metadata' && result && typeof result === 'object') {
      if (!result.type_id || typeof result.type_id !== 'string' || !result.type_id.trim()) {
        const scriptBase = path.basename(r.p, path.extname(r.p));
        const widgetName = (typeof result.name === 'string' && result.name.trim()) ? result.name.trim() : scriptBase;
        if (typeof r.mod.getUUID === 'function') result.type_id = String(r.mod.getUUID());
        if (!result.type_id || !result.type_id.trim()) {
          throw new Error('Widget ' + widgetName + ' has no hardcoded UUID (type_id/getUUID missing)');
        }
      }
    if (init->early_return()) {
        std::cerr << "Node bridge init aborted early with exit code " << rc << std::endl;
        teardown();
        return rc;
    }

    std::vector<std::string> envErrors;
    auto setup = node::CommonEnvironmentSetup::Create(
        init->platform(),
        &envErrors,
        init->args(),
        init->exec_args());
    if (!envErrors.empty()) {
        for (const auto &err : envErrors) {
            std::cerr << "Node bridge environment error: " << err << std::endl;
    if (hook === 'render') {
      let html = '';
      if (typeof result === 'string') html = result;
      else if (result && typeof result === 'object' && typeof result.html === 'string') html = result.html;
      else throw new Error('render() must return a string or { html: string }');

      fs.writeFileSync(resPath, JSON.stringify({ status: 'success', html, result: { html } }), 'utf8');
      return true;
    }

    fs.writeFileSync(resPath, JSON.stringify({ status: 'success', missing_hook: false, result }), 'utf8');
    return true;
  } catch (err) {
    const msg = (err && err.message) ? err.message : 'Unknown embedded runner error';
    try {
      fs.writeFileSync(resPath, JSON.stringify({ status: 'error', message: msg }), 'utf8');
    } catch (_) {
      process.stderr.write(msg + '\n');
    }
    return false;
  }
    if (!setup) {
        std::cerr << "Node bridge setup failed: CommonEnvironmentSetup::Create returned null" << std::endl;
        teardown();
};
)JS";

class EmbeddedRuntime {
public:
    int runRequest(const std::string &reqPath, const std::string &resPath) {
        std::lock_guard<std::mutex> lock(mutex_);
        if (!ensureInitializedLocked()) {
            return 1;
        }

    v8::Locker locker(setup->isolate());
    v8::Isolate::Scope isolateScope(setup->isolate());
    v8::HandleScope handleScope(setup->isolate());
    v8::Context::Scope contextScope(setup->context());
        auto *isolate = setup_->isolate();
        auto context = setup_->context();

    bool runtimeOk = true;
    {
        v8::Locker locker(setup->isolate());
        v8::Isolate::Scope isolateScope(setup->isolate());
        v8::HandleScope handleScope(setup->isolate());
        v8::Context::Scope contextScope(setup->context());

        if (node::LoadEnvironment(setup->env(), bridgeScript).IsEmpty()) {
            std::cerr << "Node bridge runtime error: LoadEnvironment returned empty result" << std::endl;
            runtimeOk = false;
        } else {
            auto loopResult = node::SpinEventLoop(setup->env());
            if (loopResult.IsNothing()) {
                std::cerr << "Node bridge runtime error: SpinEventLoop returned no exit code" << std::endl;
                runtimeOk = false;
            } else {
                rc = loopResult.FromJust();
        v8::Locker locker(isolate);
        v8::Isolate::Scope isolateScope(isolate);
        v8::HandleScope handleScope(isolate);
        v8::Context::Scope contextScope(context);

        auto fn = runFunction_.Get(isolate);
        v8::Local<v8::Value> argv[2] = {
            v8::String::NewFromUtf8(isolate, reqPath.c_str()).ToLocalChecked(),
            v8::String::NewFromUtf8(isolate, resPath.c_str()).ToLocalChecked()
        };

        if (fn->Call(context, context->Global(), 2, argv).IsEmpty()) {
            std::cerr << "Node embedded runtime error: run function call failed" << std::endl;
            return 1;
        }

        auto loopResult = node::SpinEventLoop(setup_->env());
        if (loopResult.IsNothing()) {
            std::cerr << "Node embedded runtime error: SpinEventLoop returned no exit code" << std::endl;
            return 1;
        }

        return loopResult.FromJust();
    }

    setup.reset();
    teardown();
    return runtimeOk ? rc : 1;
private:
    bool ensureInitializedLocked() {
        if (initialized_) {
            return true;
        }

inline bool isBridgeRunnerInvocation(int argc, char *argv[]) {
    return argc == 4 && argv != nullptr && argv[1] != nullptr && std::strcmp(argv[1], kBridgeRunnerArg) == 0;
        std::vector<std::string> args = {"blogi-editor-embedded"};
        init_ = node::InitializeOncePerProcess(args);
        if (!init_) {
            std::cerr << "Node embedded init failed: InitializeOncePerProcess returned null" << std::endl;
            return false;
        }

inline int runNodeRunnerFromArgs(int argc, char *argv[]) {
    if (!isBridgeRunnerInvocation(argc, argv)) {
        return -1;
        if (!init_->errors().empty()) {
            for (const auto &err : init_->errors()) {
                std::cerr << "Node embedded init warning/error: " << err << std::endl;
            }
    return runNodeRunnerInProcess(argv[2], argv[3]);
        }

inline int runNodeRunner(const std::string &reqPath,
                         const std::string &resPath) {
    const std::string selfPath = getSelfExecutablePath();
    if (selfPath.empty()) {
        return -1;
        if (init_->early_return()) {
            std::cerr << "Node embedded init aborted early with exit code " << init_->exit_code() << std::endl;
            node::TearDownOncePerProcess();
            init_.reset();
            return false;
        }

#ifdef _WIN32
    std::string cmdLine = "\"" + selfPath + "\" " + std::string(kBridgeRunnerArg) + " \"" + reqPath + "\" \"" + resPath + "\"";
    std::vector<char> mutableCmd(cmdLine.begin(), cmdLine.end());
    mutableCmd.push_back('\0');

    STARTUPINFOA si;
    PROCESS_INFORMATION pi;
    std::memset(&si, 0, sizeof(si));
    std::memset(&pi, 0, sizeof(pi));
    si.cb = sizeof(si);

    BOOL ok = CreateProcessA(
        selfPath.c_str(),
        mutableCmd.data(),
        nullptr,
        nullptr,
        FALSE,
        0,
        nullptr,
        nullptr,
        &si,
        &pi);
    if (!ok) {
        return -1;
        std::vector<std::string> envErrors;
        setup_ = node::CommonEnvironmentSetup::Create(
            init_->platform(),
            &envErrors,
            init_->args(),
            init_->exec_args());
        if (!envErrors.empty()) {
            for (const auto &err : envErrors) {
                std::cerr << "Node embedded environment error: " << err << std::endl;
            }
        }
        if (!setup_) {
            node::TearDownOncePerProcess();
            init_.reset();
            return false;
        }

    WaitForSingleObject(pi.hProcess, INFINITE);
    DWORD exitCode = 1;
    GetExitCodeProcess(pi.hProcess, &exitCode);
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    return static_cast<int>(exitCode);
#else
    pid_t pid = fork();
    if (pid < 0) {
        return -1;
        auto *isolate = setup_->isolate();
        auto context = setup_->context();

        {
            v8::Locker locker(isolate);
            v8::Isolate::Scope isolateScope(isolate);
            v8::HandleScope handleScope(isolate);
            v8::Context::Scope contextScope(context);

            if (node::LoadEnvironment(setup_->env(), kEmbeddedBootstrapScript).IsEmpty()) {
                std::cerr << "Node embedded bootstrap failed: LoadEnvironment returned empty" << std::endl;
                setup_.reset();
                node::TearDownOncePerProcess();
                init_.reset();
                return false;
            }

    if (pid == 0) {
        execl(selfPath.c_str(),
              selfPath.c_str(),
              kBridgeRunnerArg,
              reqPath.c_str(),
              resPath.c_str(),
              static_cast<char *>(nullptr));
        _exit(127);
            v8::Local<v8::Value> fnVal;
            auto fnName = v8::String::NewFromUtf8(isolate, "__blogiEmbeddedRun").ToLocalChecked();
            if (!context->Global()->Get(context, fnName).ToLocal(&fnVal) || !fnVal->IsFunction()) {
                std::cerr << "Node embedded bootstrap failed: __blogiEmbeddedRun not found" << std::endl;
                setup_.reset();
                node::TearDownOncePerProcess();
                init_.reset();
                return false;
            }

    int status = 0;
    if (waitpid(pid, &status, 0) < 0) {
        return -1;
            runFunction_.Reset(isolate, fnVal.As<v8::Function>());
        }

    if (WIFEXITED(status)) {
        return WEXITSTATUS(status);
        initialized_ = true;
        return true;
    }
    if (WIFSIGNALED(status)) {
        return 128 + WTERMSIG(status);

    std::mutex mutex_;
    bool initialized_ = false;
    std::shared_ptr<node::InitializationResult> init_;
    std::unique_ptr<node::CommonEnvironmentSetup> setup_;
    v8::Global<v8::Function> runFunction_;
};

inline EmbeddedRuntime &runtime() {
    static EmbeddedRuntime r;
    return r;
}
    return -1;
#endif

inline int runNodeRunner(const std::string &reqPath,
                         const std::string &resPath) {
    return runtime().runRequest(reqPath, resPath);
}

} // namespace blogi::webedit::nodebridge
} // namespace blogi::webedit::embedded
+2 −2
Original line number Diff line number Diff line
@@ -59,7 +59,7 @@ struct NodeWidgetDescriptor {
};

std::string getEnvOrDefault(const char *key, const char *fallback) {
    return blogi::webedit::nodebridge::getEnvOrDefault(key, fallback);
    return blogi::webedit::embedded::getEnvOrDefault(key, fallback);
}

std::string makeTempPath(const std::string &name) {
@@ -99,7 +99,7 @@ bool queryNodeWidgetDescriptor(const std::string &scriptPath, NodeWidgetDescript
    writeFile(reqPath, reqJson ? reqJson : "{}");
    json_object_put(req);

    (void)blogi::webedit::nodebridge::runNodeRunner(reqPath, resPath);
    (void)blogi::webedit::embedded::runNodeRunner(reqPath, resPath);

    std::string raw = readFile(resPath);
    json_object *res = raw.empty() ? nullptr : json_tokener_parse(raw.c_str());
+1 −1
Original line number Diff line number Diff line
@@ -634,7 +634,7 @@ protected:
            writeFile(reqPath, reqJson ? reqJson : "{}");
            json_object_put(req);

            int rc = blogi::webedit::nodebridge::runNodeRunner(reqPath, resPath);
            int rc = blogi::webedit::embedded::runNodeRunner(reqPath, resPath);
            (void)rc;

            std::string resRaw = readFile(resPath);