一、简介
基于Qt框架实现了一套客户端、服务器架构的自动化构建与文件传输系统,核心功能是服务端触发构建任务、客户端执行构建并上传产物,同时支持产物下载、状态监控等能力。


测试服务器的基本指令
1、测试健康检查
curl.exe "http://127.0.0.1:8080/health?token=build_secret_token_2026"
2、查询状态
curl.exe "http://127.0.0.1:8080/stats?token=build_secret_token_2026"
3、测试轮询
curl http://127.0.0.1:8080/poll?agent=win_tools\&token=build_secret_token_2026
4、触发构建
curl.exe -X POST http://127.0.0.1:8080/trigger -H "Content-Type: application/json" -d "{\"type\":\"win_tools\",\"branch\":\"main\",\"token\":\"build_secret_token_2026\"}"
curl.exe -X POST http://127.0.0.1:8080/trigger -H "Content-Type: application/json" -d "{\"type\":\"win_services\",\"branch\":\"main\",\"token\":\"build_secret_token_2026\"}"
curl.exe -X POST http://127.0.0.1:8080/trigger -H "Content-Type: application/json" -d "{\"type\":\"arm_sim\",\"branch\":\"main\",\"token\":\"build_secret_token_2026\"}"
curl.exe -X POST http://127.0.0.1:8080/trigger -H "Content-Type: application/json" -d "{\"type\":\"linux_x86\",\"branch\":\"main\",\"token\":\"build_secret_token_2026\"}"
5、发布状态
curl.exe "http://127.0.0.1:8080/download?agent=win_tools\&token=build_secret_token_2026" --output win_tool
6、下载
curl.exe "http://127.0.0.1:8080/download?agent=win_tools\&token=build_secret_token_2026" --output win_tool.zip
二、详解
1、服务端
(1)build_server.pro
# build_server.pro
QT += core network
QT -= gui
TARGET = BuildServer
TEMPLATE = app
CONFIG += c++11 console automoc
CONFIG -= qt_quickcompiler
VERSION = 1.0.0
DEFINES += VERSION=\"\\\"$${VERSION}\\\"\"
unix {
DEFINES += Q_OS_LINUX
QMAKE_CXXFLAGS += -Wall -Wextra
LIBS += -lpthread
} else:win32 {
DEFINES += Q_OS_WIN
LIBS += -lws2_32
}
INCLUDEPATH += $$PWD/include
SOURCES += \
src/main.cpp \
src/http_server.cpp
HEADERS += \
include/http_server.h
DESTDIR = $$PWD/bin
QMAKE_CLEAN += bin/* obj/*
(2)http_server.h
#ifndef HTTP_SERVER_H
#define HTTP_SERVER_H
#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
#include <QMap>
#include <QMutex>
#include <QTimer>
#include <QFile>
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QDateTime>
#include <QStandardPaths>
#include <QByteArray>
#include <QString>
#include <QDebug>
#include <QUrlQuery>
#include <QCryptographicHash>
// 上传上下文结构
struct UploadContext {
QFile *file;
QString type;
QString filename;
qint64 size;
qint64 received;
qint64 sinceLastFlush; // 距上次 flush 已累积字节数
UploadContext() : file(nullptr), size(0), received(0), sinceLastFlush(0) {}
~UploadContext() {
if (file) {
if (file->isOpen()) file->close();
delete file;
}
}
};
struct BuildTask {
QString type;
QString branch;
bool requested = false;
bool in_progress = false;
bool completed = false;
QString status = "idle";
QString lastBuildTime;
int requestCount = 0;
BuildTask() : type(""), branch("main"), requested(false),
in_progress(false), completed(false), status("idle"),
lastBuildTime(""), requestCount(0) {}
BuildTask(const QString &t, const QString &b, bool req, bool prog,
bool comp, const QString &st, const QString &time, int cnt)
: type(t), branch(b), requested(req), in_progress(prog),
completed(comp), status(st), lastBuildTime(time), requestCount(cnt) {}
};
struct ReleaseFile {
QString type;
QString filename;
QString filepath;
QString md5;
qint64 size;
QString version;
QString updateTime;
ReleaseFile() : size(0), version("1.0.0"), updateTime("") {}
ReleaseFile(const QString &t, const QString &fn, const QString &fp,
const QString &m, qint64 s, const QString &v, const QString &ut)
: type(t), filename(fn), filepath(fp), md5(m), size(s), version(v), updateTime(ut) {}
};
class HttpServer : public QObject {
Q_OBJECT
public:
explicit HttpServer(quint16 port = 8080, QObject *parent = nullptr);
~HttpServer();
void start();
void stop();
void triggerBuild(const QString &type, const QString &branch = "main");
void triggerDownload(const QString &type, const QString &version);
QString getStatus(const QString &type);
QByteArray getAllStatus();
QByteArray getReleaseInfo(const QString &type);
private slots:
void handleNewConnection();
void readClient();
void discardClient();
void handleSocketError(QAbstractSocket::SocketError socketError);
private:
void finishUpload(QTcpSocket *socket, UploadContext *context);
void cleanupUploadContext(QTcpSocket *socket);
void initializeTasks();
void initializeReleases();
QString getReleaseDir(const QString &type);
void saveBuildLog(const QString &type, const QString &message);
bool authenticate(const QString &token);
QByteArray parseHttpRequest(const QByteArray &data);
QByteArray handlePoll(const QUrlQuery &query);
QByteArray handleUpload(const QUrlQuery &query, const QByteArray &data, QTcpSocket *socket);
void handleTrigger(const QByteArray &body);
QByteArray handleHealth();
QByteArray handleStats();
QByteArray handleDownload(const QUrlQuery &query);
QByteArray handleReleaseInfo(const QUrlQuery &query);
QString calculateMD5(const QString &filePath);
void saveReleaseInfo(const QString &type);
QByteArray getReleaseJson();
quint16 port_;
QTcpServer tcpServer_;
QMap<QString, BuildTask> tasks_;
QMap<QString, ReleaseFile> releases_;
QMutex mutex_;
QMutex releaseMutex_;
QString releaseDir_;
QString apiToken_;
bool running_ = false;
QMap<QTcpSocket*, UploadContext*> uploadFiles_;
QMap<QTcpSocket*, QString> uploadTypes_;
QMap<QTcpSocket*, QString> uploadFilenames_;
QMap<QTcpSocket*, QByteArray> uploadMd5_;
QMap<QTcpSocket*, qint64> uploadSize_;
QMap<QTcpSocket*, qint64> uploadReceived_;
static constexpr const char *DEFAULT_TOKEN = "build_secret_token_2026";
};
#endif // HTTP_SERVER_H
(3)http_server.cpp
#include "http_server.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QFile>
#include <QDir>
#include <QDateTime>
#include <QUrlQuery>
#include <QHostAddress>
#include <QDebug>
#include <QCryptographicHash>
#include <QCoreApplication>
#ifdef Q_OS_LINUX
#include <sys/socket.h>
#include <netinet/tcp.h>
#endif
#ifdef Q_OS_WIN
#include <winsock2.h>
#include <windows.h>
#endif
// 上传进度刷新间隔(每接收 1MB flush 一次)
static const qint64 FLUSH_INTERVAL_BYTES = 1024 * 1024;
// 下载块大小(256KB)
static const qint64 DOWNLOAD_CHUNK_SIZE = 256 * 1024;
HttpServer::HttpServer(quint16 port, QObject *parent)
: QObject(parent)
, port_(port)
, releaseDir_(QCoreApplication::applicationDirPath() + "/build_release")
, apiToken_(DEFAULT_TOKEN)
, running_(false)
{
initializeTasks();
initializeReleases();
QDir dir(releaseDir_);
if (!dir.exists()) dir.mkpath(".");
connect(&tcpServer_, &QTcpServer::newConnection, this, &HttpServer::handleNewConnection);
}
HttpServer::~HttpServer() {
stop();
for (auto *ctx : uploadFiles_) {
if (ctx) {
if (ctx->file) {
ctx->file->close();
delete ctx->file;
}
delete ctx;
}
}
uploadFiles_.clear();
}
void HttpServer::initializeTasks() {
tasks_["win_tools"] = BuildTask("win_tools", "main", false, false, false, "idle", "", 0);
tasks_["win_services"] = BuildTask("win_services", "main", false, false, false, "idle", "", 0);
tasks_["arm_sim"] = BuildTask("arm_sim", "main", false, false, false, "idle", "", 0);
tasks_["linux_x86"] = BuildTask("linux_x86", "main", false, false, false, "idle", "", 0);
}
void HttpServer::initializeReleases() {
releases_["win_tools"] = ReleaseFile("win_tools", "", "", "", 0, "", "");
releases_["win_services"] = ReleaseFile("win_services", "", "", "", 0, "", "");
releases_["arm_sim"] = ReleaseFile("arm_sim", "", "", "", 0, "", "");
releases_["linux_x86"] = ReleaseFile("linux_x86", "", "", "", 0, "", "");
}
void HttpServer::start() {
if (running_) return;
if (tcpServer_.listen(QHostAddress::Any, port_)) {
running_ = true;
qDebug() << "[Server] Started on port" << port_;
} else {
qCritical() << "[Server] Failed to start:" << tcpServer_.errorString();
}
}
void HttpServer::stop() {
if (!running_) return;
tcpServer_.close();
running_ = false;
qDebug() << "[Server] Stopped";
}
void HttpServer::triggerBuild(const QString &type, const QString &branch) {
QMutexLocker locker(&mutex_);
if (!tasks_.contains(type)) return;
BuildTask &task = tasks_[type];
task.requested = true;
task.branch = branch;
task.status = "pending";
task.requestCount++;
task.completed = false;
task.in_progress = false;
qDebug() << "[Server] Build triggered:" << type << branch;
saveBuildLog(type, "Build triggered for branch: " + branch);
}
void HttpServer::triggerDownload(const QString &type, const QString &version) {
QMutexLocker locker(&releaseMutex_);
if (!releases_.contains(type)) return;
ReleaseFile &release = releases_[type];
release.version = version;
release.updateTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
}
QString HttpServer::getStatus(const QString &type) {
QMutexLocker locker(&mutex_);
if (!tasks_.contains(type)) return QJsonDocument(QJsonObject{{"status","unknown"}}).toJson();
const BuildTask &t = tasks_[type];
QJsonObject obj;
obj["type"]=t.type; obj["status"]=t.status; obj["requested"]=t.requested;
obj["in_progress"]=t.in_progress; obj["completed"]=t.completed;
obj["branch"]=t.branch; obj["lastBuildTime"]=t.lastBuildTime; obj["requestCount"]=t.requestCount;
return QJsonDocument(obj).toJson();
}
QByteArray HttpServer::getAllStatus() {
QMutexLocker locker(&mutex_);
QJsonObject root;
for (auto &t : tasks_) {
QJsonObject obj;
obj["status"]=t.status; obj["requested"]=t.requested; obj["in_progress"]=t.in_progress;
obj["completed"]=t.completed; obj["branch"]=t.branch; obj["lastBuildTime"]=t.lastBuildTime;
obj["requestCount"]=t.requestCount;
root[t.type]=obj;
}
return QJsonDocument(root).toJson();
}
QByteArray HttpServer::getReleaseInfo(const QString &type) {
QMutexLocker locker(&releaseMutex_);
if (!releases_.contains(type)) return QJsonDocument(QJsonObject{{"status","unknown"}}).toJson();
const ReleaseFile &r = releases_[type];
QJsonObject obj;
obj["type"]=r.type; obj["filename"]=r.filename; obj["md5"]=r.md5; obj["size"]=r.size;
obj["version"]=r.version; obj["updateTime"]=r.updateTime;
return QJsonDocument(obj).toJson();
}
bool HttpServer::authenticate(const QString &token) { return token == apiToken_; }
QString HttpServer::getReleaseDir(const QString &type) { return releaseDir_ + "/" + type; }
void HttpServer::saveBuildLog(const QString &type, const QString &message) {
QString logPath = getReleaseDir(type) + "/build.log";
QFile f(logPath);
if (f.open(QIODevice::Append|QIODevice::Text)) {
f.write(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss").toUtf8() + " - " + message.toUtf8() + "\n");
f.close();
}
}
QString HttpServer::calculateMD5(const QString &filePath) {
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) return QString();
QCryptographicHash hash(QCryptographicHash::Md5);
while (!f.atEnd()) hash.addData(f.read(8192));
f.close();
return hash.result().toHex();
}
void HttpServer::saveReleaseInfo(const QString &type) {
if (!releases_.contains(type)) return;
ReleaseFile &r = releases_[type];
QString path = getReleaseDir(type) + "/release_info.json";
QJsonObject obj;
obj["type"]=r.type; obj["filename"]=r.filename; obj["md5"]=r.md5; obj["size"]=r.size;
obj["version"]=r.version; obj["updateTime"]=r.updateTime;
QFile f(path);
if (f.open(QIODevice::WriteOnly)) f.write(QJsonDocument(obj).toJson());
}
QByteArray HttpServer::getReleaseJson() {
QMutexLocker locker(&releaseMutex_);
QJsonObject root;
for (auto &r : releases_) {
QJsonObject obj;
obj["filename"]=r.filename; obj["md5"]=r.md5; obj["size"]=r.size;
obj["version"]=r.version; obj["updateTime"]=r.updateTime;
root[r.type]=obj;
}
return QJsonDocument(root).toJson();
}
void HttpServer::handleNewConnection() {
while (tcpServer_.hasPendingConnections()) {
QTcpSocket *socket = tcpServer_.nextPendingConnection();
// 基础跨平台选项
socket->setSocketOption(QAbstractSocket::ReceiveBufferSizeSocketOption, 10*1024*1024);
socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); // 禁用 Nagle
#ifdef Q_OS_LINUX
int fd = socket->socketDescriptor();
// 更精细的 TCP keep-alive 参数
int keepIdle = 10, keepIntvl = 5, keepCnt = 3;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(keepIdle));
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &keepIntvl, sizeof(keepIntvl));
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &keepCnt, sizeof(keepCnt));
#endif
connect(socket, &QTcpSocket::readyRead, this, &HttpServer::readClient, Qt::QueuedConnection);
connect(socket, &QTcpSocket::disconnected, this, &HttpServer::discardClient, Qt::QueuedConnection);
connect(socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error),
this, &HttpServer::handleSocketError, Qt::QueuedConnection);
}
}
void HttpServer::handleSocketError(QAbstractSocket::SocketError) {
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
if (!socket) return;
qWarning() << "[Server] Socket error:" << socket->errorString();
socket->deleteLater();
}
void HttpServer::readClient() {
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
if (!socket) return;
if (socket->state() != QAbstractSocket::ConnectedState) {
discardClient();
return;
}
// 处理正在进行的文件上传
if (uploadFiles_.contains(socket)) {
UploadContext *ctx = uploadFiles_[socket];
if (!ctx || !ctx->file || !ctx->file->isOpen()) {
discardClient();
return;
}
// 一次最多读取 1MB
QByteArray data = socket->read(1024 * 1024);
if (data.isEmpty()) {
if (ctx->received >= ctx->size) {
finishUpload(socket, ctx);
} else {
qWarning() << "[Server] Upload incomplete, socket closed. Received:"
<< ctx->received << "/" << ctx->size;
cleanupUploadContext(socket);
socket->deleteLater();
}
return;
}
qint64 written = ctx->file->write(data);
if (written != data.size()) {
qCritical() << "[Server] File write error: wrote" << written << "of" << data.size()
<< "error:" << ctx->file->errorString();
cleanupUploadContext(socket);
socket->close();
return;
}
ctx->received += written;
ctx->sinceLastFlush += written;
// 每累积 1MB 或最后一块时 flush 一次,减少磁盘 I/O
if (ctx->sinceLastFlush >= FLUSH_INTERVAL_BYTES || ctx->received >= ctx->size) {
ctx->file->flush();
ctx->sinceLastFlush = 0;
}
qDebug() << "[Server] Upload progress:" << ctx->received << "/" << ctx->size;
if (ctx->received >= ctx->size) {
finishUpload(socket, ctx);
}
return;
}
// 处理新HTTP请求
QByteArray data = socket->readAll();
if (data.isEmpty()) {
socket->close();
return;
}
QByteArray response = parseHttpRequest(data);
if (response.isEmpty()) {
response = "HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"error\"}";
}
socket->write(response);
socket->waitForBytesWritten(5000);
socket->flush();
bool is100 = response.startsWith("HTTP/1.1 100");
if (is100) {
if (socket->bytesAvailable() > 0)
QMetaObject::invokeMethod(this, [this,socket](){ readClient(); }, Qt::QueuedConnection);
return;
}
if (socket->state() == QAbstractSocket::ConnectedState) socket->close();
}
void HttpServer::finishUpload(QTcpSocket *socket, UploadContext *ctx) {
if (!ctx || !ctx->file) return;
ctx->file->close();
QString filePath = ctx->file->fileName();
QString md5 = calculateMD5(filePath);
qDebug() << "[Server] Upload finished, MD5:" << md5;
{
QMutexLocker locker(&releaseMutex_);
if (releases_.contains(ctx->type)) {
ReleaseFile &r = releases_[ctx->type];
r.filename = ctx->filename;
r.filepath = filePath;
r.md5 = md5;
r.size = ctx->size;
r.version = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
r.updateTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
saveReleaseInfo(ctx->type);
}
}
delete ctx->file;
ctx->file = nullptr;
// 重置构建任务状态
{
QMutexLocker locker(&mutex_);
if (tasks_.contains(ctx->type)) {
BuildTask &task = tasks_[ctx->type];
task.requested = false;
task.in_progress = false;
task.completed = true;
task.status = "idle";
task.lastBuildTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
qDebug() << "[Server] Build task for" << ctx->type << "reset to idle";
}
}
QByteArray resp = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n"
"{\"status\":\"success\",\"md5\":\"" + md5.toUtf8() + "\",\"size\":" + QByteArray::number(ctx->size) + "}";
if (socket->state() == QAbstractSocket::ConnectedState) {
socket->write(resp);
socket->flush();
socket->waitForBytesWritten(5000);
socket->close();
}
cleanupUploadContext(socket);
socket->deleteLater();
}
void HttpServer::cleanupUploadContext(QTcpSocket *socket) {
if (!uploadFiles_.contains(socket)) return;
UploadContext *ctx = uploadFiles_.take(socket);
if (ctx) {
if (ctx->file) {
if (ctx->file->isOpen()) {
ctx->file->close();
QFile::remove(ctx->file->fileName());
}
delete ctx->file;
}
delete ctx;
}
uploadTypes_.remove(socket); uploadFilenames_.remove(socket); uploadMd5_.remove(socket);
uploadSize_.remove(socket); uploadReceived_.remove(socket);
}
void HttpServer::discardClient() {
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
if (!socket) return;
qDebug() << "[Server] discardClient for socket" << socket->socketDescriptor();
cleanupUploadContext(socket);
socket->deleteLater();
}
QByteArray HttpServer::parseHttpRequest(const QByteArray &data) {
QStringList lines = QString::fromUtf8(data).split("\r\n");
if (lines.isEmpty()) return {};
QStringList reqLine = lines[0].split(" ");
if (reqLine.size() < 2) return {};
QString method = reqLine[0], path = reqLine[1];
QUrl url(path);
QUrlQuery query(url);
QString token = query.queryItemValue("token");
int contentLen = 0;
for (int i=1; i<lines.size(); ++i) {
if (lines[i].startsWith("Content-Length:"))
contentLen = lines[i].mid(16).trimmed().toInt();
}
QByteArray body;
int bodyStart = data.indexOf("\r\n\r\n");
if (bodyStart != -1) body = data.mid(bodyStart+4, contentLen);
if (method=="POST" && !body.isEmpty()) {
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(body, &err);
if (err.error==QJsonParseError::NoError && doc.isObject())
token = doc.object()["token"].toString();
}
if (!authenticate(token))
return "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"unauthorized\"}";
if (path.contains("/poll")) return handlePoll(query);
if (path.contains("/upload")) return handleUpload(query, data, qobject_cast<QTcpSocket*>(sender()));
if (path.contains("/download")) return handleDownload(query);
if (path.contains("/release")) return handleReleaseInfo(query);
if (path.contains("/trigger")) { handleTrigger(body); return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"triggered\"}"; }
if (path.contains("/health")) return handleHealth();
if (path.contains("/stats")) return handleStats();
return "HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"not_found\"}";
}
QByteArray HttpServer::handlePoll(const QUrlQuery &query) {
QString agent = query.queryItemValue("agent"), token = query.queryItemValue("token");
QMutexLocker locker(&mutex_);
if (!tasks_.contains(agent)) return "HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"unknown_agent\"}";
if (!authenticate(token)) return "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"unauthorized\"}";
BuildTask &t = tasks_[agent];
if (t.requested && !t.in_progress) {
t.in_progress = true; t.status = "building";
QJsonObject resp; resp["status"]="build"; resp["branch"]=t.branch; resp["type"]=t.type; resp["message"]="Start building now";
return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + QJsonDocument(resp).toJson();
} else {
QJsonObject resp; resp["status"]="idle"; resp["message"]="No build requested";
return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + QJsonDocument(resp).toJson();
}
}
QByteArray HttpServer::handleUpload(const QUrlQuery &query, const QByteArray &data, QTcpSocket *socket) {
QString agent = query.queryItemValue("agent"), token = query.queryItemValue("token");
QMutexLocker locker(&mutex_);
if (!tasks_.contains(agent)) return "HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"unknown_agent\"}";
if (!authenticate(token)) return "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"unauthorized\"}";
QStringList lines = QString::fromUtf8(data).split("\r\n");
qint64 fileSize = 0;
for (int i=1; i<lines.size(); ++i)
if (lines[i].startsWith("Content-Length:")) { fileSize = lines[i].mid(16).trimmed().toLongLong(); break; }
QString filename = "artifact.zip";
for (int i=1; i<lines.size(); ++i)
if (lines[i].startsWith("X-Filename:")) { filename = lines[i].mid(12).trimmed().replace(QRegExp("[\\\\/:*?\"<>|]"), "_"); break; }
QString uploadDir = getReleaseDir(agent);
QDir().mkpath(uploadDir);
QString filePath = uploadDir + "/" + filename;
if (QFile::exists(filePath)) QFile::remove(filePath);
QFile *file = new QFile(filePath);
if (!file->open(QIODevice::WriteOnly)) { delete file; return "HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"file_open_error\"}"; }
int bodyStart = data.indexOf("\r\n\r\n");
QByteArray fileData = (bodyStart != -1) ? data.mid(bodyStart+4) : QByteArray();
if (fileData.size() >= fileSize || fileSize == 0) {
// 一次性完整上传
if (file->write(fileData) != fileData.size()) {
file->close(); delete file; QFile::remove(filePath);
return "HTTP/1.1 500\r\n\r\n";
}
file->close();
QString md5 = calculateMD5(filePath);
{
QMutexLocker l(&releaseMutex_);
if (releases_.contains(agent)) {
ReleaseFile &r = releases_[agent];
r.filename = filename; r.filepath = filePath; r.md5 = md5; r.size = fileData.size();
r.version = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
r.updateTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
saveReleaseInfo(agent);
}
}
delete file;
// 重置构建任务状态
if (tasks_.contains(agent)) {
BuildTask &task = tasks_[agent];
task.requested = false;
task.in_progress = false;
task.completed = true;
task.status = "idle";
task.lastBuildTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
qDebug() << "[Server] Build task for" << agent << "reset to idle (one-shot upload)";
}
return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"success\",\"md5\":\"" + md5.toUtf8() + "\",\"size\":" + QByteArray::number(fileData.size()) + "}";
}
// 分块上传:写入已接收部分,返回 100 Continue
file->write(fileData);
file->flush();
UploadContext *ctx = new UploadContext;
ctx->file = file;
ctx->type = agent;
ctx->filename = filename;
ctx->size = fileSize;
ctx->received = fileData.size();
ctx->sinceLastFlush = fileData.size(); // 初始数据已 flush
uploadFiles_[socket] = ctx;
return "HTTP/1.1 100 Continue\r\nConnection: keep-alive\r\n\r\n";
}
void HttpServer::handleTrigger(const QByteArray &body) {
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(body, &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) return;
QJsonObject obj = doc.object();
QString type = obj["type"].toString();
QString branch = obj["branch"].toString("main");
QString token = obj["token"].toString();
if (!authenticate(token)) return;
if (type.isEmpty()) return;
triggerBuild(type, branch);
}
QByteArray HttpServer::handleDownload(const QUrlQuery &query) {
QString agent = query.queryItemValue("agent"), token = query.queryItemValue("token");
QMutexLocker locker(&releaseMutex_);
if (!releases_.contains(agent)) return "HTTP/1.1 404\r\n\r\n";
if (!authenticate(token)) return "HTTP/1.1 401\r\n\r\n";
const ReleaseFile &r = releases_[agent];
if (r.filename.isEmpty()) return "HTTP/1.1 404\r\n\r\n";
QFile file(r.filepath);
if (!file.open(QIODevice::ReadOnly)) return "HTTP/1.1 500\r\n\r\n";
QByteArray resp = "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Disposition: attachment; filename=\"" + r.filename.toUtf8() + "\"\r\nContent-Length: " + QByteArray::number(r.size) + "\r\nX-MD5: " + r.md5.toUtf8() + "\r\nConnection: close\r\n\r\n";
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
if (socket) {
socket->write(resp);
socket->waitForBytesWritten(1000);
// 使用 256KB 块发送,减少系统调用
while (!file.atEnd()) {
QByteArray chunk = file.read(DOWNLOAD_CHUNK_SIZE);
if (chunk.isEmpty()) break;
qint64 sent = socket->write(chunk);
if (sent != chunk.size()) {
qWarning() << "[Server] Failed to send all data during download";
break;
}
socket->waitForBytesWritten(1000);
}
file.close();
socket->close();
}
return {};
}
QByteArray HttpServer::handleReleaseInfo(const QUrlQuery &query) {
QString agent = query.queryItemValue("agent"), token = query.queryItemValue("token");
if (!authenticate(token)) return "HTTP/1.1 401\r\n\r\n";
return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + getReleaseInfo(agent);
}
QByteArray HttpServer::handleHealth() {
QJsonObject obj; obj["status"]="ok"; obj["timestamp"]=QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + QJsonDocument(obj).toJson();
}
QByteArray HttpServer::handleStats() {
return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + getAllStatus();
}
(4)main.cpp
#include <QCoreApplication>
#include <QCommandLineParser>
#include <QCommandLineOption>
#include <QDir>
#include <QDebug>
#include "http_server.h"
#ifdef Q_OS_UNIX
#include <signal.h>
#include <sys/resource.h>
#endif
#ifdef Q_OS_WIN
#include <windows.h>
#endif
static QCoreApplication *gApp = nullptr;
#ifdef Q_OS_UNIX
void signalHandler(int sig) {
if (gApp) {
qDebug() << "Received signal" << sig << ", shutting down...";
gApp->quit();
}
}
void setMaxFileDescriptors() {
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE, &rl) == 0) {
rlim_t newMax = rl.rlim_max;
if (rl.rlim_cur < newMax) {
rl.rlim_cur = newMax;
if (setrlimit(RLIMIT_NOFILE, &rl) == 0) {
qDebug() << "[Main] File descriptor limit increased to" << newMax;
} else {
qWarning() << "[Main] Failed to increase file descriptor limit";
}
} else {
qDebug() << "[Main] File descriptor limit already at" << rl.rlim_cur;
}
} else {
qWarning() << "[Main] Failed to get file descriptor limit";
}
}
#endif
#ifdef Q_OS_WIN
BOOL WINAPI consoleHandler(DWORD dwType) {
if (dwType == CTRL_C_EVENT || dwType == CTRL_CLOSE_EVENT) {
if (gApp) {
qDebug() << "Received shutdown event, quitting...";
QMetaObject::invokeMethod(gApp, &QCoreApplication::quit, Qt::QueuedConnection);
}
return TRUE;
}
return FALSE;
}
#endif
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
gApp = &app;
app.setApplicationName("BuildServer");
app.setApplicationVersion("1.0");
#ifdef Q_OS_UNIX
// 提高文件描述符限制(仅 Unix)
setMaxFileDescriptors();
// 注册信号处理(仅 Unix)
signal(SIGINT, signalHandler);
signal(SIGTERM, signalHandler);
signal(SIGHUP, signalHandler);
#endif
#ifdef Q_OS_WIN
// Windows 控制台事件处理
SetConsoleCtrlHandler(consoleHandler, TRUE);
#endif
QCommandLineParser parser;
parser.setApplicationDescription("Build Server - Central build management");
parser.addHelpOption();
parser.addVersionOption();
QCommandLineOption portOption(QStringList() << "p" << "port",
"Server port", "port", "8080");
parser.addOption(portOption);
parser.process(app);
quint16 port = parser.value(portOption).toUShort();
HttpServer server(port);
server.start();
QObject::connect(&app, &QCoreApplication::aboutToQuit, [&server]() {
server.stop();
});
qDebug() << "========================================";
qDebug() << " Build Server (Cross-Platform)";
qDebug() << "========================================";
qDebug() << "Port:" << port;
qDebug() << "Press Ctrl+C to stop";
qDebug() << "========================================";
return app.exec();
}
2、客户端
(1)build_client.pro
# build_client.pro
QT += core network
QT -= gui
TARGET = BuildClient
TEMPLATE = app
CONFIG += c++17 console automoc
CONFIG -= qt_quickcompiler
VERSION = 1.0.0
DEFINES += VERSION=\\\"$${VERSION}\\\"
# 编译优化
CONFIG += release
CONFIG += warn_on
unix {
DEFINES += Q_OS_LINUX
QMAKE_CXXFLAGS += -Wall -Wextra -Wpedantic -O2
QMAKE_LFLAGS += -s # Strip symbols
LIBS += -lpthread
}
win32 {
DEFINES += Q_OS_WIN
QMAKE_CXXFLAGS += /MP /O2 /W3
QMAKE_LFLAGS += /LTCG # Link Time Code Generation
}
macx {
DEFINES += Q_OS_MAC
QMAKE_CXXFLAGS += -Wall -Wextra -O2
}
INCLUDEPATH += $$PWD/include
SOURCES += \
src/main.cpp \
src/build_client.cpp
HEADERS += \
include/build_client.h
DESTDIR = $$PWD/bin
OBJECTS_DIR = $$PWD/obj
MOC_DIR = $$PWD/moc
QMAKE_CLEAN += bin/* obj/* moc/*
# 安装配置
unix:!macx {
target.path = /usr/local/bin
INSTALLS += target
}
# Windows 资源文件
win32 {
RC_ICONS = $$PWD/resources/icon.ico
}
# 打印编译信息
message("Building BuildClient v$${VERSION}")
message("Target: $$TARGET")
message("Output directory: $$DESTDIR")
(2)build_client.h
#ifndef BUILD_CLIENT_H
#define BUILD_CLIENT_H
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrl>
#include <QUrlQuery>
#include <QTimer>
#include <QProcess>
#include <QFile>
#include <QDir>
#include <QStandardPaths>
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QSettings>
#include <QThread>
#include <QMutex>
#include <QReadWriteLock>
#include <QElapsedTimer>
#include <QDebug>
#include <QCryptographicHash>
#include <memory>
#include <functional>
// 客户端状态枚举
enum class ClientState {
Idle,
Polling,
Building,
Uploading,
Downloading,
Stopped
};
// 构建结果枚举
enum class BuildResult {
Success,
Failed,
Cancelled
};
class BuildClient : public QObject {
Q_OBJECT
public:
explicit BuildClient(const QString &serverUrl, const QString &agentType,
const QString &token, QObject *parent = nullptr);
~BuildClient();
// 生命周期管理
void start();
void stop();
bool isRunning() const;
ClientState state() const;
// 配置方法
void setBuildScript(const QString &scriptPath);
void setOutputDir(const QString &outputDir);
void setPollInterval(int milliseconds);
void setRequestTimeout(int milliseconds);
void setMaxRetries(int retries);
// 统计信息
int getSuccessCount() const;
int getFailCount() const;
QString getLastError() const;
// 信号用于监控
signals:
void stateChanged(ClientState newState, ClientState oldState);
void buildStarted(const QString &branch);
void buildCompleted(bool success, int exitCode);
void uploadProgress(qint64 bytesSent, qint64 totalBytes);
void downloadProgress(qint64 bytesReceived, qint64 totalBytes);
void statusUpdated(const QString &status, const QString &message);
void errorOccurred(const QString &error, int errorCode);
private slots:
void pollServer();
void onPollReply(QNetworkReply *reply);
void onUploadReply(QNetworkReply *reply);
void onDownloadReply(QNetworkReply *reply);
void onReleaseInfoReply(QNetworkReply *reply);
void onBuildProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
void onBuildProcessError(QProcess::ProcessError error);
void onNetworkError(QNetworkReply::NetworkError code);
void retryPoll();
private:
// 核心功能
void executeBuildScript(const QString &branch);
void uploadArtifact(const QString &filePath);
void downloadArtifact(const QString &version = QString());
void checkForUpdate();
// 辅助方法
QString createZipFile(const QString &sourceDir);
QString createTemporaryDirectory() const;
void cleanupTemporaryFiles();
bool verifyMD5(const QString &filePath, const QString &expectedMd5) const;
void saveBuildLog(const QString &message) const;
void updateState(ClientState newState);
void updateStatus(const QString &status, const QString &message = QString());
// 网络请求辅助方法
QNetworkReply* sendGetRequest(const QString &endpoint, const QUrlQuery &extraQuery = QUrlQuery());
QNetworkReply* sendPostRequest(const QString &endpoint, const QByteArray &data,
const QMap<QByteArray, QByteArray> &headers = QMap<QByteArray, QByteArray>());
QNetworkReply* sendFileUpload(const QString &endpoint, const QString &filePath,
const QMap<QByteArray, QByteArray> &extraHeaders = QMap<QByteArray, QByteArray>());
// 错误处理
void handleNetworkError(QNetworkReply *reply, const QString &context);
void scheduleRetry(const std::function<void()> &operation, const QString &operationName);
// 成员变量
QString serverUrl_;
QString agentType_;
QString token_;
QString buildScript_;
QString outputDir_;
QString tempDir_;
// 网络组件
std::unique_ptr<QNetworkAccessManager> networkManager_;
std::unique_ptr<QTimer> pollTimer_;
std::unique_ptr<QTimer> retryTimer_;
std::unique_ptr<QProcess> buildProcess_;
// 状态管理
mutable QReadWriteLock stateLock_;
ClientState currentState_;
bool building_;
int retryCount_;
int maxRetries_;
// 统计信息
int successCount_;
int failCount_;
QString lastError_;
// 配置参数
int pollIntervalMs_;
int requestTimeoutMs_;
// 当前构建信息
QString currentBranch_;
QString currentArtifactPath_;
QString currentVersion_;
QString expectedMd5_;
// 时间统计
QElapsedTimer buildTimer_;
// 常量定义
static constexpr int DEFAULT_POLL_INTERVAL_MS = 3000;
static constexpr int DEFAULT_REQUEST_TIMEOUT_MS = 30000;
static constexpr int DEFAULT_MAX_RETRIES = 3;
static constexpr int RETRY_DELAY_MS = 5000;
static constexpr int CHUNK_SIZE = 8192;
static constexpr int PROCESS_TIMEOUT_MS = 300000; // 5 minutes
static constexpr int WAIT_FOR_FINISHED_MS = 5000;
};
#endif
(3)build_client.cpp
#include "build_client.h"
#include <QCoreApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QFile>
#include <QDir>
#include <QStandardPaths>
#include <QDateTime>
#include <QProcess>
#include <QSettings>
#include <QUrlQuery>
#include <QCryptographicHash>
#include <QTemporaryDir>
#include <QThread>
#include <QTimer>
#include <QDebug>
// 辅助宏
#define LOCK_READ(mutex) QReadLocker locker(&mutex)
#define LOCK_WRITE(mutex) QWriteLocker locker(&mutex)
BuildClient::BuildClient(const QString &serverUrl, const QString &agentType,
const QString &token, QObject *parent)
: QObject(parent)
, serverUrl_(serverUrl)
, agentType_(agentType)
, token_(token)
, networkManager_(std::make_unique<QNetworkAccessManager>(this))
, pollTimer_(std::make_unique<QTimer>(this))
, retryTimer_(std::make_unique<QTimer>(this))
, buildProcess_(std::make_unique<QProcess>(this))
, currentState_(ClientState::Idle)
, building_(false)
, retryCount_(0)
, maxRetries_(DEFAULT_MAX_RETRIES)
, successCount_(0)
, failCount_(0)
, pollIntervalMs_(DEFAULT_POLL_INTERVAL_MS)
, requestTimeoutMs_(DEFAULT_REQUEST_TIMEOUT_MS)
{
// 初始化路径
buildScript_ = QCoreApplication::applicationDirPath() + QDir::separator() + "build.sh";
outputDir_ = QCoreApplication::applicationDirPath() + QDir::separator() + "build_output";
tempDir_ = QDir::tempPath() + QDir::separator() + "build_client_" +
QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss");
// 创建必要的目录
QDir dir(outputDir_);
if (!dir.exists()) {
dir.mkpath(".");
}
QDir tempDir(tempDir_);
if (!tempDir.exists()) {
tempDir.mkpath(".");
}
// 配置定时器
pollTimer_->setInterval(pollIntervalMs_);
pollTimer_->setSingleShot(false);
retryTimer_->setSingleShot(true);
// 连接信号槽
connect(pollTimer_.get(), &QTimer::timeout, this, &BuildClient::pollServer);
connect(retryTimer_.get(), &QTimer::timeout, this, &BuildClient::retryPoll);
// 处理网络错误
connect(networkManager_.get(), &QNetworkAccessManager::finished,
this, [this](QNetworkReply *reply) {
if (reply->error() != QNetworkReply::NoError) {
handleNetworkError(reply, reply->url().toString());
}
QString url = reply->url().toString();
if (url.contains("/poll")) {
onPollReply(reply);
} else if (url.contains("/upload")) {
onUploadReply(reply);
} else if (url.contains("/download")) {
onDownloadReply(reply);
} else if (url.contains("/release")) {
onReleaseInfoReply(reply);
}
reply->deleteLater();
});
// 构建进程信号
connect(buildProcess_.get(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, &BuildClient::onBuildProcessFinished);
connect(buildProcess_.get(), QOverload<QProcess::ProcessError>::of(&QProcess::errorOccurred),
this, &BuildClient::onBuildProcessError);
qDebug() << "[Client] Initialized - Agent:" << agentType_ << "Server:" << serverUrl_;
}
BuildClient::~BuildClient() {
stop();
cleanupTemporaryFiles();
}
void BuildClient::start() {
if (isRunning()) return;
updateState(ClientState::Polling);
pollTimer_->start();
retryCount_ = 0;
qDebug() << "[Client] Started - Polling every" << pollIntervalMs_ << "ms";
qDebug() << "[Client] Server URL:" << serverUrl_;
qDebug() << "[Client] Agent Type:" << agentType_;
qDebug() << "[Client] Output Directory:" << outputDir_;
updateStatus("started", "Client started successfully");
}
void BuildClient::stop() {
if (!isRunning()) return;
updateState(ClientState::Stopped);
pollTimer_->stop();
retryTimer_->stop();
if (buildProcess_ && buildProcess_->state() != QProcess::NotRunning) {
buildProcess_->terminate();
buildProcess_->waitForFinished(WAIT_FOR_FINISHED_MS);
if (buildProcess_->state() != QProcess::NotRunning) {
buildProcess_->kill();
buildProcess_->waitForFinished(WAIT_FOR_FINISHED_MS);
}
}
building_ = false;
qDebug() << "[Client] Stopped - Success:" << successCount_ << ", Failed:" << failCount_;
updateStatus("stopped", QString("Success: %1, Failed: %2").arg(successCount_).arg(failCount_));
}
bool BuildClient::isRunning() const {
LOCK_READ(stateLock_);
return currentState_ != ClientState::Stopped && pollTimer_->isActive();
}
ClientState BuildClient::state() const {
LOCK_READ(stateLock_);
return currentState_;
}
void BuildClient::setBuildScript(const QString &scriptPath) {
buildScript_ = scriptPath;
qDebug() << "[Client] Build script set to:" << buildScript_;
}
void BuildClient::setOutputDir(const QString &outputDir) {
outputDir_ = outputDir;
QDir dir(outputDir_);
if (!dir.exists()) {
dir.mkpath(".");
}
qDebug() << "[Client] Output directory set to:" << outputDir_;
}
void BuildClient::setPollInterval(int milliseconds) {
if (milliseconds >= 1000) {
pollIntervalMs_ = milliseconds;
if (pollTimer_->isActive()) {
pollTimer_->setInterval(pollIntervalMs_);
}
qDebug() << "[Client] Poll interval set to:" << pollIntervalMs_ << "ms";
}
}
void BuildClient::setRequestTimeout(int milliseconds) {
requestTimeoutMs_ = milliseconds;
qDebug() << "[Client] Request timeout set to:" << requestTimeoutMs_ << "ms";
}
void BuildClient::setMaxRetries(int retries) {
maxRetries_ = qMax(0, retries);
qDebug() << "[Client] Max retries set to:" << maxRetries_;
}
int BuildClient::getSuccessCount() const {
return successCount_;
}
int BuildClient::getFailCount() const {
return failCount_;
}
QString BuildClient::getLastError() const {
return lastError_;
}
void BuildClient::pollServer() {
if (building_ || currentState_ == ClientState::Stopped) {
return;
}
QUrlQuery query;
query.addQueryItem("agent", agentType_);
query.addQueryItem("token", token_);
sendGetRequest("/poll", query);
}
void BuildClient::onPollReply(QNetworkReply *reply) {
if (reply->error() != QNetworkReply::NoError) {
return; // 错误已在 handleNetworkError 中处理
}
retryCount_ = 0; // 重置重试计数
QByteArray response = reply->readAll();
qDebug() << "[Client] Poll response:" << response.left(200);
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "[Client] JSON parse error:" << parseError.errorString();
return;
}
QJsonObject obj = doc.object();
QString status = obj["status"].toString();
if (status == "build") {
currentBranch_ = obj["branch"].toString("main");
qDebug() << "[Client] Build requested for branch:" << currentBranch_;
executeBuildScript(currentBranch_);
} else {
// 空闲时检查更新
if (!building_) {
checkForUpdate();
}
}
}
void BuildClient::onUploadReply(QNetworkReply *reply) {
updateState(ClientState::Idle);
building_ = false;
if (reply->error() != QNetworkReply::NoError) {
failCount_++;
updateStatus("upload_failed", reply->errorString());
return;
}
QByteArray response = reply->readAll();
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(response, &parseError);
if (parseError.error == QJsonParseError::NoError) {
QJsonObject obj = doc.object();
QString status = obj["status"].toString();
if (status == "success") {
successCount_++;
qDebug() << "[Client] Upload successful - MD5:" << obj["md5"].toString();
updateStatus("upload_success", "Artifact uploaded successfully");
emit uploadProgress(reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong(),
reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong());
} else {
failCount_++;
QString message = obj["message"].toString();
qWarning() << "[Client] Upload failed:" << message;
updateStatus("upload_failed", message);
}
}
// 清理临时文件
if (!currentArtifactPath_.isEmpty() && QFile::exists(currentArtifactPath_)) {
QFile::remove(currentArtifactPath_);
}
}
void BuildClient::onDownloadReply(QNetworkReply *reply) {
updateState(ClientState::Idle);
if (reply->error() != QNetworkReply::NoError) {
failCount_++;
updateStatus("download_failed", reply->errorString());
return;
}
// 获取 MD5
QString expectedMd5 = QString::fromUtf8(reply->rawHeader("X-MD5"));
// 保存文件
QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
QString filename = QString("download_%1_%2.zip").arg(agentType_).arg(timestamp);
QString filepath = outputDir_ + "/" + filename;
QFile file(filepath);
if (file.open(QIODevice::WriteOnly)) {
QByteArray data = reply->readAll();
file.write(data);
file.close();
qDebug() << "[Client] Downloaded" << data.size() << "bytes to" << filepath;
// 验证 MD5
if (!expectedMd5.isEmpty()) {
if (verifyMD5(filepath, expectedMd5)) {
qDebug() << "[Client] Download successful, MD5 verified:" << expectedMd5;
successCount_++;
updateStatus("download_success", "File downloaded and verified");
emit downloadProgress(data.size(), data.size());
} else {
qWarning() << "[Client] MD5 verification failed! Expected:" << expectedMd5;
failCount_++;
updateStatus("md5_failed", "MD5 verification failed");
QFile::remove(filepath);
}
} else {
qDebug() << "[Client] Download successful (no MD5 verification)";
successCount_++;
updateStatus("download_success", "File downloaded");
}
} else {
qWarning() << "[Client] Failed to save downloaded file";
failCount_++;
updateStatus("save_failed", "Cannot save file");
}
}
void BuildClient::onReleaseInfoReply(QNetworkReply *reply) {
if (reply->error() != QNetworkReply::NoError) {
return;
}
QByteArray response = reply->readAll();
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
return;
}
QJsonObject obj = doc.object();
QString version = obj["version"].toString();
QString md5 = obj["md5"].toString();
// 检查是否有新版本
if (version != lastVersion_ || md5 != lastMd5_) {
qDebug() << "[Client] New version available:" << version;
lastVersion_ = version;
lastMd5_ = md5;
downloadArtifact(version);
}
}
void BuildClient::onBuildProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) {
qint64 buildTimeMs = buildTimer_.elapsed();
if (exitCode == 0 && exitStatus == QProcess::NormalExit) {
qDebug() << "[Client] Build completed successfully in" << (buildTimeMs / 1000.0) << "seconds";
updateStatus("build_success", QString("Completed in %1 ms").arg(buildTimeMs));
QString zipFile = createZipFile(outputDir_);
if (!zipFile.isEmpty()) {
currentArtifactPath_ = zipFile;
uploadArtifact(zipFile);
} else {
failCount_++;
updateStatus("zip_failed", "Failed to create archive");
building_ = false;
updateState(ClientState::Polling);
}
} else {
qCritical() << "[Client] Build failed with exit code:" << exitCode;
failCount_++;
updateStatus("build_failed", QString("Exit code: %1").arg(exitCode));
building_ = false;
updateState(ClientState::Polling);
}
emit buildCompleted(exitCode == 0, exitCode);
}
void BuildClient::onBuildProcessError(QProcess::ProcessError error) {
QString errorMsg;
switch (error) {
case QProcess::FailedToStart:
errorMsg = "Failed to start build process";
break;
case QProcess::Crashed:
errorMsg = "Build process crashed";
break;
case QProcess::Timedout:
errorMsg = "Build process timed out";
break;
default:
errorMsg = "Unknown build process error";
}
qCritical() << "[Client] Build error:" << errorMsg;
failCount_++;
updateStatus("build_error", errorMsg);
building_ = false;
updateState(ClientState::Polling);
emit errorOccurred(errorMsg, static_cast<int>(error));
}
void BuildClient::onNetworkError(QNetworkReply::NetworkError code) {
QString errorMsg = QNetworkReply::errorString(code);
qWarning() << "[Client] Network error:" << errorMsg << "(code:" << code << ")";
emit errorOccurred(errorMsg, code);
}
void BuildClient::retryPoll() {
if (!building_ && currentState_ != ClientState::Stopped) {
pollServer();
}
}
void BuildClient::executeBuildScript(const QString &branch) {
if (building_) {
qWarning() << "[Client] Already building, skipping";
return;
}
building_ = true;
currentBranch_ = branch;
updateState(ClientState::Building);
updateStatus("building", "Build started for branch: " + branch);
emit buildStarted(branch);
// 清理输出目录
QDir outputDir(outputDir_);
outputDir.removeRecursively();
outputDir.mkpath(".");
// 检查构建脚本
if (!QFile::exists(buildScript_)) {
qCritical() << "[Client] Build script not found:" << buildScript_;
building_ = false;
failCount_++;
updateStatus("script_not_found", buildScript_);
updateState(ClientState::Polling);
return;
}
// 准备构建参数
QStringList arguments;
#ifdef Q_OS_WIN
arguments << "/C" << buildScript_;
buildProcess_->start("cmd.exe", arguments);
#else
// 确保脚本可执行
QFile::setPermissions(buildScript_,
QFile::permissions(buildScript_) | QFile::ExeOwner | QFile::ExeUser);
arguments << buildScript_;
buildProcess_->start("bash", arguments);
#endif
// 设置环境变量
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert("BUILD_BRANCH", branch);
env.insert("BUILD_AGENT_TYPE", agentType_);
env.insert("BUILD_OUTPUT_DIR", outputDir_);
buildProcess_->setProcessEnvironment(env);
buildProcess_->setWorkingDirectory(QFileInfo(buildScript_).absolutePath());
if (!buildProcess_->waitForStarted(WAIT_FOR_FINISHED_MS)) {
qCritical() << "[Client] Failed to start build process:" << buildProcess_->errorString();
building_ = false;
failCount_++;
updateStatus("start_failed", buildProcess_->errorString());
updateState(ClientState::Polling);
return;
}
buildTimer_.start();
saveBuildLog("Build started - Branch: " + branch + ", Script: " + buildScript_);
// 设置超时
QTimer::singleShot(PROCESS_TIMEOUT_MS, this, [this]() {
if (building_ && buildProcess_->state() != QProcess::NotRunning) {
qWarning() << "[Client] Build process timed out, terminating...";
buildProcess_->terminate();
}
});
}
void BuildClient::uploadArtifact(const QString &filePath) {
if (!QFile::exists(filePath)) {
qWarning() << "[Client] Artifact not found:" << filePath;
failCount_++;
updateStatus("artifact_not_found", filePath);
building_ = false;
updateState(ClientState::Polling);
return;
}
updateState(ClientState::Uploading);
updateStatus("uploading", "Uploading artifact: " + QFileInfo(filePath).fileName());
QFileInfo fileInfo(filePath);
QMap<QByteArray, QByteArray> headers;
headers["X-Agent-Type"] = agentType_.toUtf8();
headers["X-Filename"] = fileInfo.fileName().toUtf8();
sendFileUpload("/upload", filePath, headers);
}
void BuildClient::downloadArtifact(const QString &version) {
if (building_) {
qDebug() << "[Client] Build in progress, skipping download";
return;
}
updateState(ClientState::Downloading);
updateStatus("downloading", "Downloading version: " + (version.isEmpty() ? "latest" : version));
QUrlQuery query;
query.addQueryItem("agent", agentType_);
query.addQueryItem("token", token_);
if (!version.isEmpty()) {
query.addQueryItem("version", version);
}
sendGetRequest("/download", query);
}
void BuildClient::checkForUpdate() {
QUrlQuery query;
query.addQueryItem("agent", agentType_);
query.addQueryItem("token", token_);
sendGetRequest("/release", query);
}
QString BuildClient::createZipFile(const QString &sourceDir) {
QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
QString zipName = QString("artifact_%1_%2.zip").arg(agentType_).arg(timestamp);
QString zipPath = tempDir_ + "/" + zipName;
qDebug() << "[Client] Creating zip archive:" << zipPath;
QDir sourceDirObj(sourceDir);
QStringList files = sourceDirObj.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
if (files.isEmpty()) {
qWarning() << "[Client] No files to archive in:" << sourceDir;
return QString();
}
bool success = false;
#ifdef Q_OS_WIN
QProcess zipProcess;
QStringList args;
args << "-Command"
<< QString("Compress-Archive -Path '%1\\*' -DestinationPath '%2' -Force")
.arg(sourceDir).arg(zipPath);
zipProcess.start("powershell.exe", args);
success = zipProcess.waitForFinished(PROCESS_TIMEOUT_MS) && zipProcess.exitCode() == 0;
#else
QProcess zipProcess;
zipProcess.setWorkingDirectory(sourceDir);
zipProcess.start("zip", {"-r", zipPath, "."});
success = zipProcess.waitForFinished(PROCESS_TIMEOUT_MS) && zipProcess.exitCode() == 0;
#endif
if (success && QFile::exists(zipPath)) {
qint64 fileSize = QFileInfo(zipPath).size();
qDebug() << "[Client] Zip archive created:" << zipPath << "(" << fileSize << "bytes)";
return zipPath;
} else {
qWarning() << "[Client] Failed to create zip archive";
return QString();
}
}
QString BuildClient::createTemporaryDirectory() const {
QString tempPath = QDir::tempPath() + QDir::separator() +
"build_client_" + QUuid::createUuid().toString(QUuid::WithoutBraces);
QDir().mkpath(tempPath);
return tempPath;
}
void BuildClient::cleanupTemporaryFiles() {
if (!tempDir_.isEmpty() && QDir(tempDir_).exists()) {
QDir(tempDir_).removeRecursively();
qDebug() << "[Client] Cleaned up temporary directory:" << tempDir_;
}
}
bool BuildClient::verifyMD5(const QString &filePath, const QString &expectedMd5) const {
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) return false;
QCryptographicHash hash(QCryptographicHash::Md5);
QByteArray buffer;
buffer.resize(CHUNK_SIZE);
while (!file.atEnd()) {
qint64 bytesRead = file.read(buffer.data(), CHUNK_SIZE);
if (bytesRead > 0) {
hash.addData(buffer.data(), bytesRead);
}
}
file.close();
QString actualMd5 = hash.result().toHex();
return actualMd5.compare(expectedMd5, Qt::CaseInsensitive) == 0;
}
void BuildClient::saveBuildLog(const QString &message) const {
QString logPath = outputDir_ + "/build.log";
QFile file(logPath);
if (file.open(QIODevice::Append | QIODevice::Text)) {
QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz");
QString logEntry = QString("[%1] %2\n").arg(timestamp).arg(message);
file.write(logEntry.toUtf8());
file.close();
}
}
void BuildClient::updateState(ClientState newState) {
LOCK_WRITE(stateLock_);
if (currentState_ != newState) {
ClientState oldState = currentState_;
currentState_ = newState;
emit stateChanged(newState, oldState);
qDebug() << "[Client] State changed:" << static_cast<int>(oldState)
<< "->" << static_cast<int>(newState);
}
}
void BuildClient::updateStatus(const QString &status, const QString &message) {
QString fullMessage = message.isEmpty() ? status : message;
qDebug() << "[Client] Status:" << status << "-" << fullMessage;
emit statusUpdated(status, fullMessage);
saveBuildLog("Status: " + status + " - " + fullMessage);
}
void BuildClient::handleNetworkError(QNetworkReply *reply, const QString &context) {
QNetworkReply::NetworkError error = reply->error();
QString errorString = reply->errorString();
qWarning() << "[Client] Network error in" << context << ":" << errorString << "(code:" << error << ")";
lastError_ = errorString;
// 可重试的错误
bool retryable = (error == QNetworkReply::TimeoutError ||
error == QNetworkReply::ConnectionRefusedError ||
error == QNetworkReply::HostNotFoundError ||
error == QNetworkReply::TemporaryNetworkFailureError);
if (retryable && retryCount_ < maxRetries_) {
retryCount_++;
qDebug() << "[Client] Retrying... (" << retryCount_ << "/" << maxRetries_ << ")";
retryTimer_->start(RETRY_DELAY_MS);
} else {
retryCount_ = 0;
if (currentState_ == ClientState::Polling) {
// 保持轮询状态,继续尝试
pollTimer_->start();
}
}
emit errorOccurred(errorString, error);
}
void BuildClient::scheduleRetry(const std::function<void()> &operation, const QString &operationName) {
if (retryCount_ < maxRetries_) {
retryCount_++;
qDebug() << "[Client] Scheduling retry for" << operationName
<< "(" << retryCount_ << "/" << maxRetries_ << ")";
QTimer::singleShot(RETRY_DELAY_MS, this, [this, operation]() {
operation();
});
} else {
qWarning() << "[Client] Max retries reached for" << operationName;
retryCount_ = 0;
}
}
QNetworkReply* BuildClient::sendGetRequest(const QString &endpoint, const QUrlQuery &extraQuery) {
QUrl url(serverUrl_ + endpoint);
QUrlQuery query = extraQuery;
url.setQuery(query);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setTransferTimeout(requestTimeoutMs_);
return networkManager_->get(request);
}
QNetworkReply* BuildClient::sendPostRequest(const QString &endpoint, const QByteArray &data,
const QMap<QByteArray, QByteArray> &headers) {
QUrl url(serverUrl_ + endpoint);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setTransferTimeout(requestTimeoutMs_);
for (auto it = headers.begin(); it != headers.end(); ++it) {
request.setRawHeader(it.key(), it.value());
}
return networkManager_->post(request, data);
}
QNetworkReply* BuildClient::sendFileUpload(const QString &endpoint, const QString &filePath,
const QMap<QByteArray, QByteArray> &extraHeaders) {
QUrl url(serverUrl_ + endpoint);
QUrlQuery query;
query.addQueryItem("agent", agentType_);
query.addQueryItem("token", token_);
url.setQuery(query);
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "[Client] Cannot open file for upload:" << filePath;
return nullptr;
}
QByteArray fileData = file.readAll();
file.close();
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream");
request.setHeader(QNetworkRequest::ContentLengthHeader, fileData.size());
request.setTransferTimeout(requestTimeoutMs_);
for (auto it = extraHeaders.begin(); it != extraHeaders.end(); ++it) {
request.setRawHeader(it.key(), it.value());
}
qDebug() << "[Client] Uploading file:" << QFileInfo(filePath).fileName()
<< "(" << fileData.size() << "bytes)";
return networkManager_->post(request, fileData);
}
(4)main.cpp
#include <QCoreApplication>
#include <QCommandLineParser>
#include <QCommandLineOption>
#include <QSettings>
#include <QDir>
#include <QSignalMapper>
#include <QDebug>
#include <csignal>
#include "build_client.h"
// 全局指针用于信号处理
static BuildClient *g_client = nullptr;
void signalHandler(int signum) {
Q_UNUSED(signum);
if (g_client) {
qDebug() << "\n[Main] Received shutdown signal, stopping client...";
g_client->stop();
QCoreApplication::quit();
}
}
void printBanner(const QString &serverUrl, const QString &agentType, int pollInterval) {
qDebug() << "========================================";
qDebug() << " Build Client v1.0";
qDebug() << "========================================";
qDebug() << "Server URL: " << serverUrl;
qDebug() << "Agent Type: " << agentType;
qDebug() << "Poll Interval:" << pollInterval << "seconds";
qDebug() << "========================================";
qDebug() << "Press Ctrl+C to stop";
qDebug() << "========================================";
}
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
app.setApplicationName("BuildClient");
app.setApplicationVersion("1.0.0");
// 设置信号处理
signal(SIGINT, signalHandler);
signal(SIGTERM, signalHandler);
QCommandLineParser parser;
parser.setApplicationDescription("Build Client - Build agent for Windows/ARM/Linux");
parser.addHelpOption();
parser.addVersionOption();
// 必需参数
QCommandLineOption agentOption(QStringList() << "a" << "agent",
"Agent type (win_tools, win_services, arm_sim, linux_x86)",
"type");
parser.addOption(agentOption);
// 可选参数
QCommandLineOption serverOption(QStringList() << "s" << "server",
"Server URL (default: http://localhost:8080)",
"url", "http://localhost:8080");
parser.addOption(serverOption);
QCommandLineOption tokenOption(QStringList() << "t" << "token",
"API token (default: build_secret_token_2026)",
"token", "build_secret_token_2026");
parser.addOption(tokenOption);
QCommandLineOption scriptOption(QStringList() << "b" << "build-script",
"Build script path (default: ./build.sh)",
"path");
parser.addOption(scriptOption);
QCommandLineOption outputOption(QStringList() << "o" << "output-dir",
"Output directory (default: ./build_output)",
"path");
parser.addOption(outputOption);
QCommandLineOption pollIntervalOption(QStringList() << "i" << "interval",
"Poll interval in seconds (default: 3)",
"seconds");
parser.addOption(pollIntervalOption);
QCommandLineOption timeoutOption(QStringList() << "timeout",
"Request timeout in seconds (default: 30)",
"seconds");
parser.addOption(timeoutOption);
QCommandLineOption retriesOption(QStringList() << "r" << "retries",
"Max retry count for failed requests (default: 3)",
"count");
parser.addOption(retriesOption);
parser.process(app);
// 验证必需参数
QString agentType = parser.value(agentOption);
if (agentType.isEmpty()) {
qCritical() << "Error: Agent type is required. Use -a or --agent";
parser.showHelp(1);
return 1;
}
// 验证 agent 类型
QStringList validAgents = {"win_tools", "win_services", "arm_sim", "linux_x86"};
if (!validAgents.contains(agentType)) {
qCritical() << "Error: Invalid agent type:" << agentType;
qCritical() << "Valid types:" << validAgents.join(", ");
return 1;
}
// 解析参数
QString serverUrl = parser.value(serverOption);
QString token = parser.value(tokenOption);
QString buildScript = parser.value(scriptOption);
QString outputDir = parser.value(outputOption);
int pollIntervalSec = parser.value(pollIntervalOption).toInt();
int timeoutSec = parser.value(timeoutOption).toInt();
int maxRetries = parser.value(retriesOption).toInt();
// 设置默认值
if (pollIntervalSec <= 0) pollIntervalSec = 3;
if (timeoutSec <= 0) timeoutSec = 30;
if (maxRetries < 0) maxRetries = 3;
// 创建客户端
BuildClient client(serverUrl, agentType, token);
g_client = &client;
// 配置客户端
if (!buildScript.isEmpty()) {
client.setBuildScript(buildScript);
}
if (!outputDir.isEmpty()) {
client.setOutputDir(outputDir);
}
client.setPollInterval(pollIntervalSec * 1000);
client.setRequestTimeout(timeoutSec * 1000);
client.setMaxRetries(maxRetries);
// 连接信号用于监控
QObject::connect(&client, &BuildClient::stateChanged,
[](ClientState newState, ClientState oldState) {
qDebug() << "[Main] State:" << static_cast<int>(oldState)
<< "->" << static_cast<int>(newState);
});
QObject::connect(&client, &BuildClient::buildStarted,
[](const QString &branch) {
qDebug() << "[Main] Build started for branch:" << branch;
});
QObject::connect(&client, &BuildClient::buildCompleted,
[](bool success, int exitCode) {
if (success) {
qDebug() << "[Main] Build completed successfully!";
} else {
qDebug() << "[Main] Build failed with exit code:" << exitCode;
}
});
QObject::connect(&client, &BuildClient::errorOccurred,
[](const QString &error, int errorCode) {
qDebug() << "[Main] Error:" << error << "(code:" << errorCode << ")";
});
// 启动客户端
client.start();
// 打印启动信息
printBanner(serverUrl, agentType, pollIntervalSec);
// 运行事件循环
int result = app.exec();
// 清理
g_client = nullptr;
qDebug() << "[Main] Client shutdown complete. Final stats - Success:"
<< client.getSuccessCount() << ", Failed:" << client.getFailCount();
return result;
}
三、整体交互流程
- 服务端启动,初始化构建任务,监听端口;
- 客户端启动,定期轮询服务端
/poll接口; - 外部触发服务端
/trigger接口,标记对应类型的构建任务为pending; - 客户端轮询到
build指令后,执行对应平台的构建脚本; - 构建成功后,客户端打包产物并通过
/upload接口上传至服务端; - 服务端接收并保存产物,更新发布元信息,重置任务状态;
- 其他客户端 / 系统可通过
/download下载产物,或通过/stats//release查询状态 / 版本信息。
四、总结
可以增加功能:在trigger接口出发后,可以在poll接口中先发送基础文档到客户端,客户端可以使用基础文档作为输入,后期可以逐步增加完善。