347 lines
10 KiB
C++
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);
|
|
}
|