Compare commits
110 Commits
v2.0.0
...
user/bserg
Author | SHA1 | Date | |
---|---|---|---|
067c128474 | |||
a127d9ef0d | |||
4a845e3cc4 | |||
633afa5bed | |||
49fd2a9e53 | |||
6264a8b41d | |||
3990d3bcbf | |||
aa3f201ced | |||
83c261977d | |||
6ca28d96bf | |||
c4a5647b62 | |||
720d5593a5 | |||
13fa325134 | |||
773cbb4907 | |||
a696264b48 | |||
b7db5f77fb | |||
b11678e636 | |||
f746070944 | |||
3323a51ab5 | |||
0e59927384 | |||
5c4840f129 | |||
9ac02323ad | |||
cdbed26d1f | |||
23f171f34d | |||
20b625e483 | |||
f1604c6460 | |||
ba0e007c05 | |||
643e1bf20f | |||
24a32a0603 | |||
c5caf32b77 | |||
09956d7500 | |||
d91c896e46 | |||
042e6a22b8 | |||
14ec12d1f0 | |||
288b05a048 | |||
5af3096070 | |||
570fa01c04 | |||
2a69038c4c | |||
0ba127e447 | |||
7714bdf7e0 | |||
4e5e7ae50a | |||
5741b2f6c1 | |||
76172f92e9 | |||
f8b547c028 | |||
7ccd9e1709 | |||
9217b27d40 | |||
819e9025b1 | |||
53ceab9f91 | |||
a7ed4fe5c3 | |||
3190cd322d | |||
dad2b64e15 | |||
e527ab1613 | |||
d7a0bc212d | |||
aecd5e9c94 | |||
e0edca43d5 | |||
ce70d3d728 | |||
d9be40a0de | |||
e469f04c39 | |||
11774e6825 | |||
42bdfb51c3 | |||
fd637bf1e1 | |||
8085e1416c | |||
671c9f805f | |||
ace7a7ccae | |||
9c3bdf1a77 | |||
f5242b3102 | |||
f1272f059a | |||
91595ff4c2 | |||
3755d29a45 | |||
c2b75399ae | |||
a33ecd1338 | |||
a7e29a9f36 | |||
02399dfa5c | |||
aec2941bac | |||
9315eb5289 | |||
5b2b2ea7b0 | |||
d90b634e80 | |||
6dd8cda074 | |||
701be31554 | |||
25eaf730bc | |||
4edb7447df | |||
5f3de60962 | |||
79c17aba49 | |||
80a90496d9 | |||
bbca803840 | |||
160d3869a9 | |||
afd8f64da8 | |||
6d2548b823 | |||
642356d353 | |||
ba0fa36c2a | |||
12f6cd878d | |||
9aacebbbaf | |||
701c3745c2 | |||
a41d08343c | |||
156288b17b | |||
6467f98241 | |||
b24e4334f6 | |||
bf8abcbf4a | |||
bb484414b1 | |||
fc75b13fae | |||
78f59b4207 | |||
7c5567db56 | |||
ed0e23e8a5 | |||
7ecaf1f982 | |||
d0a41f3894 | |||
57562b234f | |||
469d127d61 | |||
d6e9b61c8e | |||
7fb1b65ddd | |||
77c7fdc636 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
build
|
||||
*.pyc
|
||||
|
47
.travis.yml
47
.travis.yml
@ -1,17 +1,36 @@
|
||||
language: cpp
|
||||
dist: xenial
|
||||
|
||||
compiler:
|
||||
- gcc
|
||||
- clang
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
language: bash
|
||||
|
||||
matrix:
|
||||
exclude:
|
||||
# GCC fails on recent Travis OSX images.
|
||||
- compiler: gcc
|
||||
os: osx
|
||||
include:
|
||||
# macOS
|
||||
- os: osx
|
||||
compiler: clang
|
||||
script:
|
||||
- python test/run.py
|
||||
- make ws
|
||||
|
||||
script: python test/run.py
|
||||
# Linux
|
||||
- os: linux
|
||||
dist: xenial
|
||||
script:
|
||||
- python test/run.py
|
||||
- make ws
|
||||
env:
|
||||
- CC=gcc
|
||||
- CXX=g++
|
||||
|
||||
# Clang + Linux disabled for now
|
||||
# - os: linux
|
||||
# dist: xenial
|
||||
# script: python test/run.py
|
||||
# env:
|
||||
# - CC=clang
|
||||
# - CXX=clang++
|
||||
|
||||
# Windows
|
||||
- os: windows
|
||||
env:
|
||||
- CMAKE_PATH="/c/Program Files/CMake/bin"
|
||||
script:
|
||||
- export PATH=$CMAKE_PATH:$PATH
|
||||
- python test/run.py
|
||||
|
@ -41,6 +41,8 @@ set( IXWEBSOCKET_SOURCES
|
||||
ixwebsocket/IXSelectInterrupt.cpp
|
||||
ixwebsocket/IXSelectInterruptFactory.cpp
|
||||
ixwebsocket/IXConnectionState.cpp
|
||||
ixwebsocket/IXWebSocketCloseConstants.cpp
|
||||
ixwebsocket/IXWebSocketMessageQueue.cpp
|
||||
)
|
||||
|
||||
set( IXWEBSOCKET_HEADERS
|
||||
@ -70,6 +72,8 @@ set( IXWEBSOCKET_HEADERS
|
||||
ixwebsocket/IXSelectInterrupt.h
|
||||
ixwebsocket/IXSelectInterruptFactory.h
|
||||
ixwebsocket/IXConnectionState.h
|
||||
ixwebsocket/IXWebSocketCloseConstants.h
|
||||
ixwebsocket/IXWebSocketMessageQueue.h
|
||||
)
|
||||
|
||||
if (UNIX)
|
||||
@ -155,6 +159,6 @@ install(TARGETS ixwebsocket
|
||||
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_PREFIX}/include/ixwebsocket/
|
||||
)
|
||||
|
||||
if (NOT WIN32)
|
||||
if (USE_WS)
|
||||
add_subdirectory(ws)
|
||||
endif()
|
||||
|
@ -1 +1 @@
|
||||
1.5.3
|
||||
2.0.0
|
||||
|
@ -1 +1 @@
|
||||
docker/Dockerfile.fedora
|
||||
docker/Dockerfile.ubuntu_xenial
|
@ -16,6 +16,7 @@ ENV PATH="${CMAKE_BIN_PATH}:${PATH}"
|
||||
|
||||
RUN yum install -y python
|
||||
RUN yum install -y libtsan
|
||||
RUN yum install -y zlib-devel
|
||||
|
||||
COPY . .
|
||||
# RUN ["make", "test"]
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
namespace ix
|
||||
{
|
||||
enum class HttpErrorCode
|
||||
enum class HttpErrorCode : int
|
||||
{
|
||||
Ok = 0,
|
||||
CannotConnect = 1,
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
namespace ix
|
||||
{
|
||||
class SelectInterruptEventFd : public SelectInterrupt {
|
||||
class SelectInterruptEventFd final : public SelectInterrupt {
|
||||
public:
|
||||
SelectInterruptEventFd();
|
||||
virtual ~SelectInterruptEventFd();
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
namespace ix
|
||||
{
|
||||
class SelectInterruptPipe : public SelectInterrupt {
|
||||
class SelectInterruptPipe final : public SelectInterrupt {
|
||||
public:
|
||||
SelectInterruptPipe();
|
||||
virtual ~SelectInterruptPipe();
|
||||
|
@ -73,7 +73,7 @@ namespace ix
|
||||
|
||||
struct timeval timeout;
|
||||
timeout.tv_sec = timeoutMs / 1000;
|
||||
timeout.tv_usec = (timeoutMs < 1000) ? 0 : 1000 * (timeoutMs % 1000);
|
||||
timeout.tv_usec = 1000 * (timeoutMs % 1000);
|
||||
|
||||
// Compute the highest fd.
|
||||
int sockfd = _sockfd;
|
||||
@ -129,7 +129,7 @@ namespace ix
|
||||
}
|
||||
|
||||
// Wake up from poll/select by writing to the pipe which is watched by select
|
||||
bool Socket::wakeUpFromPoll(uint8_t wakeUpCode)
|
||||
bool Socket::wakeUpFromPoll(uint64_t wakeUpCode)
|
||||
{
|
||||
return _selectInterrupt->notify(wakeUpCode);
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ namespace ix
|
||||
|
||||
// Functions to check whether there is activity on the socket
|
||||
PollResultType poll(int timeoutMs = kDefaultPollTimeout);
|
||||
bool wakeUpFromPoll(uint8_t wakeUpCode);
|
||||
bool wakeUpFromPoll(uint64_t wakeUpCode);
|
||||
|
||||
PollResultType isReadyToWrite(int timeoutMs);
|
||||
PollResultType isReadyToRead(int timeoutMs);
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
namespace ix
|
||||
{
|
||||
class SocketAppleSSL : public Socket
|
||||
class SocketAppleSSL final : public Socket
|
||||
{
|
||||
public:
|
||||
SocketAppleSSL(int fd = -1);
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
namespace ix
|
||||
{
|
||||
class SocketOpenSSL : public Socket
|
||||
class SocketOpenSSL final : public Socket
|
||||
{
|
||||
public:
|
||||
SocketOpenSSL(int fd = -1);
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
namespace ix
|
||||
{
|
||||
class SocketSChannel : public Socket
|
||||
class SocketSChannel final : public Socket
|
||||
{
|
||||
public:
|
||||
SocketSChannel();
|
||||
|
@ -30,7 +30,9 @@ namespace ix
|
||||
_host(host),
|
||||
_backlog(backlog),
|
||||
_maxConnections(maxConnections),
|
||||
_serverFd(-1),
|
||||
_stop(false),
|
||||
_stopGc(false),
|
||||
_connectionStateFactory(&ConnectionState::createConnectionState)
|
||||
{
|
||||
|
||||
@ -124,9 +126,15 @@ namespace ix
|
||||
|
||||
void SocketServer::start()
|
||||
{
|
||||
if (_thread.joinable()) return; // we've already been started
|
||||
if (!_thread.joinable())
|
||||
{
|
||||
_thread = std::thread(&SocketServer::run, this);
|
||||
}
|
||||
|
||||
_thread = std::thread(&SocketServer::run, this);
|
||||
if (!_gcThread.joinable())
|
||||
{
|
||||
_gcThread = std::thread(&SocketServer::runGC, this);
|
||||
}
|
||||
}
|
||||
|
||||
void SocketServer::wait()
|
||||
@ -142,21 +150,21 @@ namespace ix
|
||||
|
||||
void SocketServer::stop()
|
||||
{
|
||||
while (true)
|
||||
// Stop accepting connections, and close the 'accept' thread
|
||||
if (_thread.joinable())
|
||||
{
|
||||
if (closeTerminatedThreads()) break;
|
||||
|
||||
// wait 10ms and try again later.
|
||||
// we could have a timeout, but if we exit of here
|
||||
// we leaked threads, it is quite bad.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
_stop = true;
|
||||
_thread.join();
|
||||
_stop = false;
|
||||
}
|
||||
|
||||
if (!_thread.joinable()) return; // nothing to do
|
||||
|
||||
_stop = true;
|
||||
_thread.join();
|
||||
_stop = false;
|
||||
// Join all threads and make sure that all connections are terminated
|
||||
if (_gcThread.joinable())
|
||||
{
|
||||
_stopGc = true;
|
||||
_gcThread.join();
|
||||
_stopGc = false;
|
||||
}
|
||||
|
||||
_conditionVariable.notify_one();
|
||||
Socket::closeSocket(_serverFd);
|
||||
@ -175,7 +183,7 @@ namespace ix
|
||||
// field becomes true, and we can use that to know that we can join that thread
|
||||
// and remove it from our _connectionsThreads data structure (a list).
|
||||
//
|
||||
bool SocketServer::closeTerminatedThreads()
|
||||
void SocketServer::closeTerminatedThreads()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_connectionsThreadsMutex);
|
||||
auto it = _connectionsThreads.begin();
|
||||
@ -195,8 +203,6 @@ namespace ix
|
||||
if (thread.joinable()) thread.join();
|
||||
it = _connectionsThreads.erase(it);
|
||||
}
|
||||
|
||||
return _connectionsThreads.empty();
|
||||
}
|
||||
|
||||
void SocketServer::run()
|
||||
@ -208,12 +214,6 @@ namespace ix
|
||||
{
|
||||
if (_stop) return;
|
||||
|
||||
// Garbage collection to shutdown/join threads for closed connections.
|
||||
// We could run this in its own thread, so that we dont need to accept
|
||||
// a new connection to close a thread.
|
||||
// We could also use a condition variable to be notify when we need to do this
|
||||
closeTerminatedThreads();
|
||||
|
||||
// Use select to check whether a new connection is in progress
|
||||
fd_set rfds;
|
||||
struct timeval timeout;
|
||||
@ -290,5 +290,30 @@ namespace ix
|
||||
connectionState)));
|
||||
}
|
||||
}
|
||||
|
||||
size_t SocketServer::getConnectionsThreadsCount()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_connectionsThreadsMutex);
|
||||
return _connectionsThreads.size();
|
||||
}
|
||||
|
||||
void SocketServer::runGC()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
// Garbage collection to shutdown/join threads for closed connections.
|
||||
closeTerminatedThreads();
|
||||
|
||||
// We quit this thread if all connections are closed and we received
|
||||
// a stop request by setting _stopGc to true.
|
||||
if (_stopGc && getConnectionsThreadsCount() == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Sleep a little bit then keep cleaning up
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,12 @@ namespace ix
|
||||
// background thread to wait for incoming connections
|
||||
std::atomic<bool> _stop;
|
||||
std::thread _thread;
|
||||
void run();
|
||||
|
||||
// background thread to cleanup (join) terminated threads
|
||||
std::atomic<bool> _stopGc;
|
||||
std::thread _gcThread;
|
||||
void runGC();
|
||||
|
||||
// the list of (connectionState, threads) for each connections
|
||||
ConnectionThreads _connectionsThreads;
|
||||
@ -87,13 +93,12 @@ namespace ix
|
||||
// the factory to create ConnectionState objects
|
||||
ConnectionStateFactory _connectionStateFactory;
|
||||
|
||||
// Methods
|
||||
void run();
|
||||
virtual void handleConnection(int fd,
|
||||
std::shared_ptr<ConnectionState> connectionState) = 0;
|
||||
virtual size_t getConnectedClientsCount() = 0;
|
||||
|
||||
// Returns true if all connection threads are joined
|
||||
bool closeTerminatedThreads();
|
||||
void closeTerminatedThreads();
|
||||
size_t getConnectionsThreadsCount();
|
||||
};
|
||||
}
|
||||
|
@ -142,9 +142,10 @@ namespace ix
|
||||
_thread = std::thread(&WebSocket::run, this);
|
||||
}
|
||||
|
||||
void WebSocket::stop()
|
||||
void WebSocket::stop(uint16_t code,
|
||||
const std::string& reason)
|
||||
{
|
||||
close();
|
||||
close(code, reason);
|
||||
|
||||
if (_thread.joinable())
|
||||
{
|
||||
@ -212,9 +213,10 @@ namespace ix
|
||||
return getReadyState() == ReadyState::Closing;
|
||||
}
|
||||
|
||||
void WebSocket::close()
|
||||
void WebSocket::close(uint16_t code,
|
||||
const std::string& reason)
|
||||
{
|
||||
_ws.close();
|
||||
_ws.close(code, reason);
|
||||
}
|
||||
|
||||
void WebSocket::checkConnection(bool firstConnectionAttempt)
|
||||
@ -223,7 +225,6 @@ namespace ix
|
||||
|
||||
uint32_t retries = 0;
|
||||
millis duration;
|
||||
ix::WebSocketInitResult status;
|
||||
|
||||
// Try to connect perpertually
|
||||
while (true)
|
||||
@ -249,7 +250,7 @@ namespace ix
|
||||
}
|
||||
|
||||
// Try to connect synchronously
|
||||
status = connect(_handshakeTimeoutSecs);
|
||||
ix::WebSocketInitResult status = connect(_handshakeTimeoutSecs);
|
||||
|
||||
if (!status.success)
|
||||
{
|
||||
@ -262,7 +263,7 @@ namespace ix
|
||||
connectErr.wait_time = duration.count();
|
||||
connectErr.retries = retries;
|
||||
}
|
||||
|
||||
|
||||
connectErr.reason = status.errorStr;
|
||||
connectErr.http_status = status.http_status;
|
||||
|
||||
@ -292,6 +293,9 @@ namespace ix
|
||||
break;
|
||||
}
|
||||
|
||||
// We can avoid to poll if we want to stop and are not closing
|
||||
if (_stop && !isClosing()) break;
|
||||
|
||||
// 2. Poll to see if there's any new data available
|
||||
WebSocketTransport::PollResult pollResult = _ws.poll();
|
||||
|
||||
@ -459,6 +463,11 @@ namespace ix
|
||||
_automaticReconnection = false;
|
||||
}
|
||||
|
||||
bool WebSocket::isAutomaticReconnectionEnabled() const
|
||||
{
|
||||
return _automaticReconnection;
|
||||
}
|
||||
|
||||
size_t WebSocket::bufferedAmount() const
|
||||
{
|
||||
return _ws.bufferedAmount();
|
||||
|
@ -19,6 +19,7 @@
|
||||
#include "IXWebSocketSendInfo.h"
|
||||
#include "IXWebSocketPerMessageDeflateOptions.h"
|
||||
#include "IXWebSocketHttpHeaders.h"
|
||||
#include "IXWebSocketCloseConstants.h"
|
||||
#include "IXProgressCallback.h"
|
||||
|
||||
namespace ix
|
||||
@ -99,8 +100,10 @@ namespace ix
|
||||
|
||||
// Run asynchronously, by calling start and stop.
|
||||
void start();
|
||||
|
||||
// stop is synchronous
|
||||
void stop();
|
||||
void stop(uint16_t code = WebSocketCloseConstants::kNormalClosureCode,
|
||||
const std::string& reason = WebSocketCloseConstants::kNormalClosureMessage);
|
||||
|
||||
// Run in blocking mode, by connecting first manually, and then calling run.
|
||||
WebSocketInitResult connect(int timeoutSecs);
|
||||
@ -112,7 +115,9 @@ namespace ix
|
||||
WebSocketSendInfo sendText(const std::string& text,
|
||||
const OnProgressCallback& onProgressCallback = nullptr);
|
||||
WebSocketSendInfo ping(const std::string& text);
|
||||
void close();
|
||||
|
||||
void close(uint16_t code = 1000,
|
||||
const std::string& reason = "Normal closure");
|
||||
|
||||
void setOnMessageCallback(const OnMessageCallback& callback);
|
||||
static void setTrafficTrackerCallback(const OnTrafficTrackerCallback& callback);
|
||||
@ -130,6 +135,7 @@ namespace ix
|
||||
|
||||
void enableAutomaticReconnection();
|
||||
void disableAutomaticReconnection();
|
||||
bool isAutomaticReconnectionEnabled() const;
|
||||
|
||||
private:
|
||||
|
||||
|
23
ixwebsocket/IXWebSocketCloseConstants.cpp
Normal file
23
ixwebsocket/IXWebSocketCloseConstants.cpp
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* IXWebSocketCloseConstants.cpp
|
||||
* Author: Benjamin Sergeant
|
||||
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
#include "IXWebSocketCloseConstants.h"
|
||||
|
||||
namespace ix
|
||||
{
|
||||
const uint16_t WebSocketCloseConstants::kNormalClosureCode(1000);
|
||||
const uint16_t WebSocketCloseConstants::kInternalErrorCode(1011);
|
||||
const uint16_t WebSocketCloseConstants::kAbnormalCloseCode(1006);
|
||||
const uint16_t WebSocketCloseConstants::kProtocolErrorCode(1002);
|
||||
const uint16_t WebSocketCloseConstants::kNoStatusCodeErrorCode(1005);
|
||||
|
||||
const std::string WebSocketCloseConstants::kNormalClosureMessage("Normal closure");
|
||||
const std::string WebSocketCloseConstants::kInternalErrorMessage("Internal error");
|
||||
const std::string WebSocketCloseConstants::kAbnormalCloseMessage("Abnormal closure");
|
||||
const std::string WebSocketCloseConstants::kPingTimeoutMessage("Ping timeout");
|
||||
const std::string WebSocketCloseConstants::kProtocolErrorMessage("Protocol error");
|
||||
const std::string WebSocketCloseConstants::kNoStatusCodeErrorMessage("No status code");
|
||||
}
|
29
ixwebsocket/IXWebSocketCloseConstants.h
Normal file
29
ixwebsocket/IXWebSocketCloseConstants.h
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* IXWebSocketCloseConstants.h
|
||||
* Author: Benjamin Sergeant
|
||||
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace ix
|
||||
{
|
||||
struct WebSocketCloseConstants
|
||||
{
|
||||
static const uint16_t kNormalClosureCode;
|
||||
static const uint16_t kInternalErrorCode;
|
||||
static const uint16_t kAbnormalCloseCode;
|
||||
static const uint16_t kProtocolErrorCode;
|
||||
static const uint16_t kNoStatusCodeErrorCode;
|
||||
|
||||
static const std::string kNormalClosureMessage;
|
||||
static const std::string kInternalErrorMessage;
|
||||
static const std::string kAbnormalCloseMessage;
|
||||
static const std::string kPingTimeoutMessage;
|
||||
static const std::string kProtocolErrorMessage;
|
||||
static const std::string kNoStatusCodeErrorMessage;
|
||||
};
|
||||
}
|
@ -242,7 +242,7 @@ namespace ix
|
||||
}
|
||||
|
||||
char output[29] = {};
|
||||
WebSocketHandshakeKeyGen::generate(secWebSocketKey.c_str(), output);
|
||||
WebSocketHandshakeKeyGen::generate(secWebSocketKey, output);
|
||||
if (std::string(output) != headers["sec-websocket-accept"])
|
||||
{
|
||||
std::string errorMsg("Invalid Sec-WebSocket-Accept value");
|
||||
@ -348,7 +348,7 @@ namespace ix
|
||||
}
|
||||
|
||||
char output[29] = {};
|
||||
WebSocketHandshakeKeyGen::generate(headers["sec-websocket-key"].c_str(), output);
|
||||
WebSocketHandshakeKeyGen::generate(headers["sec-websocket-key"], output);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "HTTP/1.1 101 Switching Protocols\r\n";
|
||||
|
121
ixwebsocket/IXWebSocketMessageQueue.cpp
Normal file
121
ixwebsocket/IXWebSocketMessageQueue.cpp
Normal file
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* IXWebSocketMessageQueue.cpp
|
||||
* Author: Korchynskyi Dmytro
|
||||
* Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
#include "IXWebSocketMessageQueue.h"
|
||||
|
||||
namespace ix
|
||||
{
|
||||
|
||||
WebSocketMessageQueue::WebSocketMessageQueue(WebSocket* websocket)
|
||||
{
|
||||
bindWebsocket(websocket);
|
||||
}
|
||||
|
||||
WebSocketMessageQueue::~WebSocketMessageQueue()
|
||||
{
|
||||
if (!_messages.empty())
|
||||
{
|
||||
// not handled all messages
|
||||
}
|
||||
|
||||
bindWebsocket(nullptr);
|
||||
}
|
||||
|
||||
void WebSocketMessageQueue::bindWebsocket(WebSocket * websocket)
|
||||
{
|
||||
if (_websocket == websocket) return;
|
||||
|
||||
// unbind old
|
||||
if (_websocket)
|
||||
{
|
||||
// set dummy callback just to avoid crash
|
||||
_websocket->setOnMessageCallback([](
|
||||
WebSocketMessageType,
|
||||
const std::string&,
|
||||
size_t,
|
||||
const WebSocketErrorInfo&,
|
||||
const WebSocketOpenInfo&,
|
||||
const WebSocketCloseInfo&)
|
||||
{});
|
||||
}
|
||||
|
||||
_websocket = websocket;
|
||||
|
||||
// bind new
|
||||
if (_websocket)
|
||||
{
|
||||
_websocket->setOnMessageCallback([this](
|
||||
WebSocketMessageType type,
|
||||
const std::string& str,
|
||||
size_t wireSize,
|
||||
const WebSocketErrorInfo& errorInfo,
|
||||
const WebSocketOpenInfo& openInfo,
|
||||
const WebSocketCloseInfo& closeInfo)
|
||||
{
|
||||
MessagePtr message(new Message());
|
||||
|
||||
message->type = type;
|
||||
message->str = str;
|
||||
message->wireSize = wireSize;
|
||||
message->errorInfo = errorInfo;
|
||||
message->openInfo = openInfo;
|
||||
message->closeInfo = closeInfo;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_messagesMutex);
|
||||
_messages.emplace_back(std::move(message));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketMessageQueue::setOnMessageCallback(const OnMessageCallback& callback)
|
||||
{
|
||||
_onMessageUserCallback = callback;
|
||||
}
|
||||
|
||||
void WebSocketMessageQueue::setOnMessageCallback(OnMessageCallback&& callback)
|
||||
{
|
||||
_onMessageUserCallback = std::move(callback);
|
||||
}
|
||||
|
||||
WebSocketMessageQueue::MessagePtr WebSocketMessageQueue::popMessage()
|
||||
{
|
||||
MessagePtr message;
|
||||
std::lock_guard<std::mutex> lock(_messagesMutex);
|
||||
|
||||
if (!_messages.empty())
|
||||
{
|
||||
message = std::move(_messages.front());
|
||||
_messages.pop_front();
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
void WebSocketMessageQueue::poll(int count)
|
||||
{
|
||||
if (!_onMessageUserCallback)
|
||||
return;
|
||||
|
||||
MessagePtr message;
|
||||
|
||||
while (count > 0 && (message = popMessage()))
|
||||
{
|
||||
_onMessageUserCallback(
|
||||
message->type,
|
||||
message->str,
|
||||
message->wireSize,
|
||||
message->errorInfo,
|
||||
message->openInfo,
|
||||
message->closeInfo
|
||||
);
|
||||
|
||||
--count;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
53
ixwebsocket/IXWebSocketMessageQueue.h
Normal file
53
ixwebsocket/IXWebSocketMessageQueue.h
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* IXWebSocketMessageQueue.h
|
||||
* Author: Korchynskyi Dmytro
|
||||
* Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IXWebSocket.h"
|
||||
#include <thread>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
|
||||
namespace ix
|
||||
{
|
||||
//
|
||||
// A helper class to dispatch websocket message callbacks in your thread.
|
||||
//
|
||||
class WebSocketMessageQueue
|
||||
{
|
||||
public:
|
||||
WebSocketMessageQueue(WebSocket* websocket = nullptr);
|
||||
~WebSocketMessageQueue();
|
||||
|
||||
void bindWebsocket(WebSocket* websocket);
|
||||
|
||||
void setOnMessageCallback(const OnMessageCallback& callback);
|
||||
void setOnMessageCallback(OnMessageCallback&& callback);
|
||||
|
||||
void poll(int count = 512);
|
||||
|
||||
protected:
|
||||
struct Message
|
||||
{
|
||||
WebSocketMessageType type;
|
||||
std::string str;
|
||||
size_t wireSize;
|
||||
WebSocketErrorInfo errorInfo;
|
||||
WebSocketOpenInfo openInfo;
|
||||
WebSocketCloseInfo closeInfo;
|
||||
};
|
||||
|
||||
using MessagePtr = std::shared_ptr<Message>;
|
||||
|
||||
MessagePtr popMessage();
|
||||
|
||||
private:
|
||||
WebSocket* _websocket = nullptr;
|
||||
OnMessageCallback _onMessageUserCallback;
|
||||
std::mutex _messagesMutex;
|
||||
std::list<MessagePtr> _messages;
|
||||
};
|
||||
}
|
@ -23,7 +23,7 @@ namespace ix
|
||||
using OnConnectionCallback = std::function<void(std::shared_ptr<WebSocket>,
|
||||
std::shared_ptr<ConnectionState>)>;
|
||||
|
||||
class WebSocketServer : public SocketServer {
|
||||
class WebSocketServer final : public SocketServer {
|
||||
public:
|
||||
WebSocketServer(int port = SocketServer::kDefaultPort,
|
||||
const std::string& host = SocketServer::kDefaultHost,
|
||||
|
@ -71,24 +71,14 @@ namespace ix
|
||||
const int WebSocketTransport::kDefaultPingIntervalSecs(-1);
|
||||
const int WebSocketTransport::kDefaultPingTimeoutSecs(-1);
|
||||
const bool WebSocketTransport::kDefaultEnablePong(true);
|
||||
const int WebSocketTransport::kClosingMaximumWaitingDelayInMs(200);
|
||||
const int WebSocketTransport::kClosingMaximumWaitingDelayInMs(300);
|
||||
constexpr size_t WebSocketTransport::kChunkSize;
|
||||
|
||||
const uint16_t WebSocketTransport::kInternalErrorCode(1011);
|
||||
const uint16_t WebSocketTransport::kAbnormalCloseCode(1006);
|
||||
const uint16_t WebSocketTransport::kProtocolErrorCode(1002);
|
||||
const uint16_t WebSocketTransport::kNoStatusCodeErrorCode(1005);
|
||||
const std::string WebSocketTransport::kInternalErrorMessage("Internal error");
|
||||
const std::string WebSocketTransport::kAbnormalCloseMessage("Abnormal closure");
|
||||
const std::string WebSocketTransport::kPingTimeoutMessage("Ping timeout");
|
||||
const std::string WebSocketTransport::kProtocolErrorMessage("Protocol error");
|
||||
const std::string WebSocketTransport::kNoStatusCodeErrorMessage("No status code");
|
||||
|
||||
WebSocketTransport::WebSocketTransport() :
|
||||
_useMask(true),
|
||||
_readyState(ReadyState::CLOSED),
|
||||
_closeCode(kInternalErrorCode),
|
||||
_closeReason(kInternalErrorMessage),
|
||||
_closeCode(WebSocketCloseConstants::kInternalErrorCode),
|
||||
_closeReason(WebSocketCloseConstants::kInternalErrorMessage),
|
||||
_closeWireSize(0),
|
||||
_closeRemote(false),
|
||||
_enablePerMessageDeflate(false),
|
||||
@ -134,17 +124,14 @@ namespace ix
|
||||
{
|
||||
_pingIntervalOrTimeoutGCDSecs = pingIntervalSecs;
|
||||
}
|
||||
|
||||
if (_pingIntervalOrTimeoutGCDSecs > 0)
|
||||
{
|
||||
_nextGCDTimePoint = std::chrono::steady_clock::now() + std::chrono::seconds(_pingIntervalOrTimeoutGCDSecs);
|
||||
}
|
||||
}
|
||||
|
||||
// Client
|
||||
WebSocketInitResult WebSocketTransport::connectToUrl(const std::string& url,
|
||||
int timeoutSecs)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_socketMutex);
|
||||
|
||||
std::string protocol, host, path, query;
|
||||
int port;
|
||||
|
||||
@ -154,8 +141,8 @@ namespace ix
|
||||
std::string("Could not parse URL ") + url);
|
||||
}
|
||||
|
||||
bool tls = protocol == "wss";
|
||||
std::string errorMsg;
|
||||
bool tls = protocol == "wss";
|
||||
_socket = createSocket(tls, errorMsg);
|
||||
|
||||
if (!_socket)
|
||||
@ -181,6 +168,8 @@ namespace ix
|
||||
// Server
|
||||
WebSocketInitResult WebSocketTransport::connectToSocket(int fd, int timeoutSecs)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_socketMutex);
|
||||
|
||||
// Server should not mask the data it sends to the client
|
||||
_useMask = false;
|
||||
|
||||
@ -220,11 +209,15 @@ namespace ix
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_closeDataMutex);
|
||||
_onCloseCallback(_closeCode, _closeReason, _closeWireSize, _closeRemote);
|
||||
_closeCode = kInternalErrorCode;
|
||||
_closeReason = kInternalErrorMessage;
|
||||
_closeCode = WebSocketCloseConstants::kInternalErrorCode;
|
||||
_closeReason = WebSocketCloseConstants::kInternalErrorMessage;
|
||||
_closeWireSize = 0;
|
||||
_closeRemote = false;
|
||||
}
|
||||
else if (readyState == ReadyState::OPEN)
|
||||
{
|
||||
initTimePointsAndGCDAfterConnect();
|
||||
}
|
||||
|
||||
_readyState = readyState;
|
||||
}
|
||||
@ -234,6 +227,23 @@ namespace ix
|
||||
_onCloseCallback = onCloseCallback;
|
||||
}
|
||||
|
||||
void WebSocketTransport::initTimePointsAndGCDAfterConnect()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_lastSendPingTimePointMutex);
|
||||
_lastSendPingTimePoint = std::chrono::steady_clock::now();
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_lastReceivePongTimePointMutex);
|
||||
_lastReceivePongTimePoint = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
if (_pingIntervalOrTimeoutGCDSecs > 0)
|
||||
{
|
||||
_nextGCDTimePoint = std::chrono::steady_clock::now() + std::chrono::seconds(_pingIntervalOrTimeoutGCDSecs);
|
||||
}
|
||||
}
|
||||
|
||||
// Only consider send PING time points for that computation.
|
||||
bool WebSocketTransport::pingIntervalExceeded()
|
||||
{
|
||||
@ -270,7 +280,8 @@ namespace ix
|
||||
// ping response (PONG) exceeds the maximum delay, then close the connection
|
||||
if (pingTimeoutExceeded())
|
||||
{
|
||||
close(kInternalErrorCode, kPingTimeoutMessage);
|
||||
close(WebSocketCloseConstants::kInternalErrorCode,
|
||||
WebSocketCloseConstants::kPingTimeoutMessage);
|
||||
}
|
||||
// If ping is enabled and no ping has been sent for a duration
|
||||
// exceeding our ping interval, send a ping to the server.
|
||||
@ -281,7 +292,7 @@ namespace ix
|
||||
sendPing(ss.str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// No timeout if state is not OPEN, otherwise computed
|
||||
// pingIntervalOrTimeoutGCD (equals to -1 if no ping and no ping timeout are set)
|
||||
int lastingTimeoutDelayInMs = (_readyState != ReadyState::OPEN) ? 0 : _pingIntervalOrTimeoutGCDSecs;
|
||||
@ -294,15 +305,30 @@ namespace ix
|
||||
if (now >= _nextGCDTimePoint)
|
||||
{
|
||||
_nextGCDTimePoint = now + std::chrono::seconds(_pingIntervalOrTimeoutGCDSecs);
|
||||
|
||||
|
||||
lastingTimeoutDelayInMs = _pingIntervalOrTimeoutGCDSecs * 1000;
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
lastingTimeoutDelayInMs = (int)std::chrono::duration_cast<std::chrono::milliseconds>(_nextGCDTimePoint - now).count();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Windows does not have select interrupt capabilities, so wait with a small timeout
|
||||
if (lastingTimeoutDelayInMs <= 0)
|
||||
{
|
||||
lastingTimeoutDelayInMs = 20;
|
||||
}
|
||||
#endif
|
||||
|
||||
// If we are requesting a cancellation, pass in a positive and small timeout
|
||||
// to never poll forever without a timeout.
|
||||
if (_requestInitCancellation)
|
||||
{
|
||||
lastingTimeoutDelayInMs = 100;
|
||||
}
|
||||
|
||||
// poll the socket
|
||||
PollResultType pollResult = _socket->poll(lastingTimeoutDelayInMs);
|
||||
|
||||
@ -318,7 +344,7 @@ namespace ix
|
||||
|
||||
if (result == PollResultType::Error)
|
||||
{
|
||||
_socket->close();
|
||||
closeSocket();
|
||||
setReadyState(ReadyState::CLOSED);
|
||||
break;
|
||||
}
|
||||
@ -342,8 +368,8 @@ namespace ix
|
||||
{
|
||||
// if there are received data pending to be processed, then delay the abnormal closure
|
||||
// to after dispatch (other close code/reason could be read from the buffer)
|
||||
|
||||
_socket->close();
|
||||
|
||||
closeSocket();
|
||||
|
||||
return PollResult::AbnormalClose;
|
||||
}
|
||||
@ -357,18 +383,18 @@ namespace ix
|
||||
}
|
||||
else if (pollResult == PollResultType::Error)
|
||||
{
|
||||
_socket->close();
|
||||
closeSocket();
|
||||
}
|
||||
else if (pollResult == PollResultType::CloseRequest)
|
||||
{
|
||||
_socket->close();
|
||||
closeSocket();
|
||||
}
|
||||
|
||||
if (_readyState == ReadyState::CLOSING && closingDelayExceeded())
|
||||
{
|
||||
_rxbuf.clear();
|
||||
// close code and reason were set when calling close()
|
||||
_socket->close();
|
||||
closeSocket();
|
||||
setReadyState(ReadyState::CLOSED);
|
||||
}
|
||||
|
||||
@ -600,8 +626,8 @@ namespace ix
|
||||
else
|
||||
{
|
||||
// no close code received
|
||||
code = kNoStatusCodeErrorCode;
|
||||
reason = kNoStatusCodeErrorMessage;
|
||||
code = WebSocketCloseConstants::kNoStatusCodeErrorCode;
|
||||
reason = WebSocketCloseConstants::kNoStatusCodeErrorMessage;
|
||||
}
|
||||
|
||||
// We receive a CLOSE frame from remote and are NOT the ones who triggered the close
|
||||
@ -635,8 +661,9 @@ namespace ix
|
||||
else
|
||||
{
|
||||
// Unexpected frame type
|
||||
|
||||
close(kProtocolErrorCode, kProtocolErrorMessage, _rxbuf.size());
|
||||
close(WebSocketCloseConstants::kProtocolErrorCode,
|
||||
WebSocketCloseConstants::kProtocolErrorMessage,
|
||||
_rxbuf.size());
|
||||
}
|
||||
|
||||
// Erase the message that has been processed from the input/read buffer
|
||||
@ -653,13 +680,15 @@ namespace ix
|
||||
// if we previously closed the connection (CLOSING state), then set state to CLOSED (code/reason were set before)
|
||||
if (_readyState == ReadyState::CLOSING)
|
||||
{
|
||||
_socket->close();
|
||||
closeSocket();
|
||||
setReadyState(ReadyState::CLOSED);
|
||||
}
|
||||
// if we weren't closing, then close using abnormal close code and message
|
||||
// if we weren't closing, then close using abnormal close code and message
|
||||
else if (_readyState != ReadyState::CLOSED)
|
||||
{
|
||||
closeSocketAndSwitchToClosedState(kAbnormalCloseCode, kAbnormalCloseMessage, 0, false);
|
||||
closeSocketAndSwitchToClosedState(WebSocketCloseConstants::kAbnormalCloseCode,
|
||||
WebSocketCloseConstants::kAbnormalCloseMessage,
|
||||
0, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -719,7 +748,7 @@ namespace ix
|
||||
bool compress,
|
||||
const OnProgressCallback& onProgressCallback)
|
||||
{
|
||||
if (_readyState != ReadyState::OPEN)
|
||||
if (_readyState != ReadyState::OPEN && _readyState != ReadyState::CLOSING)
|
||||
{
|
||||
return WebSocketSendInfo();
|
||||
}
|
||||
@ -929,13 +958,19 @@ namespace ix
|
||||
_enablePerMessageDeflate, onProgressCallback);
|
||||
}
|
||||
|
||||
ssize_t WebSocketTransport::send()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_socketMutex);
|
||||
return _socket->send((char*)&_txbuf[0], _txbuf.size());
|
||||
}
|
||||
|
||||
void WebSocketTransport::sendOnSocket()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_txbufMutex);
|
||||
|
||||
while (_txbuf.size())
|
||||
{
|
||||
ssize_t ret = _socket->send((char*)&_txbuf[0], _txbuf.size());
|
||||
ssize_t ret = send();
|
||||
|
||||
if (ret < 0 && Socket::isWaitNeeded())
|
||||
{
|
||||
@ -943,8 +978,7 @@ namespace ix
|
||||
}
|
||||
else if (ret <= 0)
|
||||
{
|
||||
_socket->close();
|
||||
|
||||
closeSocket();
|
||||
setReadyState(ReadyState::CLOSED);
|
||||
break;
|
||||
}
|
||||
@ -960,7 +994,7 @@ namespace ix
|
||||
bool compress = false;
|
||||
|
||||
// if a status is set/was read
|
||||
if (code != kNoStatusCodeErrorCode)
|
||||
if (code != WebSocketCloseConstants::kNoStatusCodeErrorCode)
|
||||
{
|
||||
// See list of close events here:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
|
||||
@ -978,9 +1012,17 @@ namespace ix
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketTransport::closeSocketAndSwitchToClosedState(uint16_t code, const std::string& reason, size_t closeWireSize, bool remote)
|
||||
void WebSocketTransport::closeSocket()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_socketMutex);
|
||||
_socket->close();
|
||||
}
|
||||
|
||||
void WebSocketTransport::closeSocketAndSwitchToClosedState(
|
||||
uint16_t code, const std::string& reason, size_t closeWireSize, bool remote)
|
||||
{
|
||||
closeSocket();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_closeDataMutex);
|
||||
_closeCode = code;
|
||||
@ -988,16 +1030,18 @@ namespace ix
|
||||
_closeWireSize = closeWireSize;
|
||||
_closeRemote = remote;
|
||||
}
|
||||
|
||||
setReadyState(ReadyState::CLOSED);
|
||||
_requestInitCancellation = false;
|
||||
}
|
||||
|
||||
void WebSocketTransport::close(uint16_t code, const std::string& reason, size_t closeWireSize, bool remote)
|
||||
void WebSocketTransport::close(
|
||||
uint16_t code, const std::string& reason, size_t closeWireSize, bool remote)
|
||||
{
|
||||
_requestInitCancellation = true;
|
||||
|
||||
if (_readyState == ReadyState::CLOSING || _readyState == ReadyState::CLOSED) return;
|
||||
|
||||
sendCloseFrame(code, reason);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_closeDataMutex);
|
||||
_closeCode = code;
|
||||
@ -1011,6 +1055,7 @@ namespace ix
|
||||
}
|
||||
setReadyState(ReadyState::CLOSING);
|
||||
|
||||
sendCloseFrame(code, reason);
|
||||
// wake up the poll, but do not close yet
|
||||
_socket->wakeUpFromPoll(Socket::kSendRequest);
|
||||
}
|
||||
|
@ -25,6 +25,7 @@
|
||||
#include "IXCancellationRequest.h"
|
||||
#include "IXWebSocketHandshake.h"
|
||||
#include "IXProgressCallback.h"
|
||||
#include "IXWebSocketCloseConstants.h"
|
||||
|
||||
namespace ix
|
||||
{
|
||||
@ -91,11 +92,14 @@ namespace ix
|
||||
const OnProgressCallback& onProgressCallback);
|
||||
WebSocketSendInfo sendPing(const std::string& message);
|
||||
|
||||
void close(uint16_t code = 1000,
|
||||
const std::string& reason = "Normal closure",
|
||||
void close(uint16_t code = WebSocketCloseConstants::kNormalClosureCode,
|
||||
const std::string& reason = WebSocketCloseConstants::kNormalClosureMessage,
|
||||
size_t closeWireSize = 0,
|
||||
bool remote = false);
|
||||
|
||||
void closeSocket();
|
||||
ssize_t send();
|
||||
|
||||
ReadyState getReadyState() const;
|
||||
void setReadyState(ReadyState readyState);
|
||||
void setOnCloseCallback(const OnCloseCallback& onCloseCallback);
|
||||
@ -151,6 +155,7 @@ namespace ix
|
||||
|
||||
// Underlying TCP socket
|
||||
std::shared_ptr<Socket> _socket;
|
||||
std::mutex _socketMutex;
|
||||
|
||||
// Hold the state of the connection (OPEN, CLOSED, etc...)
|
||||
std::atomic<ReadyState> _readyState;
|
||||
@ -169,22 +174,11 @@ namespace ix
|
||||
|
||||
// Used to cancel dns lookup + socket connect + http upgrade
|
||||
std::atomic<bool> _requestInitCancellation;
|
||||
|
||||
|
||||
mutable std::mutex _closingTimePointMutex;
|
||||
std::chrono::time_point<std::chrono::steady_clock>_closingTimePoint;
|
||||
static const int kClosingMaximumWaitingDelayInMs;
|
||||
|
||||
// Constants for dealing with closing conneections
|
||||
static const uint16_t kInternalErrorCode;
|
||||
static const uint16_t kAbnormalCloseCode;
|
||||
static const uint16_t kProtocolErrorCode;
|
||||
static const uint16_t kNoStatusCodeErrorCode;
|
||||
static const std::string kInternalErrorMessage;
|
||||
static const std::string kAbnormalCloseMessage;
|
||||
static const std::string kPingTimeoutMessage;
|
||||
static const std::string kProtocolErrorMessage;
|
||||
static const std::string kNoStatusCodeErrorMessage;
|
||||
|
||||
// enable auto response to ping
|
||||
std::atomic<bool> _enablePong;
|
||||
static const bool kDefaultEnablePong;
|
||||
@ -220,6 +214,8 @@ namespace ix
|
||||
// after calling close(), if no CLOSE frame answer is received back from the remote, we should close the connexion
|
||||
bool closingDelayExceeded();
|
||||
|
||||
void initTimePointsAndGCDAfterConnect();
|
||||
|
||||
void sendCloseFrame(uint16_t code, const std::string& reason);
|
||||
|
||||
void closeSocketAndSwitchToClosedState(uint16_t code,
|
||||
|
@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Lightweight URL & URI parser (RFC 1738, RFC 3986)
|
||||
* https://github.com/corporateshark/LUrlParser
|
||||
*
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
*
|
||||
* Copyright (C) 2015 Sergey Kosarevsky (sk@linderdaum.com)
|
||||
*
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
|
@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Lightweight URL & URI parser (RFC 1738, RFC 3986)
|
||||
* https://github.com/corporateshark/LUrlParser
|
||||
*
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
*
|
||||
* Copyright (C) 2015 Sergey Kosarevsky (sk@linderdaum.com)
|
||||
*
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
|
@ -20,6 +20,8 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string.h>
|
||||
|
||||
class WebSocketHandshakeKeyGen {
|
||||
template <int N, typename T>
|
||||
@ -100,7 +102,12 @@ class WebSocketHandshakeKeyGen {
|
||||
}
|
||||
|
||||
public:
|
||||
static inline void generate(const char input[24], char output[28]) {
|
||||
static inline void generate(const std::string& inputStr, char output[28]) {
|
||||
|
||||
char input[25] = {};
|
||||
strncpy(input, inputStr.c_str(), 25 - 1);
|
||||
input[25 - 1] = '\0';
|
||||
|
||||
uint32_t b_output[5] = {
|
||||
0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0
|
||||
};
|
||||
|
10
makefile
10
makefile
@ -9,7 +9,10 @@ install: brew
|
||||
# on osx it is good practice to make /usr/local user writable
|
||||
# sudo chown -R `whoami`/staff /usr/local
|
||||
brew:
|
||||
mkdir -p build && (cd build ; cmake -DUSE_TLS=1 .. ; make -j install)
|
||||
mkdir -p build && (cd build ; cmake -DUSE_TLS=1 -DUSE_WS=1 .. ; make -j install)
|
||||
|
||||
ws:
|
||||
mkdir -p build && (cd build ; cmake -DUSE_TLS=1 -DUSE_WS=1 .. ; make -j)
|
||||
|
||||
uninstall:
|
||||
xargs rm -fv < build/install_manifest.txt
|
||||
@ -48,8 +51,8 @@ test_server:
|
||||
test:
|
||||
python2.7 test/run.py
|
||||
|
||||
ws_test: all
|
||||
(cd ws ; bash test_ws.sh)
|
||||
ws_test: ws
|
||||
(cd ws ; env DEBUG=1 PATH=../ws/build:$$PATH bash test_ws.sh)
|
||||
|
||||
# For the fork that is configured with appveyor
|
||||
rebase_upstream:
|
||||
@ -64,3 +67,4 @@ install_cmake_for_linux:
|
||||
|
||||
.PHONY: test
|
||||
.PHONY: build
|
||||
.PHONY: ws
|
||||
|
7
test/.gitignore
vendored
7
test/.gitignore
vendored
@ -1,9 +1,10 @@
|
||||
CMakeCache.txt
|
||||
package-lock.json
|
||||
CMakeFiles
|
||||
ixwebsocket_unittest
|
||||
cmake_install.cmake
|
||||
CMakeFiles
|
||||
ixwebsocket_unittest
|
||||
cmake_install.cmake
|
||||
node_modules
|
||||
ixwebsocket
|
||||
Makefile
|
||||
build
|
||||
ixwebsocket_unittest.xml
|
||||
|
@ -5,12 +5,11 @@
|
||||
cmake_minimum_required (VERSION 3.4.1)
|
||||
project (ixwebsocket_unittest)
|
||||
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/../third_party/sanitizers-cmake/cmake" ${CMAKE_MODULE_PATH})
|
||||
find_package(Sanitizers)
|
||||
|
||||
set (CMAKE_CXX_STANDARD 14)
|
||||
|
||||
if (NOT WIN32)
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/../third_party/sanitizers-cmake/cmake" ${CMAKE_MODULE_PATH})
|
||||
find_package(Sanitizers)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread")
|
||||
set(CMAKE_LD_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread")
|
||||
option(USE_TLS "Add TLS support" ON)
|
||||
@ -32,25 +31,39 @@ set (SOURCES
|
||||
../third_party/msgpack11/msgpack11.cpp
|
||||
../ws/ixcore/utils/IXCoreLogger.cpp
|
||||
|
||||
IXDNSLookupTest.cpp
|
||||
IXSocketTest.cpp
|
||||
IXSocketConnectTest.cpp
|
||||
IXWebSocketServerTest.cpp
|
||||
IXWebSocketPingTest.cpp
|
||||
IXWebSocketTestConnectionDisconnection.cpp
|
||||
IXUrlParserTest.cpp
|
||||
IXWebSocketServerTest.cpp
|
||||
)
|
||||
|
||||
# Some unittest don't work on windows yet
|
||||
if (NOT WIN32)
|
||||
if (UNIX)
|
||||
list(APPEND SOURCES
|
||||
IXWebSocketPingTimeoutTest.cpp
|
||||
IXDNSLookupTest.cpp
|
||||
cmd_websocket_chat.cpp
|
||||
IXWebSocketCloseTest.cpp
|
||||
IXWebSocketPingTest.cpp
|
||||
IXWebSocketPingTimeoutTest.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
# Some unittest fail for dubious reason on Ubuntu Xenial with TSAN
|
||||
if (MAC OR WIN32)
|
||||
list(APPEND SOURCES
|
||||
IXWebSocketMessageQTest.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
# Disable tests for now that are failing or not reliable
|
||||
|
||||
add_executable(ixwebsocket_unittest ${SOURCES})
|
||||
add_sanitizers(ixwebsocket_unittest)
|
||||
|
||||
if (NOT WIN32)
|
||||
add_sanitizers(ixwebsocket_unittest)
|
||||
endif()
|
||||
|
||||
if (APPLE AND USE_TLS)
|
||||
target_link_libraries(ixwebsocket_unittest "-framework foundation" "-framework security")
|
||||
|
@ -17,8 +17,12 @@ TEST_CASE("socket_connect", "[net]")
|
||||
{
|
||||
SECTION("Test connecting to a known hostname")
|
||||
{
|
||||
int port = getFreePort();
|
||||
ix::WebSocketServer server(port);
|
||||
REQUIRE(startWebSocketEchoServer(server));
|
||||
|
||||
std::string errMsg;
|
||||
int fd = SocketConnect::connect("www.google.com", 80, errMsg, [] { return false; });
|
||||
int fd = SocketConnect::connect("127.0.0.1", port, errMsg, [] { return false; });
|
||||
std::cerr << "Error message: " << errMsg << std::endl;
|
||||
REQUIRE(fd != -1);
|
||||
}
|
||||
@ -34,9 +38,13 @@ TEST_CASE("socket_connect", "[net]")
|
||||
|
||||
SECTION("Test connecting to a good hostname, with cancellation")
|
||||
{
|
||||
int port = getFreePort();
|
||||
ix::WebSocketServer server(port);
|
||||
REQUIRE(startWebSocketEchoServer(server));
|
||||
|
||||
std::string errMsg;
|
||||
// The callback returning true means we are requesting cancellation
|
||||
int fd = SocketConnect::connect("www.google.com", 80, errMsg, [] { return true; });
|
||||
int fd = SocketConnect::connect("127.0.0.1", port, errMsg, [] { return true; });
|
||||
std::cerr << "Error message: " << errMsg << std::endl;
|
||||
REQUIRE(fd == -1);
|
||||
}
|
||||
|
@ -53,13 +53,17 @@ namespace ix
|
||||
|
||||
TEST_CASE("socket", "[socket]")
|
||||
{
|
||||
SECTION("Connect to google HTTP server. Send GET request without header. Should return 200")
|
||||
SECTION("Connect to a local websocket server over a free port. Send GET request without header. Should return 400")
|
||||
{
|
||||
// Start a server first which we'll hit with our socket code
|
||||
int port = getFreePort();
|
||||
ix::WebSocketServer server(port);
|
||||
REQUIRE(startWebSocketEchoServer(server));
|
||||
|
||||
std::string errMsg;
|
||||
bool tls = false;
|
||||
std::shared_ptr<Socket> socket = createSocket(tls, errMsg);
|
||||
std::string host("www.google.com");
|
||||
int port = 80;
|
||||
std::string host("127.0.0.1");
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "GET / HTTP/1.1\r\n";
|
||||
@ -67,14 +71,14 @@ TEST_CASE("socket", "[socket]")
|
||||
ss << "\r\n";
|
||||
std::string request(ss.str());
|
||||
|
||||
int expectedStatus = 200;
|
||||
int expectedStatus = 400;
|
||||
int timeoutSecs = 3;
|
||||
|
||||
testSocket(host, port, request, socket, expectedStatus, timeoutSecs);
|
||||
}
|
||||
|
||||
#if defined(__APPLE__) || defined(__linux__)
|
||||
SECTION("Connect to google HTTPS server. Send GET request without header. Should return 200")
|
||||
SECTION("Connect to google HTTPS server over port 443. Send GET request without header. Should return 200")
|
||||
{
|
||||
std::string errMsg;
|
||||
bool tls = true;
|
||||
|
@ -170,4 +170,58 @@ namespace ix
|
||||
|
||||
std::cout << prefix << ": " << s << " => " << ss.str() << std::endl;
|
||||
}
|
||||
|
||||
bool startWebSocketEchoServer(ix::WebSocketServer& server)
|
||||
{
|
||||
server.setOnConnectionCallback(
|
||||
[&server](std::shared_ptr<ix::WebSocket> webSocket,
|
||||
std::shared_ptr<ConnectionState> connectionState)
|
||||
{
|
||||
webSocket->setOnMessageCallback(
|
||||
[webSocket, connectionState, &server](ix::WebSocketMessageType messageType,
|
||||
const std::string& str,
|
||||
size_t wireSize,
|
||||
const ix::WebSocketErrorInfo& error,
|
||||
const ix::WebSocketOpenInfo& openInfo,
|
||||
const ix::WebSocketCloseInfo& closeInfo)
|
||||
{
|
||||
if (messageType == ix::WebSocketMessageType::Open)
|
||||
{
|
||||
Logger() << "New connection";
|
||||
Logger() << "Uri: " << openInfo.uri;
|
||||
Logger() << "Headers:";
|
||||
for (auto it : openInfo.headers)
|
||||
{
|
||||
Logger() << it.first << ": " << it.second;
|
||||
}
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Close)
|
||||
{
|
||||
Logger() << "Closed connection";
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Message)
|
||||
{
|
||||
for (auto&& client : server.getClients())
|
||||
{
|
||||
if (client != webSocket)
|
||||
{
|
||||
client->send(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
auto res = server.listen();
|
||||
if (!res.first)
|
||||
{
|
||||
Logger() << res.second;
|
||||
return false;
|
||||
}
|
||||
|
||||
server.start();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,8 @@
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <ixwebsocket/IXWebSocketServer.h>
|
||||
|
||||
namespace ix
|
||||
{
|
||||
@ -32,8 +34,9 @@ namespace ix
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
std::cerr << obj;
|
||||
std::cerr << std::endl;
|
||||
std::stringstream ss;
|
||||
ss << obj;
|
||||
spdlog::info(ss.str());
|
||||
return *this;
|
||||
}
|
||||
|
||||
@ -44,4 +47,6 @@ namespace ix
|
||||
void log(const std::string& msg);
|
||||
|
||||
int getFreePort();
|
||||
|
||||
bool startWebSocketEchoServer(ix::WebSocketServer& server);
|
||||
}
|
||||
|
456
test/IXWebSocketCloseTest.cpp
Normal file
456
test/IXWebSocketCloseTest.cpp
Normal file
@ -0,0 +1,456 @@
|
||||
/*
|
||||
* IXWebSocketCloseTest.cpp
|
||||
* Author: Alexandre Konieczny
|
||||
* Copyright (c) 2019 Machine Zone. All rights reserved.
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <queue>
|
||||
#include <ixwebsocket/IXWebSocket.h>
|
||||
#include <ixwebsocket/IXWebSocketServer.h>
|
||||
|
||||
#include "IXTest.h"
|
||||
|
||||
#include "catch.hpp"
|
||||
|
||||
using namespace ix;
|
||||
|
||||
namespace
|
||||
{
|
||||
class WebSocketClient
|
||||
{
|
||||
public:
|
||||
WebSocketClient(int port);
|
||||
|
||||
void subscribe(const std::string& channel);
|
||||
void start();
|
||||
void stop();
|
||||
void stop(uint16_t code, const std::string& reason);
|
||||
bool isReady() const;
|
||||
void sendMessage(const std::string& text);
|
||||
|
||||
uint16_t getCloseCode();
|
||||
const std::string& getCloseReason();
|
||||
bool getCloseRemote();
|
||||
|
||||
private:
|
||||
ix::WebSocket _webSocket;
|
||||
int _port;
|
||||
|
||||
mutable std::mutex _mutexCloseData;
|
||||
uint16_t _closeCode;
|
||||
std::string _closeReason;
|
||||
bool _closeRemote;
|
||||
};
|
||||
|
||||
WebSocketClient::WebSocketClient(int port)
|
||||
: _port(port)
|
||||
, _closeCode(0)
|
||||
, _closeReason(std::string(""))
|
||||
, _closeRemote(false)
|
||||
{
|
||||
;
|
||||
}
|
||||
|
||||
bool WebSocketClient::isReady() const
|
||||
{
|
||||
return _webSocket.getReadyState() == ix::ReadyState::Open;
|
||||
}
|
||||
|
||||
uint16_t WebSocketClient::getCloseCode()
|
||||
{
|
||||
std::lock_guard<std::mutex> lck(_mutexCloseData);
|
||||
|
||||
return _closeCode;
|
||||
}
|
||||
|
||||
const std::string& WebSocketClient::getCloseReason()
|
||||
{
|
||||
std::lock_guard<std::mutex> lck(_mutexCloseData);
|
||||
|
||||
return _closeReason;
|
||||
}
|
||||
|
||||
bool WebSocketClient::getCloseRemote()
|
||||
{
|
||||
std::lock_guard<std::mutex> lck(_mutexCloseData);
|
||||
|
||||
return _closeRemote;
|
||||
}
|
||||
|
||||
void WebSocketClient::stop()
|
||||
{
|
||||
_webSocket.stop();
|
||||
}
|
||||
|
||||
void WebSocketClient::stop(uint16_t code, const std::string& reason)
|
||||
{
|
||||
_webSocket.stop(code, reason);
|
||||
}
|
||||
|
||||
void WebSocketClient::start()
|
||||
{
|
||||
std::string url;
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "ws://localhost:"
|
||||
<< _port
|
||||
<< "/";
|
||||
|
||||
url = ss.str();
|
||||
}
|
||||
|
||||
_webSocket.setUrl(url);
|
||||
_webSocket.disableAutomaticReconnection();
|
||||
|
||||
std::stringstream ss;
|
||||
log(std::string("Connecting to url: ") + url);
|
||||
|
||||
_webSocket.setOnMessageCallback(
|
||||
[this](ix::WebSocketMessageType messageType,
|
||||
const std::string& str,
|
||||
size_t wireSize,
|
||||
const ix::WebSocketErrorInfo& error,
|
||||
const ix::WebSocketOpenInfo& openInfo,
|
||||
const ix::WebSocketCloseInfo& closeInfo)
|
||||
{
|
||||
std::stringstream ss;
|
||||
if (messageType == ix::WebSocketMessageType::Open)
|
||||
{
|
||||
log("client connected");
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Close)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "client disconnected("
|
||||
<< closeInfo.code
|
||||
<< ","
|
||||
<< closeInfo.reason
|
||||
<< ")";
|
||||
log(ss.str());
|
||||
|
||||
std::lock_guard<std::mutex> lck(_mutexCloseData);
|
||||
|
||||
_closeCode = closeInfo.code;
|
||||
_closeReason = std::string(closeInfo.reason);
|
||||
_closeRemote = closeInfo.remote;
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Error)
|
||||
{
|
||||
ss << "Error ! " << error.reason;
|
||||
log(ss.str());
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Pong)
|
||||
{
|
||||
ss << "Received pong message " << str;
|
||||
log(ss.str());
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Ping)
|
||||
{
|
||||
ss << "Received ping message " << str;
|
||||
log(ss.str());
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Message)
|
||||
{
|
||||
ss << "Received message " << str;
|
||||
log(ss.str());
|
||||
}
|
||||
else
|
||||
{
|
||||
ss << "Invalid ix::WebSocketMessageType";
|
||||
log(ss.str());
|
||||
}
|
||||
});
|
||||
|
||||
_webSocket.start();
|
||||
}
|
||||
|
||||
void WebSocketClient::sendMessage(const std::string& text)
|
||||
{
|
||||
_webSocket.send(text);
|
||||
}
|
||||
|
||||
bool startServer(ix::WebSocketServer& server,
|
||||
uint16_t& receivedCloseCode,
|
||||
std::string& receivedCloseReason,
|
||||
bool& receivedCloseRemote,
|
||||
std::mutex& mutexWrite)
|
||||
{
|
||||
// A dev/null server
|
||||
server.setOnConnectionCallback(
|
||||
[&server, &receivedCloseCode, &receivedCloseReason, &receivedCloseRemote, &mutexWrite](std::shared_ptr<ix::WebSocket> webSocket,
|
||||
std::shared_ptr<ConnectionState> connectionState)
|
||||
{
|
||||
webSocket->setOnMessageCallback(
|
||||
[webSocket, connectionState, &server, &receivedCloseCode, &receivedCloseReason, &receivedCloseRemote, &mutexWrite](ix::WebSocketMessageType messageType,
|
||||
const std::string& str,
|
||||
size_t wireSize,
|
||||
const ix::WebSocketErrorInfo& error,
|
||||
const ix::WebSocketOpenInfo& openInfo,
|
||||
const ix::WebSocketCloseInfo& closeInfo)
|
||||
{
|
||||
if (messageType == ix::WebSocketMessageType::Open)
|
||||
{
|
||||
Logger() << "New server connection";
|
||||
Logger() << "id: " << connectionState->getId();
|
||||
Logger() << "Uri: " << openInfo.uri;
|
||||
Logger() << "Headers:";
|
||||
for (auto it : openInfo.headers)
|
||||
{
|
||||
Logger() << it.first << ": " << it.second;
|
||||
}
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Close)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Server closed connection("
|
||||
<< closeInfo.code
|
||||
<< ","
|
||||
<< closeInfo.reason
|
||||
<< ")";
|
||||
log(ss.str());
|
||||
|
||||
std::lock_guard<std::mutex> lck(mutexWrite);
|
||||
|
||||
receivedCloseCode = closeInfo.code;
|
||||
receivedCloseReason = std::string(closeInfo.reason);
|
||||
receivedCloseRemote = closeInfo.remote;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
auto res = server.listen();
|
||||
if (!res.first)
|
||||
{
|
||||
log(res.second);
|
||||
return false;
|
||||
}
|
||||
|
||||
server.start();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Websocket_client_close_default", "[close]")
|
||||
{
|
||||
SECTION("Make sure that close code and reason was used and sent to server.")
|
||||
{
|
||||
ix::setupWebSocketTrafficTrackerCallback();
|
||||
|
||||
int port = getFreePort();
|
||||
ix::WebSocketServer server(port);
|
||||
|
||||
uint16_t serverReceivedCloseCode(0);
|
||||
bool serverReceivedCloseRemote(false);
|
||||
std::string serverReceivedCloseReason("");
|
||||
std::mutex mutexWrite;
|
||||
|
||||
REQUIRE(startServer(server, serverReceivedCloseCode, serverReceivedCloseReason, serverReceivedCloseRemote, mutexWrite));
|
||||
|
||||
std::string session = ix::generateSessionId();
|
||||
WebSocketClient webSocketClient(port);
|
||||
|
||||
webSocketClient.start();
|
||||
|
||||
// Wait for all chat instance to be ready
|
||||
while (true)
|
||||
{
|
||||
if (webSocketClient.isReady()) break;
|
||||
ix::msleep(10);
|
||||
}
|
||||
|
||||
REQUIRE(server.getClients().size() == 1);
|
||||
|
||||
ix::msleep(500);
|
||||
|
||||
webSocketClient.stop();
|
||||
|
||||
ix::msleep(500);
|
||||
|
||||
// ensure client close is the same as values given
|
||||
REQUIRE(webSocketClient.getCloseCode() == 1000);
|
||||
REQUIRE(webSocketClient.getCloseReason() == "Normal closure");
|
||||
REQUIRE(webSocketClient.getCloseRemote() == false);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lck(mutexWrite);
|
||||
|
||||
// Here we read the code/reason received by the server, and ensure that remote is true
|
||||
REQUIRE(serverReceivedCloseCode == 1000);
|
||||
REQUIRE(serverReceivedCloseReason == "Normal closure");
|
||||
REQUIRE(serverReceivedCloseRemote == true);
|
||||
}
|
||||
|
||||
// Give us 1000ms for the server to notice that clients went away
|
||||
ix::msleep(1000);
|
||||
REQUIRE(server.getClients().size() == 0);
|
||||
|
||||
ix::reportWebSocketTraffic();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Websocket_client_close_params_given", "[close]")
|
||||
{
|
||||
SECTION("Make sure that close code and reason was used and sent to server.")
|
||||
{
|
||||
ix::setupWebSocketTrafficTrackerCallback();
|
||||
|
||||
int port = getFreePort();
|
||||
ix::WebSocketServer server(port);
|
||||
|
||||
uint16_t serverReceivedCloseCode(0);
|
||||
bool serverReceivedCloseRemote(false);
|
||||
std::string serverReceivedCloseReason("");
|
||||
std::mutex mutexWrite;
|
||||
|
||||
REQUIRE(startServer(server, serverReceivedCloseCode, serverReceivedCloseReason, serverReceivedCloseRemote, mutexWrite));
|
||||
|
||||
std::string session = ix::generateSessionId();
|
||||
WebSocketClient webSocketClient(port);
|
||||
|
||||
webSocketClient.start();
|
||||
|
||||
// Wait for all chat instance to be ready
|
||||
while (true)
|
||||
{
|
||||
if (webSocketClient.isReady()) break;
|
||||
ix::msleep(10);
|
||||
}
|
||||
|
||||
REQUIRE(server.getClients().size() == 1);
|
||||
|
||||
ix::msleep(500);
|
||||
|
||||
webSocketClient.stop(4000, "My reason");
|
||||
|
||||
ix::msleep(500);
|
||||
|
||||
// ensure client close is the same as values given
|
||||
REQUIRE(webSocketClient.getCloseCode() == 4000);
|
||||
REQUIRE(webSocketClient.getCloseReason() == "My reason");
|
||||
REQUIRE(webSocketClient.getCloseRemote() == false);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lck(mutexWrite);
|
||||
|
||||
// Here we read the code/reason received by the server, and ensure that remote is true
|
||||
REQUIRE(serverReceivedCloseCode == 4000);
|
||||
REQUIRE(serverReceivedCloseReason == "My reason");
|
||||
REQUIRE(serverReceivedCloseRemote == true);
|
||||
}
|
||||
|
||||
// Give us 1000ms for the server to notice that clients went away
|
||||
ix::msleep(1000);
|
||||
REQUIRE(server.getClients().size() == 0);
|
||||
|
||||
ix::reportWebSocketTraffic();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Websocket_server_close", "[close]")
|
||||
{
|
||||
SECTION("Make sure that close code and reason was read from server.")
|
||||
{
|
||||
ix::setupWebSocketTrafficTrackerCallback();
|
||||
|
||||
int port = getFreePort();
|
||||
ix::WebSocketServer server(port);
|
||||
|
||||
uint16_t serverReceivedCloseCode(0);
|
||||
bool serverReceivedCloseRemote(false);
|
||||
std::string serverReceivedCloseReason("");
|
||||
std::mutex mutexWrite;
|
||||
|
||||
REQUIRE(startServer(server, serverReceivedCloseCode, serverReceivedCloseReason, serverReceivedCloseRemote, mutexWrite));
|
||||
|
||||
std::string session = ix::generateSessionId();
|
||||
WebSocketClient webSocketClient(port);
|
||||
|
||||
webSocketClient.start();
|
||||
|
||||
// Wait for all chat instance to be ready
|
||||
while (true)
|
||||
{
|
||||
if (webSocketClient.isReady()) break;
|
||||
ix::msleep(10);
|
||||
}
|
||||
|
||||
REQUIRE(server.getClients().size() == 1);
|
||||
|
||||
ix::msleep(500);
|
||||
|
||||
server.stop();
|
||||
|
||||
ix::msleep(500);
|
||||
|
||||
// ensure client close is the same as values given
|
||||
REQUIRE(webSocketClient.getCloseCode() == 1000);
|
||||
REQUIRE(webSocketClient.getCloseReason() == "Normal closure");
|
||||
REQUIRE(webSocketClient.getCloseRemote() == true);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lck(mutexWrite);
|
||||
|
||||
// Here we read the code/reason received by the server, and ensure that remote is true
|
||||
REQUIRE(serverReceivedCloseCode == 1000);
|
||||
REQUIRE(serverReceivedCloseReason == "Normal closure");
|
||||
REQUIRE(serverReceivedCloseRemote == false);
|
||||
}
|
||||
|
||||
// Give us 1000ms for the server to notice that clients went away
|
||||
ix::msleep(1000);
|
||||
REQUIRE(server.getClients().size() == 0);
|
||||
|
||||
ix::reportWebSocketTraffic();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Websocket_server_close_immediatly", "[close]")
|
||||
{
|
||||
SECTION("Make sure that close code and reason was read from server.")
|
||||
{
|
||||
ix::setupWebSocketTrafficTrackerCallback();
|
||||
|
||||
int port = getFreePort();
|
||||
ix::WebSocketServer server(port);
|
||||
|
||||
uint16_t serverReceivedCloseCode(0);
|
||||
bool serverReceivedCloseRemote(false);
|
||||
std::string serverReceivedCloseReason("");
|
||||
std::mutex mutexWrite;
|
||||
|
||||
REQUIRE(startServer(server, serverReceivedCloseCode, serverReceivedCloseReason, serverReceivedCloseRemote, mutexWrite));
|
||||
|
||||
std::string session = ix::generateSessionId();
|
||||
WebSocketClient webSocketClient(port);
|
||||
|
||||
webSocketClient.start();
|
||||
|
||||
server.stop();
|
||||
|
||||
ix::msleep(500);
|
||||
|
||||
// ensure client close hasn't been called
|
||||
REQUIRE(webSocketClient.getCloseCode() == 0);
|
||||
REQUIRE(webSocketClient.getCloseReason() == "");
|
||||
REQUIRE(webSocketClient.getCloseRemote() == false);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lck(mutexWrite);
|
||||
|
||||
// Here we ensure that the code/reason wasn't received by the server
|
||||
REQUIRE(serverReceivedCloseCode == 0);
|
||||
REQUIRE(serverReceivedCloseReason == "");
|
||||
REQUIRE(serverReceivedCloseRemote == false);
|
||||
}
|
||||
|
||||
// Give us 1000ms for the server to notice that clients went away
|
||||
ix::msleep(1000);
|
||||
REQUIRE(server.getClients().size() == 0);
|
||||
|
||||
ix::reportWebSocketTraffic();
|
||||
}
|
||||
}
|
193
test/IXWebSocketMessageQTest.cpp
Normal file
193
test/IXWebSocketMessageQTest.cpp
Normal file
@ -0,0 +1,193 @@
|
||||
/*
|
||||
* IXWebSocketMessageQTest.cpp
|
||||
* Author: Korchynskyi Dmytro
|
||||
* Copyright (c) 2019 Machine Zone. All rights reserved.
|
||||
*/
|
||||
|
||||
#include <ixwebsocket/IXWebSocket.h>
|
||||
#include <ixwebsocket/IXWebSocketServer.h>
|
||||
#include <ixwebsocket/IXWebSocketMessageQueue.h>
|
||||
|
||||
#include "IXTest.h"
|
||||
#include "catch.hpp"
|
||||
#include <thread>
|
||||
|
||||
using namespace ix;
|
||||
|
||||
namespace
|
||||
{
|
||||
bool startServer(ix::WebSocketServer& server)
|
||||
{
|
||||
server.setOnConnectionCallback(
|
||||
[&server](std::shared_ptr<ix::WebSocket> webSocket,
|
||||
std::shared_ptr<ConnectionState> connectionState)
|
||||
{
|
||||
webSocket->setOnMessageCallback(
|
||||
[connectionState, &server](ix::WebSocketMessageType messageType,
|
||||
const std::string & str,
|
||||
size_t wireSize,
|
||||
const ix::WebSocketErrorInfo & error,
|
||||
const ix::WebSocketOpenInfo & openInfo,
|
||||
const ix::WebSocketCloseInfo & closeInfo)
|
||||
{
|
||||
if (messageType == ix::WebSocketMessageType::Open)
|
||||
{
|
||||
Logger() << "New connection";
|
||||
connectionState->computeId();
|
||||
Logger() << "id: " << connectionState->getId();
|
||||
Logger() << "Uri: " << openInfo.uri;
|
||||
Logger() << "Headers:";
|
||||
for (auto it : openInfo.headers)
|
||||
{
|
||||
Logger() << it.first << ": " << it.second;
|
||||
}
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Close)
|
||||
{
|
||||
Logger() << "Closed connection";
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Message)
|
||||
{
|
||||
Logger() << "Message received: " << str;
|
||||
|
||||
for (auto&& client : server.getClients())
|
||||
{
|
||||
client->send(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
auto res = server.listen();
|
||||
if (!res.first)
|
||||
{
|
||||
Logger() << res.second;
|
||||
return false;
|
||||
}
|
||||
|
||||
server.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
class MsgQTestClient
|
||||
{
|
||||
public:
|
||||
MsgQTestClient()
|
||||
{
|
||||
msgQ.bindWebsocket(&ws);
|
||||
|
||||
msgQ.setOnMessageCallback([this](WebSocketMessageType messageType,
|
||||
const std::string & str,
|
||||
size_t wireSize,
|
||||
const WebSocketErrorInfo & error,
|
||||
const WebSocketOpenInfo & openInfo,
|
||||
const WebSocketCloseInfo & closeInfo)
|
||||
{
|
||||
REQUIRE(mainThreadId == std::this_thread::get_id());
|
||||
|
||||
std::stringstream ss;
|
||||
if (messageType == WebSocketMessageType::Open)
|
||||
{
|
||||
log("client connected");
|
||||
sendNextMessage();
|
||||
}
|
||||
else if (messageType == WebSocketMessageType::Close)
|
||||
{
|
||||
log("client disconnected");
|
||||
}
|
||||
else if (messageType == WebSocketMessageType::Error)
|
||||
{
|
||||
ss << "Error ! " << error.reason;
|
||||
log(ss.str());
|
||||
testDone = true;
|
||||
}
|
||||
else if (messageType == WebSocketMessageType::Pong)
|
||||
{
|
||||
ss << "Received pong message " << str;
|
||||
log(ss.str());
|
||||
}
|
||||
else if (messageType == WebSocketMessageType::Ping)
|
||||
{
|
||||
ss << "Received ping message " << str;
|
||||
log(ss.str());
|
||||
}
|
||||
else if (messageType == WebSocketMessageType::Message)
|
||||
{
|
||||
REQUIRE(str.compare("Hey dude!") == 0);
|
||||
++receivedCount;
|
||||
ss << "Received message " << str;
|
||||
log(ss.str());
|
||||
sendNextMessage();
|
||||
}
|
||||
else
|
||||
{
|
||||
ss << "Invalid WebSocketMessageType";
|
||||
log(ss.str());
|
||||
testDone = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void sendNextMessage()
|
||||
{
|
||||
if (receivedCount >= 3)
|
||||
{
|
||||
testDone = true;
|
||||
succeeded = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto info = ws.sendText("Hey dude!");
|
||||
if (info.success)
|
||||
log("sent message");
|
||||
else
|
||||
log("send failed");
|
||||
}
|
||||
}
|
||||
|
||||
void run(const std::string& url)
|
||||
{
|
||||
mainThreadId = std::this_thread::get_id();
|
||||
testDone = false;
|
||||
receivedCount = 0;
|
||||
|
||||
ws.setUrl(url);
|
||||
ws.start();
|
||||
|
||||
while (!testDone)
|
||||
{
|
||||
msgQ.poll();
|
||||
msleep(50);
|
||||
}
|
||||
}
|
||||
|
||||
bool isSucceeded() const { return succeeded; }
|
||||
|
||||
private:
|
||||
WebSocket ws;
|
||||
WebSocketMessageQueue msgQ;
|
||||
bool testDone = false;
|
||||
uint32_t receivedCount = 0;
|
||||
std::thread::id mainThreadId;
|
||||
bool succeeded = false;
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE("Websocket_message_queue", "[websocket_message_q]")
|
||||
{
|
||||
SECTION("Send several messages")
|
||||
{
|
||||
int port = getFreePort();
|
||||
WebSocketServer server(port);
|
||||
REQUIRE(startServer(server));
|
||||
|
||||
MsgQTestClient testClient;
|
||||
testClient.run("ws://127.0.0.1:" + std::to_string(port));
|
||||
REQUIRE(testClient.isSucceeded());
|
||||
|
||||
server.stop();
|
||||
}
|
||||
|
||||
}
|
@ -225,8 +225,8 @@ TEST_CASE("Websocket_ping_no_data_sent_setPingInterval", "[setPingInterval]")
|
||||
// -> expected ping messages == 2 as 2100 seconds, 1 ping sent every second
|
||||
REQUIRE(serverReceivedPingMessages == 2);
|
||||
|
||||
// Give us 500ms for the server to notice that clients went away
|
||||
ix::msleep(500);
|
||||
// Give us 1000ms for the server to notice that clients went away
|
||||
ix::msleep(1000);
|
||||
REQUIRE(server.getClients().size() == 0);
|
||||
|
||||
ix::reportWebSocketTraffic();
|
||||
@ -272,8 +272,8 @@ TEST_CASE("Websocket_ping_data_sent_setPingInterval", "[setPingInterval]")
|
||||
// -> expected ping messages == 3 as 900+900+1300 = 3100 seconds, 1 ping sent every second
|
||||
REQUIRE(serverReceivedPingMessages == 3);
|
||||
|
||||
// Give us 500ms for the server to notice that clients went away
|
||||
ix::msleep(500);
|
||||
// Give us 1000ms for the server to notice that clients went away
|
||||
ix::msleep(1000);
|
||||
REQUIRE(server.getClients().size() == 0);
|
||||
|
||||
ix::reportWebSocketTraffic();
|
||||
@ -325,8 +325,8 @@ TEST_CASE("Websocket_ping_data_sent_setPingInterval_half_full", "[setPingInterva
|
||||
|
||||
webSocketClient.stop();
|
||||
|
||||
// Give us 500ms for the server to notice that clients went away
|
||||
ix::msleep(500);
|
||||
// Give us 1000ms for the server to notice that clients went away
|
||||
ix::msleep(1000);
|
||||
REQUIRE(server.getClients().size() == 0);
|
||||
|
||||
ix::reportWebSocketTraffic();
|
||||
@ -377,8 +377,8 @@ TEST_CASE("Websocket_ping_data_sent_setPingInterval_full", "[setPingInterval]")
|
||||
|
||||
webSocketClient.stop();
|
||||
|
||||
// Give us 500ms for the server to notice that clients went away
|
||||
ix::msleep(500);
|
||||
// Give us 1000ms for the server to notice that clients went away
|
||||
ix::msleep(1000);
|
||||
REQUIRE(server.getClients().size() == 0);
|
||||
|
||||
ix::reportWebSocketTraffic();
|
||||
@ -413,17 +413,17 @@ TEST_CASE("Websocket_ping_no_data_sent_setHeartBeatPeriod", "[setHeartBeatPeriod
|
||||
|
||||
REQUIRE(server.getClients().size() == 1);
|
||||
|
||||
ix::msleep(1850);
|
||||
ix::msleep(1900);
|
||||
|
||||
webSocketClient.stop();
|
||||
|
||||
|
||||
// Here we test ping interval
|
||||
// -> expected ping messages == 1 as 1850 seconds, 1 ping sent every second
|
||||
// -> expected ping messages == 1 as 1900 seconds, 1 ping sent every second
|
||||
REQUIRE(serverReceivedPingMessages == 1);
|
||||
|
||||
// Give us 500ms for the server to notice that clients went away
|
||||
ix::msleep(500);
|
||||
// Give us 1000ms for the server to notice that clients went away
|
||||
ix::msleep(1000);
|
||||
REQUIRE(server.getClients().size() == 0);
|
||||
|
||||
ix::reportWebSocketTraffic();
|
||||
@ -460,7 +460,7 @@ TEST_CASE("Websocket_ping_data_sent_setHeartBeatPeriod", "[setHeartBeatPeriod]")
|
||||
webSocketClient.sendMessage("hello world");
|
||||
ix::msleep(900);
|
||||
webSocketClient.sendMessage("hello world");
|
||||
ix::msleep(900);
|
||||
ix::msleep(1100);
|
||||
|
||||
webSocketClient.stop();
|
||||
|
||||
@ -469,11 +469,11 @@ TEST_CASE("Websocket_ping_data_sent_setHeartBeatPeriod", "[setHeartBeatPeriod]")
|
||||
|
||||
// Here we test ping interval
|
||||
// client has sent data, but ping should have been sent no matter what
|
||||
// -> expected ping messages == 2 as 900+900+900 = 2700 seconds, 1 ping sent every second
|
||||
// -> expected ping messages == 2 as 900+900+1100 = 2900 seconds, 1 ping sent every second
|
||||
REQUIRE(serverReceivedPingMessages == 2);
|
||||
|
||||
// Give us 500ms for the server to notice that clients went away
|
||||
ix::msleep(500);
|
||||
// Give us 1000ms for the server to notice that clients went away
|
||||
ix::msleep(1000);
|
||||
REQUIRE(server.getClients().size() == 0);
|
||||
|
||||
ix::reportWebSocketTraffic();
|
||||
|
@ -259,8 +259,8 @@ TEST_CASE("Websocket_ping_timeout_not_checked", "[setPingTimeout]")
|
||||
|
||||
webSocketClient.stop();
|
||||
|
||||
// Give us 500ms for the server to notice that clients went away
|
||||
ix::msleep(500);
|
||||
// Give us 1000ms for the server to notice that clients went away
|
||||
ix::msleep(1000);
|
||||
REQUIRE(server.getClients().size() == 0);
|
||||
|
||||
// Ensure client close was not by ping timeout
|
||||
@ -298,7 +298,7 @@ TEST_CASE("Websocket_ping_no_timeout", "[setPingTimeout]")
|
||||
|
||||
REQUIRE(server.getClients().size() == 1);
|
||||
|
||||
ix::msleep(1100);
|
||||
ix::msleep(1200);
|
||||
|
||||
// Here we test ping timeout, no timeout
|
||||
REQUIRE(serverReceivedPingMessages == 1);
|
||||
@ -312,8 +312,8 @@ TEST_CASE("Websocket_ping_no_timeout", "[setPingTimeout]")
|
||||
|
||||
webSocketClient.stop();
|
||||
|
||||
// Give us 500ms for the server to notice that clients went away
|
||||
ix::msleep(500);
|
||||
// Give us 1000ms for the server to notice that clients went away
|
||||
ix::msleep(1000);
|
||||
REQUIRE(server.getClients().size() == 0);
|
||||
|
||||
// Ensure client close was not by ping timeout
|
||||
@ -350,7 +350,7 @@ TEST_CASE("Websocket_no_ping_but_timeout", "[setPingTimeout]")
|
||||
|
||||
REQUIRE(server.getClients().size() == 1);
|
||||
|
||||
ix::msleep(2700);
|
||||
ix::msleep(2900);
|
||||
|
||||
// Here we test ping timeout, no timeout yet
|
||||
REQUIRE(serverReceivedPingMessages == 0);
|
||||
@ -359,12 +359,13 @@ TEST_CASE("Websocket_no_ping_but_timeout", "[setPingTimeout]")
|
||||
REQUIRE(webSocketClient.isClosed() == false);
|
||||
REQUIRE(webSocketClient.closedDueToPingTimeout() == false);
|
||||
|
||||
ix::msleep(400);
|
||||
ix::msleep(300);
|
||||
|
||||
// Here we test ping timeout, timeout
|
||||
REQUIRE(serverReceivedPingMessages == 0);
|
||||
REQUIRE(webSocketClient.getReceivedPongMessages() == 0);
|
||||
// Ensure client close was not by ping timeout
|
||||
// Ensure client close was by ping timeout
|
||||
ix::msleep(1000);
|
||||
REQUIRE(webSocketClient.isClosed() == true);
|
||||
REQUIRE(webSocketClient.closedDueToPingTimeout() == true);
|
||||
|
||||
@ -410,12 +411,13 @@ TEST_CASE("Websocket_ping_timeout", "[setPingTimeout]")
|
||||
REQUIRE(serverReceivedPingMessages == 1);
|
||||
REQUIRE(webSocketClient.getReceivedPongMessages() == 0);
|
||||
|
||||
ix::msleep(1000);
|
||||
ix::msleep(1100);
|
||||
|
||||
// Here we test ping timeout, timeout
|
||||
REQUIRE(serverReceivedPingMessages == 1);
|
||||
REQUIRE(webSocketClient.getReceivedPongMessages() == 0);
|
||||
// Ensure client close was not by ping timeout
|
||||
// Ensure client close was by ping timeout
|
||||
ix::msleep(1000);
|
||||
REQUIRE(webSocketClient.isClosed() == true);
|
||||
REQUIRE(webSocketClient.closedDueToPingTimeout() == true);
|
||||
|
||||
@ -427,7 +429,6 @@ TEST_CASE("Websocket_ping_timeout", "[setPingTimeout]")
|
||||
}
|
||||
}
|
||||
|
||||
#if 0 // this test fails on travis / commenting it out for now to get back to a green travis state
|
||||
TEST_CASE("Websocket_ping_long_timeout", "[setPingTimeout]")
|
||||
{
|
||||
SECTION("Make sure that ping messages don't have responses (no PONG).")
|
||||
@ -456,7 +457,7 @@ TEST_CASE("Websocket_ping_long_timeout", "[setPingTimeout]")
|
||||
|
||||
REQUIRE(server.getClients().size() == 1);
|
||||
|
||||
ix::msleep(5900);
|
||||
ix::msleep(5800);
|
||||
|
||||
// Here we test ping timeout, no timeout yet (2 ping sent at 2s and 4s)
|
||||
REQUIRE(serverReceivedPingMessages == 2);
|
||||
@ -466,7 +467,7 @@ TEST_CASE("Websocket_ping_long_timeout", "[setPingTimeout]")
|
||||
REQUIRE(webSocketClient.isClosed() == false);
|
||||
REQUIRE(webSocketClient.closedDueToPingTimeout() == false);
|
||||
|
||||
ix::msleep(200);
|
||||
ix::msleep(600);
|
||||
|
||||
// Here we test ping timeout, timeout (at 6 seconds)
|
||||
REQUIRE(serverReceivedPingMessages == 2);
|
||||
@ -482,4 +483,3 @@ TEST_CASE("Websocket_ping_long_timeout", "[setPingTimeout]")
|
||||
ix::reportWebSocketTraffic();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -62,33 +62,33 @@ namespace
|
||||
std::stringstream ss;
|
||||
if (messageType == ix::WebSocketMessageType::Open)
|
||||
{
|
||||
log("cmd_websocket_satori_chat: connected !");
|
||||
log("TestConnectionDisconnection: connected !");
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Close)
|
||||
{
|
||||
log("cmd_websocket_satori_chat: disconnected !");
|
||||
log("TestConnectionDisconnection: disconnected !");
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Error)
|
||||
{
|
||||
ss << "cmd_websocket_satori_chat: Error! ";
|
||||
ss << "TestConnectionDisconnection: Error! ";
|
||||
ss << error.reason;
|
||||
log(ss.str());
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Message)
|
||||
{
|
||||
log("cmd_websocket_satori_chat: received message.!");
|
||||
log("TestConnectionDisconnection: received message.!");
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Ping)
|
||||
{
|
||||
log("cmd_websocket_satori_chat: received ping message.!");
|
||||
log("TestConnectionDisconnection: received ping message.!");
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Pong)
|
||||
{
|
||||
log("cmd_websocket_satori_chat: received pong message.!");
|
||||
log("TestConnectionDisconnection: received pong message.!");
|
||||
}
|
||||
else if (messageType == ix::WebSocketMessageType::Fragment)
|
||||
{
|
||||
log("cmd_websocket_satori_chat: received fragment.!");
|
||||
log("TestConnectionDisconnection: received fragment.!");
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -96,6 +96,12 @@ namespace
|
||||
}
|
||||
});
|
||||
|
||||
_webSocket.enableAutomaticReconnection();
|
||||
REQUIRE(_webSocket.isAutomaticReconnectionEnabled() == true);
|
||||
|
||||
_webSocket.disableAutomaticReconnection();
|
||||
REQUIRE(_webSocket.isAutomaticReconnectionEnabled() == false);
|
||||
|
||||
// Start the connection
|
||||
_webSocket.start();
|
||||
}
|
||||
@ -123,26 +129,38 @@ TEST_CASE("websocket_connections", "[websocket]")
|
||||
SECTION("Try to connect and disconnect with different timing, not enough time to succesfully connect")
|
||||
{
|
||||
IXWebSocketTestConnectionDisconnection test;
|
||||
log(std::string("50 Runs"));
|
||||
|
||||
for (int i = 0; i < 50; ++i)
|
||||
{
|
||||
log(std::string("Run: ") + std::to_string(i));
|
||||
test.start(WEBSOCKET_DOT_ORG_URL);
|
||||
|
||||
log(std::string("Sleeping"));
|
||||
ix::msleep(i);
|
||||
|
||||
log(std::string("Stopping"));
|
||||
test.stop();
|
||||
}
|
||||
}
|
||||
|
||||
// This test breaks on travis CI - Ubuntu Xenial + gcc + tsan
|
||||
// We should fix this.
|
||||
/*SECTION("Try to connect and disconnect with different timing, from not enough time to successfull connect")
|
||||
SECTION("Try to connect and disconnect with different timing, from not enough time to successfull connect")
|
||||
{
|
||||
IXWebSocketTestConnectionDisconnection test;
|
||||
log(std::string("20 Runs"));
|
||||
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
log(std::string("Run: ") + std::to_string(i));
|
||||
test.start(WEBSOCKET_DOT_ORG_URL);
|
||||
|
||||
log(std::string("Sleeping"));
|
||||
ix::msleep(i*50);
|
||||
|
||||
log(std::string("Stopping"));
|
||||
test.stop();
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
@ -336,8 +336,8 @@ TEST_CASE("Websocket_chat", "[websocket_chat]")
|
||||
REQUIRE(chatA.getReceivedMessages()[1] == "from B2");
|
||||
REQUIRE(chatA.getReceivedMessages()[2].size() == bigMessage.size());
|
||||
|
||||
// Give us 500ms for the server to notice that clients went away
|
||||
ix::msleep(500);
|
||||
// Give us 1000ms for the server to notice that clients went away
|
||||
ix::msleep(1000);
|
||||
REQUIRE(server.getClients().size() == 0);
|
||||
|
||||
ix::reportWebSocketTraffic();
|
||||
|
137
test/run.py
137
test/run.py
@ -1,10 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
'''
|
||||
Windows notes:
|
||||
generator = '-G"NMake Makefiles"'
|
||||
make = 'nmake'
|
||||
testBinary ='ixwebsocket_unittest.exe'
|
||||
'''
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
@ -28,9 +22,9 @@ try:
|
||||
except ImportError:
|
||||
hasClick = False
|
||||
|
||||
|
||||
DEFAULT_EXE = 'ixwebsocket_unittest'
|
||||
|
||||
BUILD_TYPE = 'Debug'
|
||||
XML_OUTPUT_FILE = 'ixwebsocket_unittest.xml'
|
||||
TEST_EXE_PATH = None
|
||||
|
||||
class Command(object):
|
||||
"""Run system commands with timeout
|
||||
@ -65,7 +59,7 @@ class Command(object):
|
||||
return True, self.process.returncode
|
||||
|
||||
|
||||
def runCommand(cmd, assertOnFailure=True, timeout=None):
|
||||
def runCommand(cmd, abortOnFailure=True, timeout=None):
|
||||
'''Small wrapper to run a command and make sure it succeed'''
|
||||
|
||||
if timeout is None:
|
||||
@ -73,16 +67,13 @@ def runCommand(cmd, assertOnFailure=True, timeout=None):
|
||||
|
||||
print('\nRunning', cmd)
|
||||
command = Command(cmd)
|
||||
timedout, ret = command.run(timeout)
|
||||
succeed, ret = command.run(timeout)
|
||||
|
||||
if timedout:
|
||||
print('Unittest timed out')
|
||||
|
||||
msg = 'cmd {} failed with error code {}'.format(cmd, ret)
|
||||
if ret != 0:
|
||||
if not succeed or ret != 0:
|
||||
msg = 'cmd {}\nfailed with error code {}'.format(cmd, ret)
|
||||
print(msg)
|
||||
if assertOnFailure:
|
||||
assert False
|
||||
if abortOnFailure:
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
def runCMake(sanitizer, buildDir):
|
||||
@ -91,12 +82,6 @@ def runCMake(sanitizer, buildDir):
|
||||
(remove build sub-folder).
|
||||
'''
|
||||
|
||||
# CMake installed via Self Service ends up here.
|
||||
cmake_executable = '/Applications/CMake.app/Contents/bin/cmake'
|
||||
|
||||
if not os.path.exists(cmake_executable):
|
||||
cmake_executable = 'cmake'
|
||||
|
||||
sanitizersFlags = {
|
||||
'asan': '-DSANITIZE_ADDRESS=On',
|
||||
'ubsan': '-DSANITIZE_UNDEFINED=On',
|
||||
@ -110,19 +95,23 @@ def runCMake(sanitizer, buildDir):
|
||||
if not os.path.exists(cmakeExecutable):
|
||||
cmakeExecutable = 'cmake'
|
||||
|
||||
generator = '"Unix Makefiles"'
|
||||
if platform.system() == 'Windows':
|
||||
generator = '"NMake Makefiles"'
|
||||
#generator = '"NMake Makefiles"'
|
||||
#generator = '"Visual Studio 16 2019"'
|
||||
generator = '"Visual Studio 15 2017"'
|
||||
else:
|
||||
generator = '"Unix Makefiles"'
|
||||
|
||||
fmt = '''
|
||||
{cmakeExecutable} -H. \
|
||||
CMAKE_BUILD_TYPE = BUILD_TYPE
|
||||
|
||||
fmt = '{cmakeExecutable} -H. \
|
||||
{sanitizerFlag} \
|
||||
-B{buildDir} \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-B"{buildDir}" \
|
||||
-DCMAKE_BUILD_TYPE={CMAKE_BUILD_TYPE} \
|
||||
-DUSE_TLS=1 \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-G{generator}
|
||||
'''
|
||||
-G{generator}'
|
||||
|
||||
cmakeCmd = fmt.format(**locals())
|
||||
runCommand(cmakeCmd)
|
||||
|
||||
@ -133,10 +122,10 @@ def runTest(args, buildDir, xmlOutput, testRunName):
|
||||
if args is None:
|
||||
args = ''
|
||||
|
||||
fmt = '{buildDir}/{DEFAULT_EXE} -o {xmlOutput} -n "{testRunName}" -r junit "{args}"'
|
||||
testCommand = fmt.format(**locals())
|
||||
testCommand = '{} -o {} -n "{}" -r junit "{}"'.format(TEST_EXE_PATH, xmlOutput, testRunName, args)
|
||||
|
||||
runCommand(testCommand,
|
||||
assertOnFailure=False)
|
||||
abortOnFailure=False)
|
||||
|
||||
|
||||
def validateTestSuite(xmlOutput):
|
||||
@ -280,12 +269,12 @@ def executeJob(job):
|
||||
return job
|
||||
|
||||
|
||||
def executeJobs(jobs):
|
||||
def executeJobs(jobs, cpuCount):
|
||||
'''Execute a list of job concurrently on multiple CPU/cores'''
|
||||
|
||||
poolSize = multiprocessing.cpu_count()
|
||||
print('Using {} cores to execute the unittest'.format(cpuCount))
|
||||
|
||||
pool = multiprocessing.Pool(poolSize)
|
||||
pool = multiprocessing.Pool(cpuCount)
|
||||
results = pool.map(executeJob, jobs)
|
||||
pool.close()
|
||||
pool.join()
|
||||
@ -296,8 +285,7 @@ def executeJobs(jobs):
|
||||
def computeAllTestNames(buildDir):
|
||||
'''Compute all test case names, by executing the unittest in a custom mode'''
|
||||
|
||||
executable = os.path.join(buildDir, DEFAULT_EXE)
|
||||
cmd = '"{}" --list-test-names-only'.format(executable)
|
||||
cmd = '"{}" --list-test-names-only'.format(TEST_EXE_PATH)
|
||||
names = os.popen(cmd).read().splitlines()
|
||||
names.sort() # Sort test names for execution determinism
|
||||
return names
|
||||
@ -344,7 +332,7 @@ def generateXmlOutput(results, xmlOutput, testRunName, runTime):
|
||||
})
|
||||
|
||||
systemOut = ET.Element('system-out')
|
||||
systemOut.text = result['output'].decode('utf-8')
|
||||
systemOut.text = result['output'].decode('utf-8', 'ignore')
|
||||
testCase.append(systemOut)
|
||||
|
||||
if not result['success']:
|
||||
@ -358,23 +346,22 @@ def generateXmlOutput(results, xmlOutput, testRunName, runTime):
|
||||
f.write(content.encode('utf-8'))
|
||||
|
||||
|
||||
def run(testName, buildDir, sanitizer, xmlOutput, testRunName, buildOnly, useLLDB):
|
||||
def run(testName, buildDir, sanitizer, xmlOutput,
|
||||
testRunName, buildOnly, useLLDB, cpuCount):
|
||||
'''Main driver. Run cmake, compiles, execute and validate the testsuite.'''
|
||||
|
||||
# gen build files with CMake
|
||||
runCMake(sanitizer, buildDir)
|
||||
|
||||
# build with make
|
||||
makeCmd = 'make'
|
||||
jobs = '-j8'
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
makeCmd = 'nmake'
|
||||
|
||||
# nmake does not have a -j option
|
||||
jobs = ''
|
||||
|
||||
runCommand('{} -C {} {}'.format(makeCmd, buildDir, jobs))
|
||||
if platform.system() == 'Linux':
|
||||
# build with make -j
|
||||
runCommand('make -C {} -j 2'.format(buildDir))
|
||||
elif platform.system() == 'Darwin':
|
||||
# build with make
|
||||
runCommand('make -C {} -j 8'.format(buildDir))
|
||||
else:
|
||||
# build with cmake on recent
|
||||
runCommand('cmake --build --parallel {}'.format(buildDir))
|
||||
|
||||
if buildOnly:
|
||||
return
|
||||
@ -409,12 +396,7 @@ def run(testName, buildDir, sanitizer, xmlOutput, testRunName, buildOnly, useLLD
|
||||
continue
|
||||
|
||||
# testName can contains spaces, so we enclose them in double quotes
|
||||
executable = os.path.join(buildDir, DEFAULT_EXE)
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
executable += '.exe'
|
||||
|
||||
cmd = '{} "{}" "{}" > "{}" 2>&1'.format(lldb, executable, testName, outputPath)
|
||||
cmd = '{} "{}" "{}" > "{}" 2>&1'.format(lldb, TEST_EXE_PATH, testName, outputPath)
|
||||
|
||||
jobs.append({
|
||||
'name': testName,
|
||||
@ -424,7 +406,7 @@ def run(testName, buildDir, sanitizer, xmlOutput, testRunName, buildOnly, useLLD
|
||||
})
|
||||
|
||||
start = time.time()
|
||||
results = executeJobs(jobs)
|
||||
results = executeJobs(jobs, cpuCount)
|
||||
runTime = time.time() - start
|
||||
generateXmlOutput(results, xmlOutput, testRunName, runTime)
|
||||
|
||||
@ -454,8 +436,6 @@ def main():
|
||||
if not os.path.exists(buildDir):
|
||||
os.makedirs(buildDir)
|
||||
|
||||
defaultOutput = DEFAULT_EXE + '.xml'
|
||||
|
||||
parser = argparse.ArgumentParser(description='Build and Run the engine unittest')
|
||||
|
||||
sanitizers = ['tsan', 'asan', 'ubsan', 'none']
|
||||
@ -476,19 +456,41 @@ def main():
|
||||
help='Run the test through lldb.')
|
||||
parser.add_argument('--run_name', '-n',
|
||||
help='Name of the test run.')
|
||||
parser.add_argument('--cpu_count', '-j', type=int, default=multiprocessing.cpu_count(),
|
||||
help='Number of cpus to use for running the tests.')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Windows does not play nice with multiple files opened by different processes
|
||||
# "The process cannot access the file because it is being used by another process"
|
||||
if platform.system() == 'Windows':
|
||||
args.cpu_count = 1
|
||||
|
||||
# Default sanitizer is tsan
|
||||
sanitizer = args.sanitizer
|
||||
if args.sanitizer is None:
|
||||
|
||||
if args.no_sanitizer:
|
||||
sanitizer = 'none'
|
||||
elif args.sanitizer is None:
|
||||
sanitizer = 'tsan'
|
||||
|
||||
# Sanitizers display lots of strange errors on Linux on CI,
|
||||
# which looks like false positives
|
||||
if platform.system() != 'Darwin':
|
||||
sanitizer = 'none'
|
||||
|
||||
defaultRunName = 'ixengine_{}_{}'.format(platform.system(), sanitizer)
|
||||
|
||||
xmlOutput = args.output or defaultOutput
|
||||
xmlOutput = args.output or XML_OUTPUT_FILE
|
||||
testRunName = args.run_name or os.getenv('IXENGINE_TEST_RUN_NAME') or defaultRunName
|
||||
|
||||
global TEST_EXE_PATH
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
TEST_EXE_PATH = os.path.join(buildDir, BUILD_TYPE, 'ixwebsocket_unittest.exe')
|
||||
else:
|
||||
TEST_EXE_PATH = os.path.join(buildDir, 'ixwebsocket_unittest')
|
||||
|
||||
if args.list:
|
||||
# catch2 exit with a different error code when requesting the list of files
|
||||
try:
|
||||
@ -505,13 +507,8 @@ def main():
|
||||
print('LLDB is only supported on Apple at this point')
|
||||
args.lldb = False
|
||||
|
||||
# Sanitizers display lots of strange errors on Linux on CI,
|
||||
# which looks like false positives
|
||||
if platform.system() != 'Darwin':
|
||||
sanitizer = 'none'
|
||||
|
||||
return run(args.test, buildDir, sanitizer, xmlOutput,
|
||||
testRunName, args.build_only, args.lldb)
|
||||
testRunName, args.build_only, args.lldb, args.cpu_count)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
638
third_party/spdlog/README.md
vendored
638
third_party/spdlog/README.md
vendored
@ -1,319 +1,319 @@
|
||||
# spdlog
|
||||
|
||||
Very fast, header only, C++ logging library. [](https://travis-ci.org/gabime/spdlog) [](https://ci.appveyor.com/project/gabime/spdlog)
|
||||
|
||||
|
||||
|
||||
## Install
|
||||
#### Just copy the headers:
|
||||
|
||||
* Copy the source [folder](https://github.com/gabime/spdlog/tree/v1.x/include/spdlog) to your build tree and use a C++11 compiler.
|
||||
|
||||
#### Or use your favorite package manager:
|
||||
|
||||
* Ubuntu: `apt-get install libspdlog-dev`
|
||||
* Homebrew: `brew install spdlog`
|
||||
* FreeBSD: `cd /usr/ports/devel/spdlog/ && make install clean`
|
||||
* Fedora: `yum install spdlog`
|
||||
* Gentoo: `emerge dev-libs/spdlog`
|
||||
* Arch Linux: `yaourt -S spdlog-git`
|
||||
* vcpkg: `vcpkg install spdlog`
|
||||
|
||||
|
||||
## Platforms
|
||||
* Linux, FreeBSD, OpenBSD, Solaris, AIX
|
||||
* Windows (msvc 2013+, cygwin)
|
||||
* macOS (clang 3.5+)
|
||||
* Android
|
||||
|
||||
## Features
|
||||
* Very fast (see [benchmarks](#benchmarks) below).
|
||||
* Headers only, just copy and use.
|
||||
* Feature rich formatting, using the excellent [fmt](https://github.com/fmtlib/fmt) library.
|
||||
* Fast asynchronous mode (optional)
|
||||
* [Custom](https://github.com/gabime/spdlog/wiki/3.-Custom-formatting) formatting.
|
||||
* Multi/Single threaded loggers.
|
||||
* Various log targets:
|
||||
* Rotating log files.
|
||||
* Daily log files.
|
||||
* Console logging (colors supported).
|
||||
* syslog.
|
||||
* Windows debugger (```OutputDebugString(..)```)
|
||||
* Easily extendable with custom log targets (just implement a single function in the [sink](include/spdlog/sinks/sink.h) interface).
|
||||
* Severity based filtering - threshold levels can be modified in runtime as well as in compile time.
|
||||
* Binary data logging.
|
||||
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Below are some [benchmarks](https://github.com/gabime/spdlog/blob/v1.x/bench/bench.cpp) done in Ubuntu 64 bit, Intel i7-4770 CPU @ 3.40GHz
|
||||
|
||||
#### Synchronous mode
|
||||
```
|
||||
*******************************************************************************
|
||||
Single thread, 1,000,000 iterations
|
||||
*******************************************************************************
|
||||
basic_st... Elapsed: 0.181652 5,505,042/sec
|
||||
rotating_st... Elapsed: 0.181781 5,501,117/sec
|
||||
daily_st... Elapsed: 0.187595 5,330,630/sec
|
||||
null_st... Elapsed: 0.0504704 19,813,602/sec
|
||||
*******************************************************************************
|
||||
10 threads sharing same logger, 1,000,000 iterations
|
||||
*******************************************************************************
|
||||
basic_mt... Elapsed: 0.616035 1,623,284/sec
|
||||
rotating_mt... Elapsed: 0.620344 1,612,008/sec
|
||||
daily_mt... Elapsed: 0.648353 1,542,369/sec
|
||||
null_mt... Elapsed: 0.151972 6,580,166/sec
|
||||
```
|
||||
#### Asynchronous mode
|
||||
```
|
||||
*******************************************************************************
|
||||
10 threads sharing same logger, 1,000,000 iterations
|
||||
*******************************************************************************
|
||||
async... Elapsed: 0.350066 2,856,606/sec
|
||||
async... Elapsed: 0.314865 3,175,960/sec
|
||||
async... Elapsed: 0.349851 2,858,358/sec
|
||||
```
|
||||
|
||||
## Usage samples
|
||||
|
||||
#### Basic usage
|
||||
```c++
|
||||
#include "spdlog/spdlog.h"
|
||||
int main()
|
||||
{
|
||||
spdlog::info("Welcome to spdlog!");
|
||||
spdlog::error("Some error message with arg: {}", 1);
|
||||
|
||||
spdlog::warn("Easy padding in numbers like {:08d}", 12);
|
||||
spdlog::critical("Support for int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}", 42);
|
||||
spdlog::info("Support for floats {:03.2f}", 1.23456);
|
||||
spdlog::info("Positional args are {1} {0}..", "too", "supported");
|
||||
spdlog::info("{:<30}", "left aligned");
|
||||
|
||||
spdlog::set_level(spdlog::level::debug); // Set global log level to debug
|
||||
spdlog::debug("This message should be displayed..");
|
||||
|
||||
// change log pattern
|
||||
spdlog::set_pattern("[%H:%M:%S %z] [%n] [%^---%L---%$] [thread %t] %v");
|
||||
|
||||
// Compile time log levels
|
||||
// define SPDLOG_ACTIVE_LEVEL to desired level
|
||||
SPDLOG_TRACE("Some trace message with param {}", {});
|
||||
SPDLOG_DEBUG("Some debug message");
|
||||
|
||||
}
|
||||
```
|
||||
#### create stdout/stderr logger object
|
||||
```c++
|
||||
#include "spdlog/spdlog.h"
|
||||
#include "spdlog/sinks/stdout_color_sinks.h"
|
||||
void stdout_example()
|
||||
{
|
||||
// create color multi threaded logger
|
||||
auto console = spdlog::stdout_color_mt("console");
|
||||
auto err_logger = spdlog::stderr_color_mt("stderr");
|
||||
spdlog::get("console")->info("loggers can be retrieved from a global registry using the spdlog::get(logger_name)");
|
||||
}
|
||||
```
|
||||
---
|
||||
#### Basic file logger
|
||||
```c++
|
||||
#include "spdlog/sinks/basic_file_sink.h"
|
||||
void basic_logfile_example()
|
||||
{
|
||||
try
|
||||
{
|
||||
auto my_logger = spdlog::basic_logger_mt("basic_logger", "logs/basic-log.txt");
|
||||
}
|
||||
catch (const spdlog::spdlog_ex &ex)
|
||||
{
|
||||
std::cout << "Log init failed: " << ex.what() << std::endl;
|
||||
}
|
||||
}
|
||||
```
|
||||
---
|
||||
#### Rotating files
|
||||
```c++
|
||||
#include "spdlog/sinks/rotating_file_sink.h"
|
||||
void rotating_example()
|
||||
{
|
||||
// Create a file rotating logger with 5mb size max and 3 rotated files
|
||||
auto rotating_logger = spdlog::rotating_logger_mt("some_logger_name", "logs/rotating.txt", 1048576 * 5, 3);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
#### Daily files
|
||||
```c++
|
||||
|
||||
#include "spdlog/sinks/daily_file_sink.h"
|
||||
void daily_example()
|
||||
{
|
||||
// Create a daily logger - a new file is created every day on 2:30am
|
||||
auto daily_logger = spdlog::daily_logger_mt("daily_logger", "logs/daily.txt", 2, 30);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
#### Cloning loggers
|
||||
```c++
|
||||
// clone a logger and give it new name.
|
||||
// Useful for creating subsystem loggers from some "root" logger
|
||||
void clone_example()
|
||||
{
|
||||
auto network_logger = spdlog::get("root")->clone("network");
|
||||
network_logger->info("Logging network stuff..");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
#### Periodic flush
|
||||
```c++
|
||||
// periodically flush all *registered* loggers every 3 seconds:
|
||||
// warning: only use if all your loggers are thread safe!
|
||||
spdlog::flush_every(std::chrono::seconds(3));
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
#### Binary logging
|
||||
```c++
|
||||
// log binary data as hex.
|
||||
// many types of std::container<char> types can be used.
|
||||
// ranges are supported too.
|
||||
// format flags:
|
||||
// {:X} - print in uppercase.
|
||||
// {:s} - don't separate each byte with space.
|
||||
// {:p} - don't print the position on each line start.
|
||||
// {:n} - don't split the output to lines.
|
||||
|
||||
#include "spdlog/fmt/bin_to_hex.h"
|
||||
|
||||
void binary_example()
|
||||
{
|
||||
auto console = spdlog::get("console");
|
||||
std::array<char, 80> buf;
|
||||
console->info("Binary example: {}", spdlog::to_hex(buf));
|
||||
console->info("Another binary example:{:n}", spdlog::to_hex(std::begin(buf), std::begin(buf) + 10));
|
||||
// more examples:
|
||||
// logger->info("uppercase: {:X}", spdlog::to_hex(buf));
|
||||
// logger->info("uppercase, no delimiters: {:Xs}", spdlog::to_hex(buf));
|
||||
// logger->info("uppercase, no delimiters, no position info: {:Xsp}", spdlog::to_hex(buf));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
#### Logger with multi sinks - each with different format and log level
|
||||
```c++
|
||||
|
||||
// create logger with 2 targets with different log levels and formats.
|
||||
// the console will show only warnings or errors, while the file will log all.
|
||||
void multi_sink_example()
|
||||
{
|
||||
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
|
||||
console_sink->set_level(spdlog::level::warn);
|
||||
console_sink->set_pattern("[multi_sink_example] [%^%l%$] %v");
|
||||
|
||||
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/multisink.txt", true);
|
||||
file_sink->set_level(spdlog::level::trace);
|
||||
|
||||
spdlog::logger logger("multi_sink", {console_sink, file_sink});
|
||||
logger.set_level(spdlog::level::debug);
|
||||
logger.warn("this should appear in both console and file");
|
||||
logger.info("this message should not appear in the console, only in the file");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
#### Asynchronous logging
|
||||
```c++
|
||||
#include "spdlog/async.h"
|
||||
#include "spdlog/sinks/basic_file_sink.h"
|
||||
void async_example()
|
||||
{
|
||||
// default thread pool settings can be modified *before* creating the async logger:
|
||||
// spdlog::init_thread_pool(8192, 1); // queue with 8k items and 1 backing thread.
|
||||
auto async_file = spdlog::basic_logger_mt<spdlog::async_factory>("async_file_logger", "logs/async_log.txt");
|
||||
// alternatively:
|
||||
// auto async_file = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>("async_file_logger", "logs/async_log.txt");
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
#### Asynchronous logger with multi sinks
|
||||
```c++
|
||||
#include "spdlog/sinks/stdout_color_sinks.h"
|
||||
#include "spdlog/sinks/rotating_file_sink.h"
|
||||
|
||||
void multi_sink_example2()
|
||||
{
|
||||
spdlog::init_thread_pool(8192, 1);
|
||||
auto stdout_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt >();
|
||||
auto rotating_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>("mylog.txt", 1024*1024*10, 3);
|
||||
std::vector<spdlog::sink_ptr> sinks {stdout_sink, rotating_sink};
|
||||
auto logger = std::make_shared<spdlog::async_logger>("loggername", sinks.begin(), sinks.end(), spdlog::thread_pool(), spdlog::async_overflow_policy::block);
|
||||
spdlog::register_logger(logger);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
#### User defined types
|
||||
```c++
|
||||
// user defined types logging by implementing operator<<
|
||||
#include "spdlog/fmt/ostr.h" // must be included
|
||||
struct my_type
|
||||
{
|
||||
int i;
|
||||
template<typename OStream>
|
||||
friend OStream &operator<<(OStream &os, const my_type &c)
|
||||
{
|
||||
return os << "[my_type i=" << c.i << "]";
|
||||
}
|
||||
};
|
||||
|
||||
void user_defined_example()
|
||||
{
|
||||
spdlog::get("console")->info("user defined type: {}", my_type{14});
|
||||
}
|
||||
|
||||
```
|
||||
---
|
||||
#### Custom error handler
|
||||
```c++
|
||||
void err_handler_example()
|
||||
{
|
||||
// can be set globally or per logger(logger->set_error_handler(..))
|
||||
spdlog::set_error_handler([](const std::string &msg) { spdlog::get("console")->error("*** LOGGER ERROR ***: {}", msg); });
|
||||
spdlog::get("console")->info("some invalid message to trigger an error {}{}{}{}", 3);
|
||||
}
|
||||
|
||||
```
|
||||
---
|
||||
#### syslog
|
||||
```c++
|
||||
#include "spdlog/sinks/syslog_sink.h"
|
||||
void syslog_example()
|
||||
{
|
||||
std::string ident = "spdlog-example";
|
||||
auto syslog_logger = spdlog::syslog_logger_mt("syslog", ident, LOG_PID);
|
||||
syslog_logger->warn("This is warning that will end up in syslog.");
|
||||
}
|
||||
```
|
||||
---
|
||||
#### Android example
|
||||
```c++
|
||||
#include "spdlog/sinks/android_sink.h"
|
||||
void android_example()
|
||||
{
|
||||
std::string tag = "spdlog-android";
|
||||
auto android_logger = spdlog::android_logger("android", tag);
|
||||
android_logger->critical("Use \"adb shell logcat\" to view this message.");
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
Documentation can be found in the [wiki](https://github.com/gabime/spdlog/wiki/1.-QuickStart) pages.
|
||||
# spdlog
|
||||
|
||||
Very fast, header only, C++ logging library. [](https://travis-ci.org/gabime/spdlog) [](https://ci.appveyor.com/project/gabime/spdlog)
|
||||
|
||||
|
||||
|
||||
## Install
|
||||
#### Just copy the headers:
|
||||
|
||||
* Copy the source [folder](https://github.com/gabime/spdlog/tree/v1.x/include/spdlog) to your build tree and use a C++11 compiler.
|
||||
|
||||
#### Or use your favorite package manager:
|
||||
|
||||
* Ubuntu: `apt-get install libspdlog-dev`
|
||||
* Homebrew: `brew install spdlog`
|
||||
* FreeBSD: `cd /usr/ports/devel/spdlog/ && make install clean`
|
||||
* Fedora: `yum install spdlog`
|
||||
* Gentoo: `emerge dev-libs/spdlog`
|
||||
* Arch Linux: `yaourt -S spdlog-git`
|
||||
* vcpkg: `vcpkg install spdlog`
|
||||
|
||||
|
||||
## Platforms
|
||||
* Linux, FreeBSD, OpenBSD, Solaris, AIX
|
||||
* Windows (msvc 2013+, cygwin)
|
||||
* macOS (clang 3.5+)
|
||||
* Android
|
||||
|
||||
## Features
|
||||
* Very fast (see [benchmarks](#benchmarks) below).
|
||||
* Headers only, just copy and use.
|
||||
* Feature rich formatting, using the excellent [fmt](https://github.com/fmtlib/fmt) library.
|
||||
* Fast asynchronous mode (optional)
|
||||
* [Custom](https://github.com/gabime/spdlog/wiki/3.-Custom-formatting) formatting.
|
||||
* Multi/Single threaded loggers.
|
||||
* Various log targets:
|
||||
* Rotating log files.
|
||||
* Daily log files.
|
||||
* Console logging (colors supported).
|
||||
* syslog.
|
||||
* Windows debugger (```OutputDebugString(..)```)
|
||||
* Easily extendable with custom log targets (just implement a single function in the [sink](include/spdlog/sinks/sink.h) interface).
|
||||
* Severity based filtering - threshold levels can be modified in runtime as well as in compile time.
|
||||
* Binary data logging.
|
||||
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Below are some [benchmarks](https://github.com/gabime/spdlog/blob/v1.x/bench/bench.cpp) done in Ubuntu 64 bit, Intel i7-4770 CPU @ 3.40GHz
|
||||
|
||||
#### Synchronous mode
|
||||
```
|
||||
*******************************************************************************
|
||||
Single thread, 1,000,000 iterations
|
||||
*******************************************************************************
|
||||
basic_st... Elapsed: 0.181652 5,505,042/sec
|
||||
rotating_st... Elapsed: 0.181781 5,501,117/sec
|
||||
daily_st... Elapsed: 0.187595 5,330,630/sec
|
||||
null_st... Elapsed: 0.0504704 19,813,602/sec
|
||||
*******************************************************************************
|
||||
10 threads sharing same logger, 1,000,000 iterations
|
||||
*******************************************************************************
|
||||
basic_mt... Elapsed: 0.616035 1,623,284/sec
|
||||
rotating_mt... Elapsed: 0.620344 1,612,008/sec
|
||||
daily_mt... Elapsed: 0.648353 1,542,369/sec
|
||||
null_mt... Elapsed: 0.151972 6,580,166/sec
|
||||
```
|
||||
#### Asynchronous mode
|
||||
```
|
||||
*******************************************************************************
|
||||
10 threads sharing same logger, 1,000,000 iterations
|
||||
*******************************************************************************
|
||||
async... Elapsed: 0.350066 2,856,606/sec
|
||||
async... Elapsed: 0.314865 3,175,960/sec
|
||||
async... Elapsed: 0.349851 2,858,358/sec
|
||||
```
|
||||
|
||||
## Usage samples
|
||||
|
||||
#### Basic usage
|
||||
```c++
|
||||
#include "spdlog/spdlog.h"
|
||||
int main()
|
||||
{
|
||||
spdlog::info("Welcome to spdlog!");
|
||||
spdlog::error("Some error message with arg: {}", 1);
|
||||
|
||||
spdlog::warn("Easy padding in numbers like {:08d}", 12);
|
||||
spdlog::critical("Support for int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}", 42);
|
||||
spdlog::info("Support for floats {:03.2f}", 1.23456);
|
||||
spdlog::info("Positional args are {1} {0}..", "too", "supported");
|
||||
spdlog::info("{:<30}", "left aligned");
|
||||
|
||||
spdlog::set_level(spdlog::level::debug); // Set global log level to debug
|
||||
spdlog::debug("This message should be displayed..");
|
||||
|
||||
// change log pattern
|
||||
spdlog::set_pattern("[%H:%M:%S %z] [%n] [%^---%L---%$] [thread %t] %v");
|
||||
|
||||
// Compile time log levels
|
||||
// define SPDLOG_ACTIVE_LEVEL to desired level
|
||||
SPDLOG_TRACE("Some trace message with param {}", {});
|
||||
SPDLOG_DEBUG("Some debug message");
|
||||
|
||||
}
|
||||
```
|
||||
#### create stdout/stderr logger object
|
||||
```c++
|
||||
#include "spdlog/spdlog.h"
|
||||
#include "spdlog/sinks/stdout_color_sinks.h"
|
||||
void stdout_example()
|
||||
{
|
||||
// create color multi threaded logger
|
||||
auto console = spdlog::stdout_color_mt("console");
|
||||
auto err_logger = spdlog::stderr_color_mt("stderr");
|
||||
spdlog::get("console")->info("loggers can be retrieved from a global registry using the spdlog::get(logger_name)");
|
||||
}
|
||||
```
|
||||
---
|
||||
#### Basic file logger
|
||||
```c++
|
||||
#include "spdlog/sinks/basic_file_sink.h"
|
||||
void basic_logfile_example()
|
||||
{
|
||||
try
|
||||
{
|
||||
auto my_logger = spdlog::basic_logger_mt("basic_logger", "logs/basic-log.txt");
|
||||
}
|
||||
catch (const spdlog::spdlog_ex &ex)
|
||||
{
|
||||
std::cout << "Log init failed: " << ex.what() << std::endl;
|
||||
}
|
||||
}
|
||||
```
|
||||
---
|
||||
#### Rotating files
|
||||
```c++
|
||||
#include "spdlog/sinks/rotating_file_sink.h"
|
||||
void rotating_example()
|
||||
{
|
||||
// Create a file rotating logger with 5mb size max and 3 rotated files
|
||||
auto rotating_logger = spdlog::rotating_logger_mt("some_logger_name", "logs/rotating.txt", 1048576 * 5, 3);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
#### Daily files
|
||||
```c++
|
||||
|
||||
#include "spdlog/sinks/daily_file_sink.h"
|
||||
void daily_example()
|
||||
{
|
||||
// Create a daily logger - a new file is created every day on 2:30am
|
||||
auto daily_logger = spdlog::daily_logger_mt("daily_logger", "logs/daily.txt", 2, 30);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
#### Cloning loggers
|
||||
```c++
|
||||
// clone a logger and give it new name.
|
||||
// Useful for creating subsystem loggers from some "root" logger
|
||||
void clone_example()
|
||||
{
|
||||
auto network_logger = spdlog::get("root")->clone("network");
|
||||
network_logger->info("Logging network stuff..");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
#### Periodic flush
|
||||
```c++
|
||||
// periodically flush all *registered* loggers every 3 seconds:
|
||||
// warning: only use if all your loggers are thread safe!
|
||||
spdlog::flush_every(std::chrono::seconds(3));
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
#### Binary logging
|
||||
```c++
|
||||
// log binary data as hex.
|
||||
// many types of std::container<char> types can be used.
|
||||
// ranges are supported too.
|
||||
// format flags:
|
||||
// {:X} - print in uppercase.
|
||||
// {:s} - don't separate each byte with space.
|
||||
// {:p} - don't print the position on each line start.
|
||||
// {:n} - don't split the output to lines.
|
||||
|
||||
#include "spdlog/fmt/bin_to_hex.h"
|
||||
|
||||
void binary_example()
|
||||
{
|
||||
auto console = spdlog::get("console");
|
||||
std::array<char, 80> buf;
|
||||
console->info("Binary example: {}", spdlog::to_hex(buf));
|
||||
console->info("Another binary example:{:n}", spdlog::to_hex(std::begin(buf), std::begin(buf) + 10));
|
||||
// more examples:
|
||||
// logger->info("uppercase: {:X}", spdlog::to_hex(buf));
|
||||
// logger->info("uppercase, no delimiters: {:Xs}", spdlog::to_hex(buf));
|
||||
// logger->info("uppercase, no delimiters, no position info: {:Xsp}", spdlog::to_hex(buf));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
#### Logger with multi sinks - each with different format and log level
|
||||
```c++
|
||||
|
||||
// create logger with 2 targets with different log levels and formats.
|
||||
// the console will show only warnings or errors, while the file will log all.
|
||||
void multi_sink_example()
|
||||
{
|
||||
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
|
||||
console_sink->set_level(spdlog::level::warn);
|
||||
console_sink->set_pattern("[multi_sink_example] [%^%l%$] %v");
|
||||
|
||||
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/multisink.txt", true);
|
||||
file_sink->set_level(spdlog::level::trace);
|
||||
|
||||
spdlog::logger logger("multi_sink", {console_sink, file_sink});
|
||||
logger.set_level(spdlog::level::debug);
|
||||
logger.warn("this should appear in both console and file");
|
||||
logger.info("this message should not appear in the console, only in the file");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
#### Asynchronous logging
|
||||
```c++
|
||||
#include "spdlog/async.h"
|
||||
#include "spdlog/sinks/basic_file_sink.h"
|
||||
void async_example()
|
||||
{
|
||||
// default thread pool settings can be modified *before* creating the async logger:
|
||||
// spdlog::init_thread_pool(8192, 1); // queue with 8k items and 1 backing thread.
|
||||
auto async_file = spdlog::basic_logger_mt<spdlog::async_factory>("async_file_logger", "logs/async_log.txt");
|
||||
// alternatively:
|
||||
// auto async_file = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>("async_file_logger", "logs/async_log.txt");
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
#### Asynchronous logger with multi sinks
|
||||
```c++
|
||||
#include "spdlog/sinks/stdout_color_sinks.h"
|
||||
#include "spdlog/sinks/rotating_file_sink.h"
|
||||
|
||||
void multi_sink_example2()
|
||||
{
|
||||
spdlog::init_thread_pool(8192, 1);
|
||||
auto stdout_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt >();
|
||||
auto rotating_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>("mylog.txt", 1024*1024*10, 3);
|
||||
std::vector<spdlog::sink_ptr> sinks {stdout_sink, rotating_sink};
|
||||
auto logger = std::make_shared<spdlog::async_logger>("loggername", sinks.begin(), sinks.end(), spdlog::thread_pool(), spdlog::async_overflow_policy::block);
|
||||
spdlog::register_logger(logger);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
#### User defined types
|
||||
```c++
|
||||
// user defined types logging by implementing operator<<
|
||||
#include "spdlog/fmt/ostr.h" // must be included
|
||||
struct my_type
|
||||
{
|
||||
int i;
|
||||
template<typename OStream>
|
||||
friend OStream &operator<<(OStream &os, const my_type &c)
|
||||
{
|
||||
return os << "[my_type i=" << c.i << "]";
|
||||
}
|
||||
};
|
||||
|
||||
void user_defined_example()
|
||||
{
|
||||
spdlog::get("console")->info("user defined type: {}", my_type{14});
|
||||
}
|
||||
|
||||
```
|
||||
---
|
||||
#### Custom error handler
|
||||
```c++
|
||||
void err_handler_example()
|
||||
{
|
||||
// can be set globally or per logger(logger->set_error_handler(..))
|
||||
spdlog::set_error_handler([](const std::string &msg) { spdlog::get("console")->error("*** LOGGER ERROR ***: {}", msg); });
|
||||
spdlog::get("console")->info("some invalid message to trigger an error {}{}{}{}", 3);
|
||||
}
|
||||
|
||||
```
|
||||
---
|
||||
#### syslog
|
||||
```c++
|
||||
#include "spdlog/sinks/syslog_sink.h"
|
||||
void syslog_example()
|
||||
{
|
||||
std::string ident = "spdlog-example";
|
||||
auto syslog_logger = spdlog::syslog_logger_mt("syslog", ident, LOG_PID);
|
||||
syslog_logger->warn("This is warning that will end up in syslog.");
|
||||
}
|
||||
```
|
||||
---
|
||||
#### Android example
|
||||
```c++
|
||||
#include "spdlog/sinks/android_sink.h"
|
||||
void android_example()
|
||||
{
|
||||
std::string tag = "spdlog-android";
|
||||
auto android_logger = spdlog::android_logger("android", tag);
|
||||
android_logger->critical("Use \"adb shell logcat\" to view this message.");
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
Documentation can be found in the [wiki](https://github.com/gabime/spdlog/wiki/1.-QuickStart) pages.
|
||||
|
@ -713,7 +713,7 @@ typedef basic_format_args<wprintf_context> wprintf_args;
|
||||
/**
|
||||
\rst
|
||||
Constructs an `~fmt::format_arg_store` object that contains references to
|
||||
arguments and can be implicitly converted to `~fmt::printf_args`.
|
||||
arguments and can be implicitly converted to `~fmt::printf_args`.
|
||||
\endrst
|
||||
*/
|
||||
template<typename... Args>
|
||||
@ -723,7 +723,7 @@ inline format_arg_store<printf_context, Args...>
|
||||
/**
|
||||
\rst
|
||||
Constructs an `~fmt::format_arg_store` object that contains references to
|
||||
arguments and can be implicitly converted to `~fmt::wprintf_args`.
|
||||
arguments and can be implicitly converted to `~fmt::wprintf_args`.
|
||||
\endrst
|
||||
*/
|
||||
template<typename... Args>
|
||||
|
@ -159,7 +159,7 @@ void for_each(index_sequence<Is...>, Tuple &&tup, F &&f) FMT_NOEXCEPT {
|
||||
}
|
||||
|
||||
template <class T>
|
||||
FMT_CONSTEXPR make_index_sequence<std::tuple_size<T>::value>
|
||||
FMT_CONSTEXPR make_index_sequence<std::tuple_size<T>::value>
|
||||
get_indexes(T const &) { return {}; }
|
||||
|
||||
template <class Tuple, class F>
|
||||
@ -169,14 +169,14 @@ void for_each(Tuple &&tup, F &&f) {
|
||||
}
|
||||
|
||||
template<typename Arg>
|
||||
FMT_CONSTEXPR const char* format_str_quoted(bool add_space, const Arg&,
|
||||
FMT_CONSTEXPR const char* format_str_quoted(bool add_space, const Arg&,
|
||||
typename std::enable_if<
|
||||
!is_like_std_string<typename std::decay<Arg>::type>::value>::type* = nullptr) {
|
||||
return add_space ? " {}" : "{}";
|
||||
}
|
||||
|
||||
template<typename Arg>
|
||||
FMT_CONSTEXPR const char* format_str_quoted(bool add_space, const Arg&,
|
||||
FMT_CONSTEXPR const char* format_str_quoted(bool add_space, const Arg&,
|
||||
typename std::enable_if<
|
||||
is_like_std_string<typename std::decay<Arg>::type>::value>::type* = nullptr) {
|
||||
return add_space ? " \"{}\"" : "\"{}\"";
|
||||
@ -205,7 +205,7 @@ struct is_tuple_like {
|
||||
};
|
||||
|
||||
template <typename TupleT, typename Char>
|
||||
struct formatter<TupleT, Char,
|
||||
struct formatter<TupleT, Char,
|
||||
typename std::enable_if<fmt::is_tuple_like<TupleT>::value>::type> {
|
||||
private:
|
||||
// C++11 generic lambda for format()
|
||||
|
2
third_party/spdlog/include/spdlog/tweakme.h
vendored
2
third_party/spdlog/include/spdlog/tweakme.h
vendored
@ -142,4 +142,4 @@
|
||||
// Defaults to __FUNCTION__ (should work on all compilers) if not defined.
|
||||
//
|
||||
// #define SPDLOG_FUNCTION __PRETTY_FUNCTION__
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
2
third_party/spdlog/tests/main.cpp
vendored
2
third_party/spdlog/tests/main.cpp
vendored
@ -1,2 +1,2 @@
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch.hpp"
|
||||
#include "catch.hpp"
|
||||
|
2
third_party/spdlog/tests/test_mpmc_q.cpp
vendored
2
third_party/spdlog/tests/test_mpmc_q.cpp
vendored
@ -104,4 +104,4 @@ TEST_CASE("full_queue", "[mpmc_blocking_q]")
|
||||
int item = -1;
|
||||
q.dequeue_for(item, milliseconds(0));
|
||||
REQUIRE(item == 123456);
|
||||
}
|
||||
}
|
||||
|
2
third_party/spdlog/tests/utils.h
vendored
2
third_party/spdlog/tests/utils.h
vendored
@ -13,4 +13,4 @@ std::size_t count_lines(const std::string &filename);
|
||||
|
||||
std::size_t get_filesize(const std::string &filename);
|
||||
|
||||
bool ends_with(std::string const &value, std::string const &ending);
|
||||
bool ends_with(std::string const &value, std::string const &ending);
|
||||
|
@ -61,4 +61,4 @@ sleep 2
|
||||
kill `cat /tmp/ws_test/pidfile.transfer`
|
||||
kill `cat /tmp/ws_test/pidfile.receive`
|
||||
kill `cat /tmp/ws_test/pidfile.send`
|
||||
|
||||
exit 0
|
||||
|
Reference in New Issue
Block a user