|
|
@@ -2,10 +2,12 @@
|
|
|
|
|
|
#include "../../utils.h"
|
|
|
|
|
|
+#include <cctype>
|
|
|
#include <fstream>
|
|
|
#include <iostream>
|
|
|
#include <iterator>
|
|
|
#include <sstream>
|
|
|
+#include <vector>
|
|
|
#include <pwd.h>
|
|
|
#include <unistd.h>
|
|
|
|
|
|
@@ -22,6 +24,319 @@ std::string work_dir() {
|
|
|
return join_path(software_root(), ".build");
|
|
|
}
|
|
|
|
|
|
+std::string trim_copy(std::string s) {
|
|
|
+ while (!s.empty() &&
|
|
|
+ (s.front() == ' ' || s.front() == '\t' || s.front() == '\r')) {
|
|
|
+ s.erase(s.begin());
|
|
|
+ }
|
|
|
+ while (!s.empty() &&
|
|
|
+ (s.back() == ' ' || s.back() == '\t' || s.back() == '\r' ||
|
|
|
+ s.back() == '\n')) {
|
|
|
+ s.pop_back();
|
|
|
+ }
|
|
|
+ return s;
|
|
|
+}
|
|
|
+
|
|
|
+bool read_conf_lines(std::vector<std::string>& lines, std::string& err) {
|
|
|
+ lines.clear();
|
|
|
+ if (!path_exists(conf_file())) {
|
|
|
+ err = "配置文件不存在";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ std::ifstream in(conf_file());
|
|
|
+ if (!in) {
|
|
|
+ err = "无法读取配置文件";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ std::string line;
|
|
|
+ while (std::getline(in, line)) {
|
|
|
+ if (!line.empty() && line.back() == '\r') {
|
|
|
+ line.pop_back();
|
|
|
+ }
|
|
|
+ lines.push_back(line);
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+bool write_conf_lines(const std::vector<std::string>& lines, std::string& err) {
|
|
|
+ std::ofstream out(conf_file(), std::ios::trunc);
|
|
|
+ if (!out) {
|
|
|
+ err = "无法写入配置文件";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ for (const auto& line : lines) {
|
|
|
+ out << line << '\n';
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+// Strip leading comment marker (#) and whitespace for directive matching.
|
|
|
+std::string directive_body(const std::string& line, bool& commented) {
|
|
|
+ commented = false;
|
|
|
+ std::string t = trim_copy(line);
|
|
|
+ if (t.empty()) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ if (t[0] == '#') {
|
|
|
+ commented = true;
|
|
|
+ t = trim_copy(t.substr(1));
|
|
|
+ }
|
|
|
+ return t;
|
|
|
+}
|
|
|
+
|
|
|
+bool line_directive(const std::string& line, std::string& key, std::string& value,
|
|
|
+ bool& commented) {
|
|
|
+ const std::string body = directive_body(line, commented);
|
|
|
+ if (body.empty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ size_t i = 0;
|
|
|
+ while (i < body.size() && body[i] != ' ' && body[i] != '\t' &&
|
|
|
+ body[i] != '{' && body[i] != ';' && body[i] != '}') {
|
|
|
+ ++i;
|
|
|
+ }
|
|
|
+ if (i == 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ key = body.substr(0, i);
|
|
|
+ std::string rest = trim_copy(body.substr(i));
|
|
|
+ if (!rest.empty() && rest.back() == ';') {
|
|
|
+ rest.pop_back();
|
|
|
+ rest = trim_copy(rest);
|
|
|
+ }
|
|
|
+ value = rest;
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+enum class ConfCtx { Main, Events, Http, Other };
|
|
|
+
|
|
|
+// Walk lines and report which top-level context each line is in.
|
|
|
+// events/http are only tracked at depth of the block itself (not nested server).
|
|
|
+void annotate_contexts(const std::vector<std::string>& lines,
|
|
|
+ std::vector<ConfCtx>& ctxs) {
|
|
|
+ ctxs.assign(lines.size(), ConfCtx::Main);
|
|
|
+ int depth = 0;
|
|
|
+ ConfCtx block = ConfCtx::Main;
|
|
|
+ for (size_t i = 0; i < lines.size(); ++i) {
|
|
|
+ bool commented = false;
|
|
|
+ std::string key;
|
|
|
+ std::string value;
|
|
|
+ line_directive(lines[i], key, value, commented);
|
|
|
+
|
|
|
+ ConfCtx here = ConfCtx::Main;
|
|
|
+ if (depth == 0) {
|
|
|
+ here = ConfCtx::Main;
|
|
|
+ } else if (depth == 1) {
|
|
|
+ here = block;
|
|
|
+ } else {
|
|
|
+ here = ConfCtx::Other;
|
|
|
+ }
|
|
|
+ ctxs[i] = here;
|
|
|
+
|
|
|
+ // Count braces on the line (ignore commented braces for simplicity:
|
|
|
+ // only look at active/non-commented structure markers).
|
|
|
+ const std::string raw = trim_copy(lines[i]);
|
|
|
+ if (!raw.empty() && raw[0] != '#') {
|
|
|
+ for (char c : raw) {
|
|
|
+ if (c == '{') {
|
|
|
+ if (depth == 0) {
|
|
|
+ if (key == "events") {
|
|
|
+ block = ConfCtx::Events;
|
|
|
+ } else if (key == "http") {
|
|
|
+ block = ConfCtx::Http;
|
|
|
+ } else {
|
|
|
+ block = ConfCtx::Other;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ++depth;
|
|
|
+ } else if (c == '}') {
|
|
|
+ if (depth > 0) {
|
|
|
+ --depth;
|
|
|
+ }
|
|
|
+ if (depth == 0) {
|
|
|
+ block = ConfCtx::Main;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void upsert_in_context(std::vector<std::string>& lines, ConfCtx want,
|
|
|
+ const std::string& key, const std::string& value,
|
|
|
+ const std::string& indent) {
|
|
|
+ std::vector<ConfCtx> ctxs;
|
|
|
+ annotate_contexts(lines, ctxs);
|
|
|
+
|
|
|
+ int first_active = -1;
|
|
|
+ int first_commented = -1;
|
|
|
+ for (size_t i = 0; i < lines.size(); ++i) {
|
|
|
+ if (ctxs[i] != want) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ bool commented = false;
|
|
|
+ std::string k;
|
|
|
+ std::string v;
|
|
|
+ if (!line_directive(lines[i], k, v, commented) || k != key) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (!commented && first_active < 0) {
|
|
|
+ first_active = static_cast<int>(i);
|
|
|
+ } else if (commented && first_commented < 0) {
|
|
|
+ first_commented = static_cast<int>(i);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const std::string new_line = indent + key + " " + value + ";";
|
|
|
+ if (first_active >= 0) {
|
|
|
+ lines[static_cast<size_t>(first_active)] = new_line;
|
|
|
+ // Comment out duplicate active directives in same context.
|
|
|
+ bool seen = false;
|
|
|
+ annotate_contexts(lines, ctxs);
|
|
|
+ for (size_t i = 0; i < lines.size(); ++i) {
|
|
|
+ if (ctxs[i] != want) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ bool commented = false;
|
|
|
+ std::string k;
|
|
|
+ std::string v;
|
|
|
+ if (!line_directive(lines[i], k, v, commented) || commented ||
|
|
|
+ k != key) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (!seen) {
|
|
|
+ seen = true;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ lines[i] = "#" + lines[i];
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (first_commented >= 0) {
|
|
|
+ lines[static_cast<size_t>(first_commented)] = new_line;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Insert: after opening brace of the target block, or top of main.
|
|
|
+ annotate_contexts(lines, ctxs);
|
|
|
+ if (want == ConfCtx::Main) {
|
|
|
+ // Prefer after `user` directive; else at file start.
|
|
|
+ int insert_at = 0;
|
|
|
+ for (size_t i = 0; i < lines.size(); ++i) {
|
|
|
+ bool commented = false;
|
|
|
+ std::string k;
|
|
|
+ std::string v;
|
|
|
+ if (line_directive(lines[i], k, v, commented) && !commented &&
|
|
|
+ k == "user") {
|
|
|
+ insert_at = static_cast<int>(i) + 1;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ lines.insert(lines.begin() + insert_at, new_line);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (size_t i = 0; i < lines.size(); ++i) {
|
|
|
+ bool commented = false;
|
|
|
+ std::string k;
|
|
|
+ std::string v;
|
|
|
+ if (!line_directive(lines[i], k, v, commented) || commented) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const char* block_name =
|
|
|
+ (want == ConfCtx::Events) ? "events" : "http";
|
|
|
+ if (k != block_name) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // Insert right after this line (which contains `{` or next line).
|
|
|
+ size_t insert_at = i + 1;
|
|
|
+ if (lines[i].find('{') == std::string::npos &&
|
|
|
+ insert_at < lines.size() &&
|
|
|
+ trim_copy(lines[insert_at]) == "{") {
|
|
|
+ insert_at += 1;
|
|
|
+ }
|
|
|
+ lines.insert(lines.begin() + static_cast<long>(insert_at), new_line);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // Fallback: append.
|
|
|
+ lines.push_back(new_line);
|
|
|
+}
|
|
|
+
|
|
|
+void parse_quick_from_lines(const std::vector<std::string>& lines,
|
|
|
+ QuickConfig& out) {
|
|
|
+ out = QuickConfig{};
|
|
|
+ std::vector<ConfCtx> ctxs;
|
|
|
+ annotate_contexts(lines, ctxs);
|
|
|
+ for (size_t i = 0; i < lines.size(); ++i) {
|
|
|
+ bool commented = false;
|
|
|
+ std::string k;
|
|
|
+ std::string v;
|
|
|
+ if (!line_directive(lines[i], k, v, commented) || commented) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (ctxs[i] == ConfCtx::Main && k == "worker_processes") {
|
|
|
+ out.worker_processes = trim_copy(v);
|
|
|
+ } else if (ctxs[i] == ConfCtx::Events && k == "worker_connections") {
|
|
|
+ try {
|
|
|
+ const int n = std::stoi(trim_copy(v));
|
|
|
+ if (n > 0) {
|
|
|
+ out.worker_connections = n;
|
|
|
+ }
|
|
|
+ } catch (...) {
|
|
|
+ }
|
|
|
+ } else if (ctxs[i] == ConfCtx::Http && k == "keepalive_timeout") {
|
|
|
+ out.keepalive_timeout = trim_copy(v);
|
|
|
+ } else if (ctxs[i] == ConfCtx::Http && k == "client_max_body_size") {
|
|
|
+ out.client_max_body_size = trim_copy(v);
|
|
|
+ } else if (ctxs[i] == ConfCtx::Http && k == "gzip") {
|
|
|
+ const std::string gv = trim_copy(v);
|
|
|
+ out.gzip = (gv == "on" || gv == "1" || gv == "yes");
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool valid_worker_processes(const std::string& s) {
|
|
|
+ if (s == "auto") {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (s.empty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ for (char c : s) {
|
|
|
+ if (!std::isdigit(static_cast<unsigned char>(c))) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return std::stoi(s) > 0;
|
|
|
+ } catch (...) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool valid_size_or_time(const std::string& s) {
|
|
|
+ if (s.empty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // Allow values like 65, 65s, 50m, 1g, 1024k
|
|
|
+ size_t i = 0;
|
|
|
+ while (i < s.size() && std::isdigit(static_cast<unsigned char>(s[i]))) {
|
|
|
+ ++i;
|
|
|
+ }
|
|
|
+ if (i == 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (i == s.size()) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (i + 1 == s.size()) {
|
|
|
+ const char u = static_cast<char>(std::tolower(static_cast<unsigned char>(s[i])));
|
|
|
+ return u == 's' || u == 'm' || u == 'h' || u == 'd' || u == 'k' ||
|
|
|
+ u == 'g' || u == 't';
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
bool ensure_build_deps() {
|
|
|
std::cout << "检查编译依赖...\n";
|
|
|
// Prefer apt when available; skip quietly if packages already present.
|
|
|
@@ -511,6 +826,95 @@ bool ensure_vhost_include() {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+bool read_quick_config(QuickConfig& out, std::string& err) {
|
|
|
+ err.clear();
|
|
|
+ if (!is_installed()) {
|
|
|
+ err = "Nginx 未安装";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!path_exists(conf_file())) {
|
|
|
+ out = QuickConfig{};
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ std::vector<std::string> lines;
|
|
|
+ if (!read_conf_lines(lines, err)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ parse_quick_from_lines(lines, out);
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+bool apply_quick_config(const QuickConfig& in, std::string& err) {
|
|
|
+ err.clear();
|
|
|
+ if (!is_installed()) {
|
|
|
+ err = "Nginx 未安装";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ QuickConfig cfg = in;
|
|
|
+ cfg.worker_processes = trim_copy(cfg.worker_processes);
|
|
|
+ cfg.keepalive_timeout = trim_copy(cfg.keepalive_timeout);
|
|
|
+ cfg.client_max_body_size = trim_copy(cfg.client_max_body_size);
|
|
|
+ if (!valid_worker_processes(cfg.worker_processes)) {
|
|
|
+ err = "worker_processes 无效(应为 auto 或正整数)";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (cfg.worker_connections < 1 || cfg.worker_connections > 1048576) {
|
|
|
+ err = "worker_connections 无效";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!valid_size_or_time(cfg.keepalive_timeout)) {
|
|
|
+ err = "keepalive_timeout 无效";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!valid_size_or_time(cfg.client_max_body_size)) {
|
|
|
+ err = "client_max_body_size 无效";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::vector<std::string> lines;
|
|
|
+ if (!read_conf_lines(lines, err)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ upsert_in_context(lines, ConfCtx::Main, "worker_processes",
|
|
|
+ cfg.worker_processes, "");
|
|
|
+ upsert_in_context(lines, ConfCtx::Events, "worker_connections",
|
|
|
+ std::to_string(cfg.worker_connections), " ");
|
|
|
+ upsert_in_context(lines, ConfCtx::Http, "keepalive_timeout",
|
|
|
+ cfg.keepalive_timeout, " ");
|
|
|
+ upsert_in_context(lines, ConfCtx::Http, "client_max_body_size",
|
|
|
+ cfg.client_max_body_size, " ");
|
|
|
+ upsert_in_context(lines, ConfCtx::Http, "gzip",
|
|
|
+ cfg.gzip ? "on" : "off", " ");
|
|
|
+
|
|
|
+ if (!write_conf_lines(lines, err)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Keep NGS markers / user intact after manual edits.
|
|
|
+ ensure_nginx_user();
|
|
|
+ ensure_vhost_include();
|
|
|
+
|
|
|
+ std::string detail;
|
|
|
+ if (!test_config(detail)) {
|
|
|
+ err = "配置已写入,但检测失败: " + detail;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (is_running()) {
|
|
|
+ if (!reload()) {
|
|
|
+ err = "配置已写入,但重载 Nginx 失败";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log_info("nginx quick config applied worker_processes=" +
|
|
|
+ cfg.worker_processes +
|
|
|
+ " worker_connections=" + std::to_string(cfg.worker_connections) +
|
|
|
+ " keepalive_timeout=" + cfg.keepalive_timeout +
|
|
|
+ " client_max_body_size=" + cfg.client_max_body_size +
|
|
|
+ " gzip=" + (cfg.gzip ? "on" : "off"));
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
std::string installed_version() {
|
|
|
const std::string path = join_path(install_dir(), ".ngs_version");
|
|
|
if (path_exists(path)) {
|