cutelyst 4.8.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
unixfork.cpp
1/*
2 * SPDX-FileCopyrightText: (C) 2014-2020 Daniel Nicoletti <dantti12@gmail.com>
3 * SPDX-License-Identifier: BSD-3-Clause
4 */
5#include "unixfork.h"
6
7#include "server.h"
8
9#if defined(HAS_EventLoopEPoll)
10# include "EventLoopEPoll/eventdispatcher_epoll.h"
11#endif
12
13#include <sys/stat.h>
14#include <sys/types.h>
15#include <unistd.h>
16
17#if defined(__FreeBSD__) || defined(__GNU_kFreeBSD__)
18# include <sys/cpuset.h>
19# include <sys/param.h>
20#endif
21
22#include <errno.h>
23#include <grp.h>
24#include <iostream>
25#include <pwd.h>
26#include <signal.h>
27#include <stdio.h>
28#include <stdlib.h>
29#include <sys/socket.h>
30#include <sys/wait.h>
31#include <unistd.h>
32
33#include <QAbstractEventDispatcher>
34#include <QCoreApplication>
35#include <QFile>
36#include <QLoggingCategory>
37#include <QMutex>
38#include <QSocketNotifier>
39#include <QThread>
40#include <QTimer>
41
42Q_LOGGING_CATEGORY(C_SERVER_UNIX, "cutelyst.server.unix", QtWarningMsg)
43
44#pragma GCC diagnostic push
45#pragma GCC diagnostic ignored "-Wunused-result"
46
47static int signalsFd[2];
48
49UnixFork::UnixFork(int process, int threads, bool setupSignals, QObject *parent)
50 : AbstractFork(parent)
51 , m_threads(threads)
52 , m_processes(process)
53{
54 if (setupSignals) {
55 setupUnixSignalHandlers();
56 }
57}
58
59UnixFork::~UnixFork()
60{
61 if (m_child) {
62 _exit(0);
63 }
64}
65
67{
68 Q_UNUSED(exit)
69 return true;
70}
71
72int UnixFork::exec(bool lazy, bool master)
73{
74 if (master) {
75 std::cout << "spawned WSGI master process (pid: " << QCoreApplication::applicationPid()
76 << ")" << std::endl;
77 }
78
79 int ret;
80 if (lazy) {
81 if (master) {
82 ret = internalExec();
83 } else {
84 std::cerr << "*** Master mode must be set on lazy mode" << std::endl;
85 ret = -1;
86 }
87 } else {
88 if (m_processes > 0) {
89 ret = internalExec();
90 } else {
91 Q_EMIT forked(0);
92 ret = qApp->exec();
93 }
94 }
95
96 return ret;
97}
98
100{
101 auto it = m_childs.begin();
102 while (it != m_childs.end()) {
103 it.value().restart = 1; // Mark as requiring restart
104 terminateChild(it.key());
105 ++it;
106 }
107
108 setupCheckChildTimer();
109}
110
111int UnixFork::internalExec()
112{
113 int ret;
114 bool respawn = false;
115 do {
116 if (!createProcess(respawn)) {
117 return 1;
118 }
119 respawn = true;
120
121 installTouchReload();
122
123 ret = qApp->exec();
124
125 removeTouchReload();
126 } while (!m_terminating);
127
128 return ret;
129}
130
131bool UnixFork::createProcess(bool respawn)
132{
133 if (respawn) {
134 m_recreateWorker.removeIf([this, respawn](Worker worker) {
135 worker.restart = 0;
136 if (!createChild(worker, respawn)) {
137 std::cout << "CHEAPING worker: " << worker.id << std::endl;
138 --m_processes;
139 }
140
141 return true; // Clean recreate worker list
142 });
143 } else {
144 for (int i = 0; i < m_processes; ++i) {
145 Worker worker;
146 worker.id = i + 1;
147 worker.null = false;
148 createChild(worker, respawn);
149 }
150 }
151
152 return !m_childs.empty();
153}
154
155void UnixFork::decreaseWorkerRespawn()
156{
157 int missingRespawn = 0;
158 auto it = m_childs.begin();
159 while (it != m_childs.end()) {
160 if (it.value().respawn > 0) {
161 --it.value().respawn;
162 missingRespawn += it.value().respawn;
163 }
164 ++it;
165 }
166
167 if (missingRespawn) {
168 QTimer::singleShot(std::chrono::seconds{1}, this, &UnixFork::decreaseWorkerRespawn);
169 }
170}
171
173{
174 const auto childs = m_childs.keys();
175 for (qint64 pid : childs) {
176 killChild(pid);
177 }
178}
179
180void UnixFork::killChild(qint64 pid)
181{
182 // qCDebug(C_SERVER_UNIX) << "SIGKILL " << pid;
183 ::kill(pid_t(pid), SIGKILL);
184}
185
187{
188 const auto childs = m_childs.keys();
189 for (qint64 pid : childs) {
190 terminateChild(pid);
191 }
192}
193
194void UnixFork::terminateChild(qint64 pid)
195{
196 // qCDebug(C_SERVER_UNIX) << "SIGQUIT " << pid;
197 ::kill(pid_t(pid), SIGQUIT);
198}
199
200void UnixFork::stopWSGI(const QString &pidfile)
201{
202 QFile file(pidfile);
203 if (!file.open(QFile::ReadOnly | QFile::Text)) {
204 std::cerr << "Failed open pid file " << qPrintable(pidfile) << std::endl;
205 exit(1);
206 }
207
208 QByteArray piddata = file.readLine().simplified();
209 qint64 pid = piddata.toLongLong();
210 if (pid < 2) {
211 std::cerr << "Failed read pid file " << qPrintable(pidfile) << std::endl;
212 exit(1);
213 }
214
215 ::kill(pid_t(pid), SIGINT);
216 exit(0);
217}
218
219bool UnixFork::setUmask(const QByteArray &valueStr)
220{
221 if (valueStr.size() < 3) {
222 std::cerr << "umask too small" << std::endl;
223 return false;
224 }
225
226 const char *value = valueStr.constData();
227 mode_t mode = 0;
228 if (valueStr.size() == 3) {
229 mode = (mode << 3) + (value[0] - '0');
230 mode = (mode << 3) + (value[1] - '0');
231 mode = (mode << 3) + (value[2] - '0');
232 } else {
233 mode = (mode << 3) + (value[1] - '0');
234 mode = (mode << 3) + (value[2] - '0');
235 mode = (mode << 3) + (value[3] - '0');
236 }
237 std::cout << "umask() " << value << std::endl;
238
239 umask(mode);
240
241 return true;
242}
243
244void UnixFork::signalHandler(int signal)
245{
246 // qDebug() << Q_FUNC_INFO << signal << QCoreApplication::applicationPid();
247 char sig = signal;
248 write(signalsFd[0], &sig, sizeof(sig));
249}
250
251void UnixFork::setupCheckChildTimer()
252{
253 if (!m_checkChildRestart) {
254 m_checkChildRestart = new QTimer(this);
255 m_checkChildRestart->start(std::chrono::milliseconds{500});
256 connect(m_checkChildRestart, &QTimer::timeout, this, &UnixFork::handleSigChld);
257 }
258}
259
260void UnixFork::postFork(int workerId)
261{
262 // Child must not have parent timers
263 delete m_checkChildRestart;
264
265 Q_EMIT forked(workerId - 1);
266}
267
268bool UnixFork::setGidUid(const QString &gid, const QString &uid, bool noInitgroups)
269{
270 bool ok;
271
272 if (!gid.isEmpty()) {
273 uint gidInt = gid.toUInt(&ok);
274 if (!ok) {
275 struct group *ugroup = getgrnam(qUtf8Printable(gid));
276 if (ugroup) {
277 gidInt = ugroup->gr_gid;
278 } else {
279 std::cerr << "setgid group %s not found." << qUtf8Printable(gid) << std::endl;
280 return false;
281 }
282 }
283
284 if (setgid(gidInt)) {
285 std::cerr << "Failed to set gid '%s'" << strerror(errno) << std::endl;
286 return false;
287 }
288 std::cout << "setgid() to " << gidInt << std::endl;
289
290 if (noInitgroups || uid.isEmpty()) {
291 if (setgroups(0, nullptr)) {
292 std::cerr << "Failed to setgroups()" << std::endl;
293 return false;
294 }
295 } else {
296 QByteArray uidname;
297 uint uidInt = uid.toUInt(&ok);
298 if (ok) {
299 struct passwd *pw = getpwuid(uidInt);
300 if (pw) {
301 uidname = pw->pw_name;
302 }
303 } else {
304 uidname = uid.toUtf8();
305 }
306
307 if (initgroups(uidname.constData(), gidInt)) {
308 std::cerr << "Failed to setgroups()" << std::endl;
309 return false;
310 }
311 }
312 }
313
314 if (!uid.isEmpty()) {
315 uint uidInt = uid.toUInt(&ok);
316 if (!ok) {
317 struct passwd *upasswd = getpwnam(qUtf8Printable(uid));
318 if (upasswd) {
319 uidInt = upasswd->pw_uid;
320 } else {
321 std::cerr << "setuid user" << qUtf8Printable(uid) << "not found." << std::endl;
322 return false;
323 }
324 }
325
326 if (setuid(uidInt)) {
327 std::cerr << "Failed to set uid:" << strerror(errno) << std::endl;
328 return false;
329 }
330 std::cout << "setuid() to " << uidInt << std::endl;
331 }
332 return true;
333}
334
335void UnixFork::chownSocket(const QString &filename, const QString &uidGid)
336{
337 struct group *new_group = nullptr;
338 struct passwd *new_user = nullptr;
339
340 const QString owner = uidGid.section(QLatin1Char(':'), 0, 0);
341
342 bool ok;
343 uid_t new_uid = owner.toUInt(&ok);
344
345 if (!ok) {
346 new_user = getpwnam(qUtf8Printable(owner));
347 if (!new_user) {
348 qFatal("unable to find user '%s'", qUtf8Printable(owner));
349 }
350 new_uid = new_user->pw_uid;
351 }
352
353 gid_t new_gid = -1u;
354 const QString group = uidGid.section(QLatin1Char(':'), 1, 1);
355 if (!group.isEmpty()) {
356 new_gid = group.toUInt(&ok);
357 if (!ok) {
358 new_group = getgrnam(qUtf8Printable(group));
359 if (!new_group) {
360 qFatal("unable to find group '%s'", qUtf8Printable(group));
361 }
362 new_gid = new_group->gr_gid;
363 }
364 }
365
366 if (chown(qUtf8Printable(filename), new_uid, new_gid)) {
367 qFatal("chown() error '%s'", strerror(errno));
368 }
369}
370
371#ifdef Q_OS_LINUX
372// static int cpuSockets = -1;
373
374// socket/cores
375int parseProcCpuinfo()
376{
377 int cpuSockets = 1;
378 // std::pair<int, int> ret;
379
380 // static QMutex mutex;
381 // QMutexLocker locker(&mutex);
382 QFile file(QStringLiteral("/proc/cpuinfo"));
383 if (!file.open(QFile::ReadOnly | QFile::Text)) {
384 qCWarning(C_SERVER_UNIX) << "Failed to open file" << file.errorString();
385 // cpuSockets = 1;
386 // cpuCores = QThread::idealThreadCount();
387 return cpuSockets;
388 }
389
390 char buf[1024];
391 qint64 lineLength;
392 QByteArrayList physicalIds;
393 // cpuCores = 0;
394 while ((lineLength = file.readLine(buf, sizeof(buf))) != -1) {
395 const QByteArray line(buf, int(lineLength));
396 if (line.startsWith("physical id\t: ")) {
397 const QByteArray id = line.mid(14).trimmed();
398 if (!physicalIds.contains(id)) {
399 physicalIds.push_back(id);
400 }
401 } /* else if (line.startsWith("processor \t: ")) {
402 ++cpuCores;
403 }*/
404 }
405
406 // if (cpuCores == 0) {
407 // cpuCores = QThread::idealThreadCount();
408 // }
409
410 if (physicalIds.size()) {
411 cpuSockets = physicalIds.size();
412 } else {
413 cpuSockets = 1;
414 }
415 return cpuSockets;
416}
417#endif
418
419int UnixFork::idealProcessCount()
420{
421#ifdef Q_OS_LINUX
422 static int cpuSockets = parseProcCpuinfo();
423
424 return cpuSockets;
425#else
426 return 1;
427#endif
428}
429
430int UnixFork::idealThreadCount()
431{
432#ifdef Q_OS_LINUX
433 static int cpuCores = qMax(1, QThread::idealThreadCount());
434
435 return cpuCores;
436#else
437 return qMax(1, QThread::idealThreadCount());
438#endif
439}
440
441void UnixFork::handleSigHup()
442{
443 // do Qt stuff
444 // qDebug() << Q_FUNC_INFO << QCoreApplication::applicationPid();
445 // m_proc->kill();
446}
447
448void UnixFork::handleSigTerm()
449{
450 // do Qt stuff
451 // qDebug() << Q_FUNC_INFO << QCoreApplication::applicationPid();
452 // qApp->quit();
453 // m_proc->terminate();
454}
455
456void UnixFork::handleSigInt()
457{
458 // do Qt stuff
459 // qDebug() << Q_FUNC_INFO << QCoreApplication::applicationPid();
460 m_terminating = true;
461 if (m_child || (m_childs.isEmpty())) {
462 qDebug(C_SERVER_UNIX) << "SIGINT/SIGQUIT received, worker shutting down...";
463 Q_EMIT shutdown();
464 } else {
465 std::cout << "SIGINT/SIGQUIT received, terminating workers..." << std::endl;
466 setupCheckChildTimer();
467
468 static int count = 0;
469 if (count++ > 2) {
470 std::cout << "KILL workers..." << std::endl;
471 killChild();
472 QTimer::singleShot(std::chrono::seconds{3}, qApp, &QCoreApplication::quit);
473 } else if (count > 1) {
475 } else {
476 QTimer::singleShot(std::chrono::seconds{30}, this, [this]() {
477 std::cout << "workers terminating timeout, KILL ..." << std::endl;
478 killChild();
479 QTimer::singleShot(std::chrono::seconds{30}, qApp, &QCoreApplication::quit);
480 });
481
483 }
484 }
485}
486
487void UnixFork::handleSigChld()
488{
489 pid_t p;
490 int status;
491
492 while ((p = waitpid(-1, &status, WNOHANG)) > 0) {
493 /* Handle the death of pid p */
494 // qCDebug(C_SERVER_UNIX) << "SIGCHLD worker died" << p << WEXITSTATUS(status);
495 // SIGTERM is used when CHEAPED (ie post fork failed)
496 int exitStatus = WEXITSTATUS(status);
497
498 Worker worker;
499 auto it = m_childs.constFind(p);
500 if (it != m_childs.constEnd()) {
501 worker = it.value();
502 m_childs.erase(it);
503 } else {
504 std::cout << "DAMN ! *UNKNOWN* worker (pid: " << p << ") died, killed by signal "
505 << exitStatus << " :( ignoring .." << std::endl;
506 continue;
507 }
508
509 if (WIFEXITED(status) && exitStatus == 15 && worker.restart == 0) {
510 // Child process cheaping
511 worker.null = true;
512 }
513
514 if (!worker.null && !m_terminating) {
515 if (worker.restart == 0) {
516 std::cout << "DAMN ! worker " << worker.id << " (pid: " << p
517 << ") died, killed by signal " << exitStatus << " :( trying respawn .."
518 << std::endl;
519 }
520 worker.restart = 0;
521 ++worker.respawn;
522 QTimer::singleShot(std::chrono::seconds{1}, this, &UnixFork::decreaseWorkerRespawn);
523 m_recreateWorker.push_back(worker);
524 qApp->quit();
525 } else if (!m_child && m_childs.isEmpty()) {
526 qApp->quit();
527 }
528 }
529
530 if (m_checkChildRestart) {
531 bool allRestarted = true;
532 auto it = m_childs.begin();
533 while (it != m_childs.end()) {
534 if (it.value().restart) {
535 if (++it.value().restart > 10) {
536 killChild(it.key());
537 }
538 allRestarted = false;
539 }
540 ++it;
541 }
542
543 if (allRestarted) {
544 m_checkChildRestart->deleteLater();
545 m_checkChildRestart = nullptr;
546 }
547 }
548}
549
550void UnixFork::setSched(Cutelyst::Server *wsgi, int workerId, int workerCore)
551{
552 int cpu_affinity = wsgi->cpuAffinity();
553 if (cpu_affinity) {
554 char buf[4096];
555
556 int pos =
557 snprintf(buf, 4096, "mapping worker %d core %d to CPUs:", workerId + 1, workerCore + 1);
558 if (pos < 25 || pos >= 4096) {
559 qCCritical(C_SERVER_UNIX) << "unable to initialize cpu affinity !!!";
560 exit(1);
561 }
562#if defined(__linux__) || defined(__GNU_kFreeBSD__)
563 cpu_set_t cpuset;
564#elif defined(__FreeBSD__)
565 cpuset_t cpuset;
566#endif
567#if defined(__linux__) || defined(__FreeBSD__) || defined(__GNU_kFreeBSD__)
568 int coreCount = idealThreadCount();
569
570 int workerThreads = 1;
571 if (wsgi->threads().compare(u"auto") == 0) {
572 workerThreads = coreCount;
573 } else if (wsgi->threads().toInt() > 1) {
574 workerThreads = wsgi->threads().toInt();
575 }
576
577 int base_cpu;
578 if (workerThreads > 1) {
579 base_cpu = (workerId * workerThreads) + workerCore * cpu_affinity;
580 } else {
581 base_cpu = workerId * cpu_affinity;
582 }
583
584 if (base_cpu >= coreCount) {
585 base_cpu = base_cpu % coreCount;
586 }
587
588 CPU_ZERO(&cpuset);
589 for (int i = 0; i < cpu_affinity; i++) {
590 if (base_cpu >= coreCount)
591 base_cpu = 0;
592 CPU_SET(base_cpu, &cpuset);
593 int ret = snprintf(buf + pos, 4096 - pos, " %d", base_cpu + 1);
594 if (ret < 2 || ret >= 4096) {
595 qCCritical(C_SERVER_UNIX) << "unable to initialize cpu affinity !!!";
596 exit(1);
597 }
598 pos += ret;
599 base_cpu++;
600 }
601#endif
602#if defined(__linux__) || defined(__GNU_kFreeBSD__)
603 if (sched_setaffinity(0, sizeof(cpu_set_t), &cpuset)) {
604 qFatal("failed to sched_setaffinity()");
605 }
606#elif defined(__FreeBSD__)
607 if (cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, -1, sizeof(cpuset), &cpuset)) {
608 qFatal("cpuset_setaffinity");
609 }
610#endif
611 std::cout << buf << std::endl;
612 }
613}
614
615int UnixFork::setupUnixSignalHandlers()
616{
617 setupSocketPair(false, true);
618
619 // struct sigaction hup;
620 // hup.sa_handler = UnixFork::signalHandler;
621 // sigemptyset(&hup.sa_mask);
622 // hup.sa_flags = 0;
623 // hup.sa_flags |= SA_RESTART;
624
625 // if (sigaction(SIGHUP, &hup, 0) > 0)
626 // return 1;
627
628 // struct sigaction term;
629 // term.sa_handler = UnixFork::signalHandler;
630 // sigemptyset(&term.sa_mask);
631 // term.sa_flags |= SA_RESTART;
632
633 // if (sigaction(SIGTERM, &term, 0) > 0)
634 // return 2;
635
636 struct sigaction action;
637
638 // qDebug() << Q_FUNC_INFO << QCoreApplication::applicationPid();
639
640 memset(&action, 0, sizeof(struct sigaction));
641 action.sa_handler = UnixFork::signalHandler;
642 sigemptyset(&action.sa_mask);
643 action.sa_flags |= SA_RESTART;
644 if (sigaction(SIGINT, &action, nullptr) > 0)
645 return SIGINT;
646
647 memset(&action, 0, sizeof(struct sigaction));
648 action.sa_handler = UnixFork::signalHandler;
649 sigemptyset(&action.sa_mask);
650 action.sa_flags |= SA_RESTART;
651 if (sigaction(SIGQUIT, &action, nullptr) > 0)
652 return SIGQUIT;
653
654 memset(&action, 0, sizeof(struct sigaction));
655 action.sa_handler = UnixFork::signalHandler;
656 sigemptyset(&action.sa_mask);
657 action.sa_flags |= SA_RESTART;
658
659 if (sigaction(SIGCHLD, &action, nullptr) > 0)
660 return SIGCHLD;
661
662 return 0;
663}
664
665void UnixFork::setupSocketPair(bool closeSignalsFD, bool createPair)
666{
667 if (closeSignalsFD) {
668 close(signalsFd[0]);
669 close(signalsFd[1]);
670 }
671
672 if (createPair && ::socketpair(AF_UNIX, SOCK_STREAM, 0, signalsFd)) {
673 qFatal("Couldn't create SIGNALS socketpair");
674 }
675 delete m_signalNotifier;
676
677 m_signalNotifier = new QSocketNotifier(signalsFd[1], QSocketNotifier::Read, this);
678 connect(m_signalNotifier, &QSocketNotifier::activated, this, [this]() {
679 char signal;
680 read(signalsFd[1], &signal, sizeof(signal));
681
682 // qCDebug(C_SERVER_UNIX) << "Got signal:" << static_cast<int>(signal) << "pid:" <<
683 // QCoreApplication::applicationPid();
684 switch (signal) {
685 case SIGCHLD:
686 QTimer::singleShot(std::chrono::seconds{0}, this, &UnixFork::handleSigChld);
687 break;
688 case SIGINT:
689 case SIGQUIT:
690 handleSigInt();
691 break;
692 default:
693 break;
694 }
695 });
696}
697
698bool UnixFork::createChild(const Worker &worker, bool respawn)
699{
700 if (m_child) {
701 return false;
702 }
703
704 delete m_signalNotifier;
705 m_signalNotifier = nullptr;
706
707 qint64 childPID = fork();
708
709 if (childPID >= 0) {
710 if (childPID == 0) {
711 if (worker.respawn >= 5) {
712 std::cout << "WSGI worker " << worker.id << " respawned too much, sleeping a bit"
713 << std::endl;
714 sleep(2);
715 }
716
717#if defined(HAS_EventLoopEPoll)
719 if (epoll) {
720 epoll->reinstall();
721 }
722#endif
723
724 setupSocketPair(true, true);
725
726 m_child = true;
727 postFork(worker.id);
728
729 int ret = qApp->exec();
730 _exit(ret);
731 } else {
732 setupSocketPair(false, false);
733
734 if (respawn) {
735 std::cout << "Respawned WSGI worker " << worker.id << " (new pid: " << childPID
736 << ", cores: " << m_threads << ")" << std::endl;
737 } else {
738 if (m_processes == 1) {
739 std::cout << "spawned WSGI worker (and the only) (pid: " << childPID
740 << ", cores: " << m_threads << ")" << std::endl;
741 } else {
742 std::cout << "spawned WSGI worker " << worker.id << " (pid: " << childPID
743 << ", cores: " << m_threads << ")" << std::endl;
744 }
745 }
746 m_childs.insert(childPID, worker);
747 return true;
748 }
749 } else {
750 qFatal("Fork failed, quitting!!!!!!");
751 }
752
753 return false;
754}
755
756#include "moc_unixfork.cpp"
Implements a web server.
Definition server.h:60
QString threads
Definition server.h:150
virtual void terminateChild() override
Definition unixfork.cpp:186
virtual bool continueMaster(int *exit=nullptr) override
Definition unixfork.cpp:66
virtual void restart() override
Definition unixfork.cpp:99
virtual int exec(bool lazy, bool master) override
Definition unixfork.cpp:72
virtual void killChild() override
Definition unixfork.cpp:172
QAbstractEventDispatcher * instance(QThread *thread)
const char * constData() const const
QByteArray mid(qsizetype pos, qsizetype len) &&
qsizetype size() const const
qlonglong toLongLong(bool *ok, int base) const const
QByteArray trimmed() const const
qint64 applicationPid()
bool contains(const AT &value) const const
void push_back(QList< T >::parameter_type value)
qsizetype size() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
T qobject_cast(QObject *object)
void activated(QSocketDescriptor socket, QSocketNotifier::Type type)
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
bool isEmpty() const const
QString section(QChar sep, qsizetype start, qsizetype end, QString::SectionFlags flags) const const
int toInt(bool *ok, int base) const const
uint toUInt(bool *ok, int base) const const
QByteArray toUtf8() const const
int idealThreadCount()
void timeout()