diff --git a/servatrice/dev/main.cpp b/servatrice/dev/main.cpp index 39195f565..9ebba095b 100644 --- a/servatrice/dev/main.cpp +++ b/servatrice/dev/main.cpp @@ -15,12 +15,14 @@ #include "smtpclient.h" #include "version_string.h" +#include #include #include #include #include #include #include +#include #include #include #include @@ -88,6 +90,23 @@ void dumpState(Servatrice &server, const Servatrice_InMemoryDatabaseInterface &d std::cout << "servatrice-dev: log entries=" << database.getLogEntries().size() << std::endl; } +/** + * @brief Publishes the bound port the way a script can read it. + * + * The stdout announcement is written from this thread while the logger thread writes to the same + * stream, so a reader can see a log line inside it. A file written through QSaveFile is either + * absent or complete, which makes the port deterministic to consume. + */ +bool writeListeningPort(const QString &path, int port) +{ + QSaveFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + return false; + } + file.write(QByteArray::number(port) + '\n'); + return file.commit(); +} + } // namespace int main(int argc, char *argv[]) @@ -103,9 +122,13 @@ int main(int argc, char *argv[]) parser.addVersionOption(); QCommandLineOption portOption({"p", "port"}, "TCP port to listen on; 0 picks an ephemeral port.", "port", "0"); + QCommandLineOption portFileOption("port-file", + "Write the bound listening port to this file once the server listens.", + "path"); QCommandLineOption seedOption("seed", "Seed for the game RNG; random when omitted.", "seed"); QCommandLineOption dumpStateOption("dump-state", "Print the server state after startup and keep serving."); parser.addOption(portOption); + parser.addOption(portFileOption); parser.addOption(seedOption); parser.addOption(dumpStateOption); parser.process(app); @@ -169,11 +192,18 @@ int main(int argc, char *argv[]) std::cerr << "servatrice-dev: the server is not listening" << std::endl; retval = EXIT_FAILURE; } else { - std::cout << "servatrice-dev: listening on port " << listeningPort << std::endl; - if (parser.isSet(dumpStateOption)) { - dumpState(*server, *server->getInMemoryDatabase()); + const QString portFilePath = parser.value(portFileOption); + if (!portFilePath.isEmpty() && !writeListeningPort(portFilePath, listeningPort)) { + std::cerr << "servatrice-dev: cannot write the listening port to " << portFilePath.toStdString() + << std::endl; + retval = EXIT_FAILURE; + } else { + std::cout << "servatrice-dev: listening on port " << listeningPort << std::endl; + if (parser.isSet(dumpStateOption)) { + dumpState(*server, *server->getInMemoryDatabase()); + } + retval = QCoreApplication::exec(); } - retval = QCoreApplication::exec(); } } diff --git a/tests/server_dev_smoke_test.cpp b/tests/server_dev_smoke_test.cpp index adcb8fe0c..7d89bcf9c 100644 --- a/tests/server_dev_smoke_test.cpp +++ b/tests/server_dev_smoke_test.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -92,6 +93,7 @@ protected: void SetUp() override { ASSERT_TRUE(tempDir.isValid()); + portFilePath = tempDir.filePath(QStringLiteral("listening-port")); // servatrice-dev writes its throwaway settings under QDir::tempPath(); pointing that at // a directory the test owns means a terminated server cannot leave an ini behind. @@ -102,9 +104,10 @@ protected: process.setProcessEnvironment(environment); process.setProcessChannelMode(QProcess::SeparateChannels); process.start(QStringLiteral(SERVATRICE_DEV_BINARY), - {QStringLiteral("--port"), QStringLiteral("0"), QStringLiteral("--seed"), QStringLiteral("42")}); + {QStringLiteral("--port"), QStringLiteral("0"), QStringLiteral("--seed"), QStringLiteral("42"), + QStringLiteral("--port-file"), portFilePath}); ASSERT_TRUE(process.waitForStarted(kStartupTimeoutMs)) << process.errorString().toStdString(); - ASSERT_TRUE(readListeningPort()) << "servatrice-dev did not announce a listening port.\nstdout:\n" + ASSERT_TRUE(readListeningPort()) << "servatrice-dev did not publish its listening port.\nstdout:\n" << stdoutText.toStdString() << "\nstderr:\n" << stderrText.toStdString(); @@ -128,34 +131,32 @@ protected: } } - /// @brief Reads the child's output until it announces the ephemeral port it listens on. + /// @brief Waits for the child to publish the ephemeral port it listens on. bool readListeningPort() { - const QString prefix = QStringLiteral("servatrice-dev: listening on port "); QElapsedTimer elapsed; elapsed.start(); while (elapsed.elapsed() < kStartupTimeoutMs) { - if (!process.waitForReadyRead(100)) { - if (process.state() == QProcess::NotRunning) { - drainProcessOutput(); - return false; - } - continue; - } + // The child logs heavily during startup; keep its pipes drained so it cannot block. drainProcessOutput(); - const int at = stdoutText.indexOf(prefix); - if (at < 0) { - continue; + QFile portFile(portFilePath); + if (portFile.open(QIODevice::ReadOnly)) { + bool ok = false; + const int port = QString::fromLatin1(portFile.readAll()).trimmed().toInt(&ok); + if (ok && port > 0) { + listeningPort = port; + return true; + } } - const int end = stdoutText.indexOf(QLatin1Char('\n'), at); - if (end < 0) { - continue; + + if (process.state() == QProcess::NotRunning) { + drainProcessOutput(); + return false; } - bool ok = false; - listeningPort = stdoutText.mid(at + prefix.size(), end - at - prefix.size()).trimmed().toInt(&ok); - return ok && listeningPort > 0; + QThread::msleep(20); } + drainProcessOutput(); return false; } @@ -228,6 +229,7 @@ protected: QProcess process; QTemporaryDir tempDir; + QString portFilePath; StubNetworkSettingsProvider provider; std::unique_ptr client; int listeningPort = 0;