CreoPluging9.0/HttpServer.cpp
2026-01-15 16:23:23 +08:00

347 lines
10 KiB
C++

#include "pch.h"
#include "HttpServer.h"
#include "JsonLite.h"
#include "Shrinkwrap90Manager.h"
#include <pfcExceptions.h>
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <iomanip>
#include <sstream>
#include <vector>
namespace {
std::string ToLower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return s;
}
bool GetString(const jsonlite::Object& obj, const char* key, std::string& out) {
auto it = obj.find(key);
if (it == obj.end()) return false;
if (it->second.type != jsonlite::Value::Type::String) return false;
out = it->second.s;
return true;
}
bool GetBool(const jsonlite::Object& obj, const char* key, bool& out) {
auto it = obj.find(key);
if (it == obj.end()) return false;
if (it->second.type != jsonlite::Value::Type::Bool) return false;
out = it->second.b;
return true;
}
bool GetNumber(const jsonlite::Object& obj, const char* key, double& out) {
auto it = obj.find(key);
if (it == obj.end()) return false;
if (it->second.type != jsonlite::Value::Type::Number) return false;
out = it->second.n;
return true;
}
std::string BuildHttpResponse(int status, const std::string& body) {
const char* reason = "OK";
if (status == 400) reason = "Bad Request";
else if (status == 404) reason = "Not Found";
else if (status == 405) reason = "Method Not Allowed";
else if (status == 500) reason = "Internal Server Error";
std::ostringstream oss;
oss << "HTTP/1.1 " << status << " " << reason << "\r\n";
oss << "Content-Type: application/json; charset=utf-8\r\n";
oss << "Connection: close\r\n";
oss << "Content-Length: " << body.size() << "\r\n";
oss << "\r\n";
oss << body;
return oss.str();
}
std::string JsonError(const std::string& message) {
std::ostringstream oss;
oss << "{\"success\":false,\"data\":null,\"error\":\"" << jsonlite::EscapeString(message) << "\"}";
return oss.str();
}
std::string JsonSuccess(const ShrinkwrapShellResult& data) {
std::ostringstream oss;
oss << "{\"success\":true,\"data\":{";
oss << "\"output_path\":\"" << jsonlite::EscapeString(data.output_path) << "\",";
oss << "\"file_size\":\"" << jsonlite::EscapeString(data.file_size) << "\",";
oss << "\"execution_time\":\"" << jsonlite::EscapeString(data.execution_time) << "\"";
oss << "},\"error\":null}";
return oss.str();
}
} // namespace
HttpServer::HttpServer() = default;
HttpServer::~HttpServer() {
Stop();
}
bool HttpServer::Start(int port, std::string* err, TriggerCallback trigger) {
if (thread_.joinable()) {
if (err) *err = "already started";
return false;
}
port_ = port;
stop_ = false;
trigger_callback_ = trigger;
WSADATA wsa{};
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
if (err) *err = "WSAStartup failed";
return false;
}
listen_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listen_ == INVALID_SOCKET) {
if (err) *err = "socket failed";
WSACleanup();
return false;
}
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = htons(static_cast<u_short>(port_));
int opt = 1;
setsockopt(listen_, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&opt), sizeof(opt));
if (::bind(listen_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == SOCKET_ERROR) {
if (err) *err = "bind failed";
closesocket(listen_);
listen_ = INVALID_SOCKET;
WSACleanup();
return false;
}
if (listen(listen_, SOMAXCONN) == SOCKET_ERROR) {
if (err) *err = "listen failed";
closesocket(listen_);
listen_ = INVALID_SOCKET;
WSACleanup();
return false;
}
thread_ = std::thread([this] { Run(); });
return true;
}
void HttpServer::Stop() {
stop_ = true;
if (listen_ != INVALID_SOCKET) {
closesocket(listen_);
listen_ = INVALID_SOCKET;
}
task_cv_.notify_all();
if (thread_.joinable()) {
thread_.join();
}
WSACleanup();
}
void HttpServer::Run() {
while (!stop_) {
SOCKET client = accept(listen_, nullptr, nullptr);
if (client == INVALID_SOCKET) {
if (stop_) break;
continue;
}
HandleClient(client);
closesocket(client);
}
}
bool HttpServer::HasPendingTask() const {
std::lock_guard<std::mutex> lock(task_mutex_);
return pending_task_ != nullptr && !pending_task_->completed;
}
void HttpServer::ProcessPendingTask() {
std::lock_guard<std::mutex> lock(task_mutex_);
if (!pending_task_ || pending_task_->completed) return;
try {
auto start = std::chrono::steady_clock::now();
Shrinkwrap90Manager mgr;
pending_task_->result = mgr.ExportShell(pending_task_->request);
auto end = std::chrono::steady_clock::now();
auto secs = std::chrono::duration_cast<std::chrono::seconds>(end - start).count();
pending_task_->result.execution_time = Shrinkwrap90Manager::FormatHhMmSs(secs);
pending_task_->success = true;
} catch (pfcXPFC& ex) {
xstring msg = ex.GetMessage();
const char* msgPtr = msg;
pending_task_->error = (msgPtr && msgPtr[0]) ? std::string(msgPtr) : "Creo OTK error";
pending_task_->success = false;
} catch (xthrowable&) {
pending_task_->error = "Creo OTK exception";
pending_task_->success = false;
} catch (const std::exception& ex) {
pending_task_->error = ex.what();
pending_task_->success = false;
} catch (...) {
pending_task_->error = "unknown error";
pending_task_->success = false;
}
pending_task_->completed = true;
task_cv_.notify_all();
}
static bool ReadExact(SOCKET s, std::string& buf, size_t bytes) {
while (buf.size() < bytes) {
char tmp[4096];
int want = static_cast<int>(std::min<size_t>(sizeof(tmp), bytes - buf.size()));
int n = recv(s, tmp, want, 0);
if (n <= 0) return false;
buf.append(tmp, tmp + n);
}
return true;
}
void HttpServer::HandleClient(SOCKET client) {
std::string raw;
raw.reserve(8192);
while (raw.find("\r\n\r\n") == std::string::npos) {
char tmp[4096];
int n = recv(client, tmp, sizeof(tmp), 0);
if (n <= 0) return;
raw.append(tmp, tmp + n);
if (raw.size() > 1024 * 1024) {
auto resp = BuildHttpResponse(400, JsonError("request too large"));
send(client, resp.data(), static_cast<int>(resp.size()), 0);
return;
}
}
size_t headerEnd = raw.find("\r\n\r\n");
std::string header = raw.substr(0, headerEnd);
std::string body = raw.substr(headerEnd + 4);
std::istringstream hs(header);
std::string requestLine;
if (!std::getline(hs, requestLine)) return;
if (!requestLine.empty() && requestLine.back() == '\r') requestLine.pop_back();
std::istringstream rl(requestLine);
std::string method, path, version;
rl >> method >> path >> version;
std::string line;
size_t contentLength = 0;
while (std::getline(hs, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
auto pos = line.find(':');
if (pos == std::string::npos) continue;
std::string k = ToLower(line.substr(0, pos));
std::string v = line.substr(pos + 1);
while (!v.empty() && std::isspace(static_cast<unsigned char>(v[0]))) v.erase(v.begin());
if (k == "content-length") {
contentLength = static_cast<size_t>(std::strtoull(v.c_str(), nullptr, 10));
}
}
if (contentLength > 0 && body.size() < contentLength) {
std::string more;
if (!ReadExact(client, body, contentLength)) return;
}
if (contentLength > 0 && body.size() > contentLength) {
body.resize(contentLength);
}
if (method != "POST") {
auto resp = BuildHttpResponse(405, JsonError("only POST is supported"));
send(client, resp.data(), static_cast<int>(resp.size()), 0);
return;
}
if (path != "/api/creo/shrinkwrap/shell") {
auto resp = BuildHttpResponse(404, JsonError("not found"));
send(client, resp.data(), static_cast<int>(resp.size()), 0);
return;
}
jsonlite::Object obj;
std::string parseErr;
if (!jsonlite::ParseObject(body, obj, parseErr)) {
auto resp = BuildHttpResponse(400, JsonError("invalid json: " + parseErr));
send(client, resp.data(), static_cast<int>(resp.size()), 0);
return;
}
ShrinkwrapShellRequest req;
GetString(obj, "software_type", req.software_type);
double num = 0.0;
if (GetNumber(obj, "quality", num)) req.quality = static_cast<int>(num);
if (GetNumber(obj, "chord_height", num)) req.chord_height = num;
GetBool(obj, "fill_holes", req.fill_holes);
GetBool(obj, "ignore_small_surfaces", req.ignore_small_surfaces);
if (GetNumber(obj, "small_surface_percentage", num)) req.small_surface_percentage = num;
GetString(obj, "output_file_path", req.output_file_path);
GetString(obj, "output_type", req.output_type);
GetBool(obj, "ignore_quilts", req.ignore_quilts);
GetBool(obj, "ignore_skeleton", req.ignore_skeleton);
GetBool(obj, "assign_mass_properties", req.assign_mass_properties);
if (GetNumber(obj, "timeout_seconds", num)) req.timeout_seconds = static_cast<int>(num);
if (req.output_file_path.empty()) {
auto resp = BuildHttpResponse(400, JsonError("output_file_path is required"));
send(client, resp.data(), static_cast<int>(resp.size()), 0);
return;
}
PendingTask task;
task.request = req;
{
std::unique_lock<std::mutex> lock(task_mutex_);
if (pending_task_ != nullptr) {
auto resp = BuildHttpResponse(503, JsonError("server busy, another task is pending"));
send(client, resp.data(), static_cast<int>(resp.size()), 0);
return;
}
pending_task_ = &task;
}
if (trigger_callback_) {
trigger_callback_();
}
{
std::unique_lock<std::mutex> lock(task_mutex_);
int timeout_ms = req.timeout_seconds * 1000;
if (timeout_ms <= 0) timeout_ms = 60000;
bool completed = task_cv_.wait_for(lock, std::chrono::milliseconds(timeout_ms), [&task] {
return task.completed;
});
pending_task_ = nullptr;
if (!completed) {
auto resp = BuildHttpResponse(504, JsonError("task timeout"));
send(client, resp.data(), static_cast<int>(resp.size()), 0);
return;
}
}
if (!task.success) {
auto resp = BuildHttpResponse(500, JsonError(task.error));
send(client, resp.data(), static_cast<int>(resp.size()), 0);
return;
}
auto resp = BuildHttpResponse(200, JsonSuccess(task.result));
send(client, resp.data(), static_cast<int>(resp.size()), 0);
}