载入中...
搜索中...
未找到
DevServer.cpp
浏览该文件的文档.
1#include <CLI11.hpp>
2#include <atomic>
3#include <chrono>
4#include <condition_variable>
5#include <filesystem>
6#include <fstream>
7#include <iostream>
8#include <memory>
9#include <sstream>
10#include <string>
11#include <thread>
12#include <vector>
13#include "cmdline.h"
14// EVENGINE_WEBGPU comes from common/config.h (cmakedefine), so it must be
15// included before any Poco headers the #ifndef guard depends on.
16#include "common/config.h"
17
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>
28#include <Poco/URI.h>
29#endif
30
31using namespace std::filesystem;
32using namespace std;
33
34namespace eve::cmd {
35
36#ifndef EVENGINE_WEBGPU
37
38namespace {
39
48class DevFileServer {
49public:
50 ~DevFileServer() { stop(); }
51
52 bool start(uint16_t port, std::string root) {
53 stop();
54 root_ = std::move(root);
55 if (root_.empty()) root_ = ".";
56 // Bind all interfaces so mobile devices on the same LAN can connect.
57 try {
58 Poco::Net::SocketAddress addr(port);
59 server_ = std::make_unique<Poco::Net::ServerSocket>(addr);
60 } catch (...) {
61 return false;
62 }
63 boundPort_ = static_cast<uint16_t>(server_->address().port());
64 running_.store(true);
65 thread_ = std::thread([this]() { serveLoop(); });
66 return true;
67 }
68
69 void stop() {
70 if (!running_.load()) {
71 if (thread_.joinable()) thread_.join();
72 return;
73 }
74 running_.store(false);
75 if (server_) {
76 try {
77 server_->close(); // unblocks acceptConnection
78 } catch (...) {
79 }
80 }
81 if (thread_.joinable()) thread_.join();
82 }
83
84 uint16_t port() const { return boundPort_; }
85
86private:
87 void serveLoop() {
88 while (running_.load()) {
89 Poco::Net::StreamSocket sock;
90 try {
91 sock = server_->acceptConnection();
92 } catch (...) {
93 continue; // closed socket during stop()
94 }
95 sock.setReceiveTimeout(Poco::Timespan(3, 0));
96 sock.setSendTimeout(Poco::Timespan(3, 0));
97 try {
98 handle(sock);
99 } catch (...) {
100 }
101 try {
102 sock.close();
103 } catch (...) {
104 }
105 }
106 }
107
108 // Read the request line + headers (up to "\r\n\r\n") from a single client.
109 std::string readRequest(Poco::Net::StreamSocket& sock) {
110 std::string buf;
111 char chunk[4096];
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));
116 }
117 return buf;
118 }
119
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"
128 << "\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()));
132 }
133
134 static string urlDecode(const string& s) {
135 try {
136 string out;
137 Poco::URI::decode(s, out);
138 return out;
139 } catch (...) {
140 return s;
141 }
142 }
143
144 bool safeRelPath(const string& rel, string& out) const {
145 if (rel.empty()) return false;
146 // Reject traversal / absolute / backslash forms.
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;
150 // Confirm the resolved path stays inside the root.
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;
157 }
158 if (rp != rootAbs.end()) return false;
159 out = target.string();
160 return true;
161 }
162
163 void handle(Poco::Net::StreamSocket& sock) {
164 const string req = readRequest(sock);
165 if (req.empty()) return;
166
167 // Request line: METHOD SP PATH SP HTTP/x.y
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");
176 return;
177 }
178
179 if (path == "/ping") {
180 sendResponse(sock, 200, "OK", "text/plain", "ok\n");
181 return;
182 }
183 if (path == "/" || path == "/manifest") {
184 sendResponse(sock, 200, "OK", "application/json", manifestJson());
185 return;
186 }
187 if (path.rfind("/raw/", 0) == 0) {
188 const string rel = urlDecode(path.substr(5));
189 string real;
190 if (!safeRelPath(rel, real)) {
191 sendResponse(sock, 400, "Bad Request", "text/plain", "bad path\n");
192 return;
193 }
194 std::error_code ec;
195 if (!is_regular_file(real, ec)) {
196 sendResponse(sock, 404, "Not Found", "text/plain", "not found\n");
197 return;
198 }
199 std::ifstream ifs(real, std::ios::binary);
200 if (!ifs) {
201 sendResponse(sock, 404, "Not Found", "text/plain", "not found\n");
202 return;
203 }
204 std::ostringstream oss;
205 oss << ifs.rdbuf();
206 sendResponse(sock, 200, "OK", "application/octet-stream", oss.str());
207 return;
208 }
209 sendResponse(sock, 404, "Not Found", "text/plain", "unknown endpoint\n");
210 }
211
212 string manifestJson() {
213 Poco::JSON::Array::Ptr arr = new Poco::JSON::Array();
214 std::error_code ec;
215 if (is_directory(path(root_), ec)) {
216 std::vector<path> stack{path(root_)};
217 while (!stack.empty()) {
218 const path dir = stack.back();
219 stack.pop_back();
220 std::vector<path> children;
221 std::copy(directory_iterator(dir, ec), directory_iterator(), back_inserter(children));
222 if (ec) {
223 ec.clear();
224 continue;
225 }
226 for (const auto& p : children) {
227 const file_status st = symlink_status(p, ec);
228 if (ec) {
229 ec.clear();
230 continue;
231 }
232 if (is_directory(st)) {
233 stack.push_back(p);
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;
237 std::error_code fec;
238 const auto sz = file_size(p, fec);
239 if (fec) continue;
240 std::error_code mec;
241 const auto mt = last_write_time(p, mec);
242 if (mec) continue;
243 Poco::JSON::Object::Ptr o = new Poco::JSON::Object();
244 o->set("path", rel);
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())
249 .count()));
250 arr->add(o);
251 }
252 }
253 }
254 }
255 std::ostringstream oss;
256 Poco::JSON::Stringifier::stringify(arr, oss, 0, 0);
257 return oss.str();
258 }
259
260 std::string root_;
261 std::unique_ptr<Poco::Net::ServerSocket> server_;
262 std::thread thread_;
263 std::atomic<bool> running_{false};
264 uint16_t boundPort_ = 0;
265};
266
267std::vector<string> lanIPv4s() {
268 std::vector<string> out;
269 try {
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());
278 }
279 }
280 } catch (...) {
281 }
282 return out;
283}
284
285} // namespace
286
288 int port = 8765;
289
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);
295 }
296
297 int parse(CLI::App& app, Cmdline& cmd) override {
298 auto create = app.get_subcommand("dev");
299 if (create->parsed()) {
300 string path = cmd.get_remaining(create, ".");
301 int res = cmd.DevServer(path, port);
302 return res;
303 }
304 return -1; // not handle
305 }
306};
307
309
310
311int Cmdline::DevServer(std::string path, int port) {
312
313 if (!path.empty() && path != ".") {
314 std::error_code ec;
315 if (!is_directory(path, ec)) {
316 cerr << "eve dev: not a directory: " << path << endl;
317 return 2;
318 }
319 }
320
321 DevFileServer server;
322 if (!server.start(port, path.empty() ? std::string(".") : path)) {
323 cerr << "eve dev: cannot bind port " << port << endl;
324 return 2;
325 }
326
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;
333
334 // Block until interrupted; the serving happens on a background thread.
335 std::mutex m;
336 std::condition_variable cv;
337 std::unique_lock<std::mutex> lk(m);
338 cv.wait(lk, [] { return false; });
339 return 0;
340}
341
342#else // EVENGINE_WEBGPU
343
344struct DevServerArgs : Handler {
345 int port = 8765;
346
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);
352 }
353
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);
359 return res;
360 }
361 return -1; // not handle
362 }
363};
364
365CMD_REG(DevServerArgs);
366
367int Cmdline::DevServer(std::string, int) {
368 cerr << "eve dev: not supported on this platform" << endl;
369 return 2;
370}
371
372#endif // EVENGINE_WEBGPU
373
374} // namespace eve::cmd
glm::vec3 n
Definition Grass.cpp:64
JobFunc body
JobStatus status
uint32_t a
glm::vec4 p[6]
int children
Definition TreeMesh.cpp:177
V3 dir
Definition TreeMesh.cpp:121
float m[16]
uint32_t s
Definition Weather.cpp:28
命令行模块(eve.cmd):run / build / package / test / zip / dev-server 等子命令入口。
Definition cmdline.h:30
static std::string get_remaining(CLI::App *sub, std::string default_path=".")
子命令剩余位置参数。
Definition cmdline.cpp:79
int DevServer(std::string path, int port=8765)
启动热重载开发服务器。
#define CMD_REG(name)
Definition cmdline.h:92
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...
Definition Widget.cpp:457
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.
Definition cmdline.h:21