4#include <condition_variable>
16#include "common/config.h"
18#ifndef EVENGINE_WEBGPU
19#include <Poco/JSON/Array.h>
20#include <Poco/JSON/Object.h>
21#include <Poco/JSON/Stringifier.h>
22#include <Poco/Net/NetException.h>
23#include <Poco/Net/NetworkInterface.h>
24#include <Poco/Net/ServerSocket.h>
25#include <Poco/Net/SocketAddress.h>
26#include <Poco/Net/StreamSocket.h>
27#include <Poco/Timespan.h>
31using namespace std::filesystem;
36#ifndef EVENGINE_WEBGPU
50 ~DevFileServer() { stop(); }
52 bool start(uint16_t port, std::string root) {
54 root_ = std::move(root);
55 if (root_.empty()) root_ =
".";
58 Poco::Net::SocketAddress addr(port);
59 server_ = std::make_unique<Poco::Net::ServerSocket>(addr);
63 boundPort_ =
static_cast<uint16_t
>(server_->address().port());
65 thread_ = std::thread([
this]() { serveLoop(); });
70 if (!running_.load()) {
71 if (thread_.joinable()) thread_.join();
74 running_.store(
false);
81 if (thread_.joinable()) thread_.join();
84 uint16_t port()
const {
return boundPort_; }
88 while (running_.load()) {
89 Poco::Net::StreamSocket sock;
91 sock = server_->acceptConnection();
95 sock.setReceiveTimeout(Poco::Timespan(3, 0));
96 sock.setSendTimeout(Poco::Timespan(3, 0));
109 std::string readRequest(Poco::Net::StreamSocket& sock) {
112 while (buf.find(
"\r\n\r\n") == std::string::npos && buf.size() < 1u << 16) {
113 int n = sock.receiveBytes(chunk,
sizeof(chunk));
114 if (
n <= 0)
return {};
115 buf.append(chunk,
static_cast<size_t>(
n));
120 void sendResponse(Poco::Net::StreamSocket& sock,
int status,
const string& reason,
121 const string& contentType,
const string&
body) {
122 std::ostringstream head;
123 head <<
"HTTP/1.1 " <<
status <<
" " << reason <<
"\r\n"
124 <<
"Content-Type: " << contentType <<
"\r\n"
125 <<
"Content-Length: " <<
body.size() <<
"\r\n"
126 <<
"Connection: close\r\n"
127 <<
"Cache-Control: no-store\r\n"
129 const string header = head.str();
130 sock.sendBytes(header.data(),
static_cast<int>(header.size()));
131 if (!
body.empty()) sock.sendBytes(
body.data(),
static_cast<int>(
body.size()));
134 static string urlDecode(
const string&
s) {
137 Poco::URI::decode(
s, out);
144 bool safeRelPath(
const string& rel,
string& out)
const {
145 if (rel.empty())
return false;
147 if (rel[0] ==
'/' || rel.find(
"..") != string::npos || rel.find(
'\\') != string::npos)
return false;
148 path base = path(root_) / path(rel).lexically_normal();
149 if (base.empty())
return false;
151 const path rootAbs = absolute(path(root_)).lexically_normal();
152 const path target = absolute(base).lexically_normal();
153 auto rp = rootAbs.begin();
154 auto tp = target.begin();
155 for (; rp != rootAbs.end() && tp != target.end(); ++rp, ++tp) {
156 if (*rp != *tp)
return false;
158 if (rp != rootAbs.end())
return false;
159 out = target.string();
163 void handle(Poco::Net::StreamSocket& sock) {
164 const string req = readRequest(sock);
165 if (req.empty())
return;
168 const size_t sp1 = req.find(
' ');
169 if (sp1 == string::npos)
return;
170 const size_t sp2 = req.find(
' ', sp1 + 1);
171 if (sp2 == string::npos)
return;
172 const string method = req.substr(0, sp1);
173 string path = req.substr(sp1 + 1, sp2 - sp1 - 1);
174 if (method !=
"GET") {
175 sendResponse(sock, 405,
"Method Not Allowed",
"text/plain",
"only GET supported\n");
179 if (path ==
"/ping") {
180 sendResponse(sock, 200,
"OK",
"text/plain",
"ok\n");
183 if (path ==
"/" || path ==
"/manifest") {
184 sendResponse(sock, 200,
"OK",
"application/json", manifestJson());
187 if (path.rfind(
"/raw/", 0) == 0) {
188 const string rel = urlDecode(path.substr(5));
190 if (!safeRelPath(rel, real)) {
191 sendResponse(sock, 400,
"Bad Request",
"text/plain",
"bad path\n");
195 if (!is_regular_file(real, ec)) {
196 sendResponse(sock, 404,
"Not Found",
"text/plain",
"not found\n");
199 std::ifstream ifs(real, std::ios::binary);
201 sendResponse(sock, 404,
"Not Found",
"text/plain",
"not found\n");
204 std::ostringstream oss;
206 sendResponse(sock, 200,
"OK",
"application/octet-stream", oss.str());
209 sendResponse(sock, 404,
"Not Found",
"text/plain",
"unknown endpoint\n");
212 string manifestJson() {
213 Poco::JSON::Array::Ptr arr =
new Poco::JSON::Array();
215 if (is_directory(path(root_), ec)) {
216 std::vector<path> stack{path(root_)};
217 while (!stack.empty()) {
218 const path
dir = stack.back();
221 std::copy(directory_iterator(
dir, ec), directory_iterator(), back_inserter(
children));
227 const file_status st = symlink_status(
p, ec);
232 if (is_directory(st)) {
234 }
else if (is_regular_file(st)) {
235 const std::string rel = relative(
p, path(root_)).generic_string();
236 if (rel ==
".eve-manifest.json")
continue;
238 const auto sz = file_size(
p, fec);
241 const auto mt = last_write_time(
p, mec);
243 Poco::JSON::Object::Ptr o =
new Poco::JSON::Object();
245 o->set(
"size",
static_cast<Poco::Int64
>(sz));
246 o->set(
"mtime",
static_cast<Poco::Int64
>(
247 std::chrono::duration_cast<std::chrono::seconds>(
248 mt.time_since_epoch())
255 std::ostringstream oss;
256 Poco::JSON::Stringifier::stringify(arr, oss, 0, 0);
261 std::unique_ptr<Poco::Net::ServerSocket> server_;
263 std::atomic<bool> running_{
false};
264 uint16_t boundPort_ = 0;
267std::vector<string> lanIPv4s() {
268 std::vector<string> out;
270 const auto list = Poco::Net::NetworkInterface::list();
271 for (
const auto& ni :
list) {
272 if (!ni.isUp() || ni.isLoopback())
continue;
273 const auto addrs = ni.addressList();
274 for (
const auto&
a : addrs) {
275 const auto& addr =
a.get<0>();
276 if (addr.family() == Poco::Net::IPAddress::IPv4 && !addr.isLoopback())
277 out.push_back(addr.toString());
290 void setup(CLI::App& app, std::shared_ptr<CLI::Formatter> formatter)
override {
291 auto create = app.add_subcommand(
"dev",
"Start a development server for the current game");
292 create->allow_extras();
293 create->add_option(
"--port",
port,
"HTTP port to listen on (default 8765)");
294 create->formatter(formatter);
298 auto create = app.get_subcommand(
"dev");
299 if (create->parsed()) {
313 if (!path.empty() && path !=
".") {
315 if (!is_directory(path, ec)) {
316 cerr <<
"eve dev: not a directory: " << path << endl;
321 DevFileServer server;
322 if (!server.start(port, path.empty() ? std::string(
".") : path)) {
323 cerr <<
"eve dev: cannot bind port " << port << endl;
327 cout <<
"eve dev: serving '" << (path.empty() ? std::string(
".") : path) <<
"' on port "
328 << server.port() << endl;
329 const auto ips = lanIPv4s();
330 for (
const auto& ip : ips)
331 cout <<
" device config.devServer = \"http://" << ip <<
":" << server.port() <<
"\"" << endl;
332 cout <<
" Ctrl+C to stop" << endl;
336 std::condition_variable cv;
337 std::unique_lock<std::mutex> lk(
m);
338 cv.wait(lk, [] {
return false; });
347 void setup(CLI::App& app, std::shared_ptr<CLI::Formatter> formatter)
override {
348 auto create = app.add_subcommand(
"dev",
"Start a development server for the current game");
349 create->allow_extras();
350 create->add_option(
"--port",
port,
"HTTP port to listen on (default 8765)");
351 create->formatter(formatter);
354 int parse(CLI::App& app, Cmdline& cmd)
override {
355 auto create = app.get_subcommand(
"dev");
356 if (create->parsed()) {
357 string path = cmd.get_remaining(create,
".");
358 int res = cmd.DevServer(path,
port);
368 cerr <<
"eve dev: not supported on this platform" << endl;
命令行模块(eve.cmd):run / build / package / test / zip / dev-server 等子命令入口。
static std::string get_remaining(CLI::App *sub, std::string default_path=".")
子命令剩余位置参数。
int DevServer(std::string path, int port=8765)
启动热重载开发服务器。
WidgetDesc list(std::string listId, const std::vector< std::string > &items, const std::function< WidgetDesc(const std::string &, int)> &itemFn)
Expand a string list into a Group of item widgets. itemFn(label, index) builds each row; keys default...
int parse(CLI::App &app, Cmdline &cmd) override
Parses arguments and runs the command; returns the exit code.
void setup(CLI::App &app, std::shared_ptr< CLI::Formatter > formatter) override
Registers options on the CLI11 sub-app.
One eve <subcommand> handler: CLI setup + argument parsing.