Browse Source

synchronous stratum client test (wip)

cl-refactor
Genoil 9 years ago
parent
commit
148d71a5cb
  1. 2
      CMakeLists.txt
  2. 132
      ethminer/MinerAux.h
  3. 4
      libethash-cuda/ethash_cuda_miner.cpp
  4. 7
      libethash-cuda/ethash_cuda_miner_kernel.cu
  5. 3
      libethash-cuda/ethash_cuda_miner_kernel.h
  6. 18
      libethcore/EthashCUDAMiner.cpp
  7. 10
      libethcore/Miner.h
  8. 16
      libstratum/EthStratumClient.cpp
  9. 6
      libstratum/EthStratumClient.h
  10. 332
      libstratum/EthStratumClientV2.cpp
  11. 83
      libstratum/EthStratumClientV2.h

2
CMakeLists.txt

@ -2,7 +2,7 @@
cmake_minimum_required(VERSION 2.8.12)
set(PROJECT_VERSION "0.9.41")
set(GENOIL_VERSION "1.1")
set(GENOIL_VERSION "1.1.1")
if (${CMAKE_VERSION} VERSION_GREATER 3.0)
cmake_policy(SET CMP0042 OLD) # fix MACOSX_RPATH
cmake_policy(SET CMP0048 NEW) # allow VERSION argument in project()

132
ethminer/MinerAux.h

@ -60,6 +60,7 @@
#endif
#if ETH_STRATUM || !ETH_TRUE
#include <libstratum/EthStratumClient.h>
#include <libstratum/EthStratumClientV2.h>
#endif
using namespace std;
using namespace dev;
@ -187,6 +188,19 @@ public:
if (p + 1 <= userpass.length())
m_pass = userpass.substr(p+1);
}
else if ((arg == "-SV" || arg == "--stratum-version") && i + 1 < argc)
{
try {
m_stratumClientVersion = atoi(argv[++i]);
if (m_stratumClientVersion > 2) m_stratumClientVersion = 2;
else if (m_stratumClientVersion < 1) m_stratumClientVersion = 1;
}
catch (...)
{
cerr << "Bad " << arg << " option: " << argv[i] << endl;
BOOST_THROW_EXCEPTION(BadArgument());
}
}
else if ((arg == "-FO" || arg == "--failover-userpass") && i + 1 < argc)
{
string userpass = string(argv[++i]);
@ -601,6 +615,7 @@ public:
<< " -O, --userpass <username.workername:password> Stratum login credentials" << endl
<< " -FO, --failover-userpass <username.workername:password> Failover stratum login credentials (optional, will use normal credentials when omitted)" << endl
<< " --work-timeout <n> reconnect/failover after n seconds of working on the same (stratum) job. Defaults to 180. Don't set lower than max. avg. block time" << endl
<< " -SV, --stratum-version <n> Stratum client version. Defaults to 1 (async client). Use 2 to test new synchronous client."
#endif
#if ETH_JSONRPC || ETH_STRATUM || !ETH_TRUE
<< " --farm-recheck <n> Leave n ms between checks for changed work (default: 500). When using stratum, use a high value (i.e. 2000) to get more stable hashrate output" << endl
@ -619,14 +634,6 @@ public:
#if ETH_JSONRPC || !ETH_TRUE
<< " --phone-home <on/off> When benchmarking, publish results (default: off)" << endl
#endif
// << "DAG file management:" << endl
// << " -D,--create-dag <number> Create the DAG in preparation for mining on given block and exit." << endl
// << " -R <s>, --dag-dir <s> Store/Load DAG files in/from the specified directory. Useful for running multiple instances with different configurations." << endl
// << " -E <mode>, --erase-dags <mode> Erase unneeded DAG files. Default is 'none'. Possible values are:" << endl
// << " none - don't erase DAG files (default)" << endl
// << " old - erase all DAG files older than current epoch" << endl
// << " bench - like old, but keep epoch 0 for benchmarking" << endl
// << " all - erase all DAG files. After deleting all files, setting changes to none." << endl
<< "Mining configuration:" << endl
<< " -C,--cpu When mining, use the CPU." << endl
<< " -G,--opencl When mining use the GPU via OpenCL." << endl
@ -915,20 +922,7 @@ private:
Json::Value v = prpc->eth_getWork();
h256 hh(v[0].asString());
h256 newSeedHash(v[1].asString());
/*
if (current.seedHash != newSeedHash)
{
minelog << "Grabbing DAG for" << newSeedHash;
}
if (!(dag = EthashAux::full(newSeedHash, true, [&](unsigned _pc){ cout << "\rCreating DAG. " << _pc << "% done..." << flush; return 0; })))
{
BOOST_THROW_EXCEPTION(DAGCreationFailure());
}
if (m_precompute)
{
EthashAux::computeFull(sha3(newSeedHash), true);
}
*/
if (hh != current.headerHash)
{
x_current.lock();
@ -948,11 +942,6 @@ private:
}
cnote << "Solution found; Submitting to" << _remote << "...";
cnote << " Nonce:" << solution.nonce.hex();
//cnote << " Mixhash:" << solution.mixHash.hex();
//cnote << " Header-hash:" << current.headerHash.hex();
//cnote << " Seedhash:" << solved.seedHash.hex();
//cnote << " Target: " << h256(solved.boundary).hex();
//cnote << " Ethash: " << h256(EthashAux::eval(solved.seedHash, solved.headerHash, solution.nonce).value).hex();
if (EthashAux::eval(current.seedHash, current.headerHash, solution.nonce).value < current.boundary)
{
bool ok = prpc->eth_submitWork("0x" + toString(solution.nonce), "0x" + toString(current.headerHash), "0x" + toString(solution.mixHash));
@ -1036,39 +1025,79 @@ private:
m_farmRecheckPeriod = m_defaultStratumFarmRecheckPeriod;
GenericFarm<EthashProofOfWork> f;
EthStratumClient client(&f, m_minerType, m_farmURL, m_port, m_user, m_pass, m_maxFarmRetries, m_worktimeout, m_precompute);
if (m_farmFailOverURL != "")
{
if (m_fuser != "")
// this is very ugly, but if Stratum Client V2 tunrs out to be a success, V1 will be completely removed anyway
if (m_stratumClientVersion == 1) {
EthStratumClient client(&f, m_minerType, m_farmURL, m_port, m_user, m_pass, m_maxFarmRetries, m_worktimeout, m_precompute);
if (m_farmFailOverURL != "")
{
client.setFailover(m_farmFailOverURL, m_fport, m_fuser, m_fpass);
if (m_fuser != "")
{
client.setFailover(m_farmFailOverURL, m_fport, m_fuser, m_fpass);
}
else
{
client.setFailover(m_farmFailOverURL, m_fport);
}
}
else
f.setSealers(sealers);
f.onSolutionFound([&](EthashProofOfWork::Solution sol)
{
client.setFailover(m_farmFailOverURL, m_fport);
client.submit(sol);
return false;
});
while (client.isRunning())
{
auto mp = f.miningProgress();
f.resetMiningProgress();
if (client.isConnected())
{
if (client.current())
minelog << "Mining on PoWhash" << "#" + (client.currentHeaderHash().hex().substr(0, 8)) << ": " << mp << f.getSolutionStats();
else if (client.waitState() == MINER_WAIT_STATE_WORK)
minelog << "Waiting for work package...";
}
this_thread::sleep_for(chrono::milliseconds(m_farmRecheckPeriod));
}
}
f.setSealers(sealers);
else if (m_stratumClientVersion == 2) {
EthStratumClientV2 client(&f, m_minerType, m_farmURL, m_port, m_user, m_pass, m_maxFarmRetries, m_worktimeout);
if (m_farmFailOverURL != "")
{
if (m_fuser != "")
{
client.setFailover(m_farmFailOverURL, m_fport, m_fuser, m_fpass);
}
else
{
client.setFailover(m_farmFailOverURL, m_fport);
}
}
f.setSealers(sealers);
f.onSolutionFound([&](EthashProofOfWork::Solution sol)
{
client.submit(sol);
return false;
});
while (client.isRunning())
{
auto mp = f.miningProgress();
f.resetMiningProgress();
if (client.isConnected())
f.onSolutionFound([&](EthashProofOfWork::Solution sol)
{
if (client.current())
minelog << "Mining on PoWhash" << "#"+(client.currentHeaderHash().hex().substr(0,8)) << ": " << mp << f.getSolutionStats();
else if (client.waitState() == MINER_WAIT_STATE_WORK)
minelog << "Waiting for work package...";
client.submit(sol);
return false;
});
while (client.isRunning())
{
auto mp = f.miningProgress();
f.resetMiningProgress();
if (client.isConnected())
{
if (client.current())
minelog << "Mining on PoWhash" << "#" + (client.currentHeaderHash().hex().substr(0, 8)) << ": " << mp << f.getSolutionStats();
else if (client.waitState() == MINER_WAIT_STATE_WORK)
minelog << "Waiting for work package...";
}
this_thread::sleep_for(chrono::milliseconds(m_farmRecheckPeriod));
}
this_thread::sleep_for(chrono::milliseconds(m_farmRecheckPeriod));
}
}
#endif
@ -1129,6 +1158,7 @@ private:
bool m_precompute = true;
#if ETH_STRATUM || !ETH_TRUE
int m_stratumClientVersion = 1;
string m_user;
string m_pass;
string m_port;

4
libethash-cuda/ethash_cuda_miner.cpp

@ -256,8 +256,8 @@ bool ethash_cuda_miner::init(ethash_light_t _light, uint8_t const* _lightData, u
m_sharedBytes = device_props.major * 100 < SHUFFLE_MIN_VER ? (64 * s_blockSize) / 8 : 0 ;
cout << "Generating DAG..." << endl;
ethash_generate_dag(dagSize, s_gridSize, s_blockSize, m_streams[0]);
cout << "Generating DAG for GPU #" << device_num << endl;
ethash_generate_dag(dagSize, s_gridSize, s_blockSize, m_streams[0], device_num);
return true;
}

7
libethash-cuda/ethash_cuda_miner_kernel.cu

@ -114,7 +114,8 @@ void ethash_generate_dag(
uint64_t dag_size,
uint32_t blocks,
uint32_t threads,
cudaStream_t stream
cudaStream_t stream,
int device
)
{
uint32_t const work = (uint32_t)(dag_size / sizeof(hash64_t));
@ -126,9 +127,9 @@ void ethash_generate_dag(
{
ethash_calculate_dag_item <<<blocks, threads, 0, stream >>>(i * blocks * threads);
CUDA_SAFE_CALL(cudaDeviceSynchronize());
printf("%.0f%%\n",100.0f * (float)i / (float)fullRuns);
printf("GPU#%d %.0f%%\r",device, 100.0f * (float)i / (float)fullRuns);
}
//printf("GPU#%d 100%%\n");
CUDA_SAFE_CALL(cudaGetLastError());
}

3
libethash-cuda/ethash_cuda_miner_kernel.h

@ -59,7 +59,8 @@ void ethash_generate_dag(
uint64_t dag_size,
uint32_t blocks,
uint32_t threads,
cudaStream_t stream
cudaStream_t stream,
int device
);

18
libethcore/EthashCUDAMiner.cpp

@ -152,24 +152,6 @@ void EthashCUDAMiner::workLoop()
unsigned device = s_devices[index()] > -1 ? s_devices[index()] : index();
/*
EthashAux::FullType dag;
while (true)
{
if ((dag = EthashAux::full(w.seedHash, true)))
break;
if (shouldStop())
{
delete m_miner;
m_miner = nullptr;
return;
}
cnote << "Awaiting DAG";
this_thread::sleep_for(chrono::milliseconds(500));
}
*/
EthashAux::LightType light;
light = EthashAux::light(w.seedHash);
//bytesConstRef dagData = dag->data();

10
libethcore/Miner.h

@ -24,6 +24,7 @@
#include <thread>
#include <list>
#include <atomic>
#include <string>
#include <boost/timer.hpp>
#include <libdevcore/Common.h>
#include <libdevcore/Log.h>
@ -34,6 +35,15 @@
#define MINER_WAIT_STATE_WORK 1
#define MINER_WAIT_STATE_DAG 2
using namespace std;
typedef struct {
string host;
string port;
string user;
string pass;
} cred_t;
namespace dev
{

16
libstratum/EthStratumClient.cpp

@ -303,22 +303,6 @@ void EthStratumClient::processReponse(Json::Value& responseObject)
h256 seedHash = h256(sSeedHash);
h256 headerHash = h256(sHeaderHash);
EthashAux::FullType dag;
/*
if (seedHash != m_current.seedHash)
{
cnote << "Grabbing DAG for" << seedHash;
}
if (!(dag = EthashAux::full(seedHash, true, [&](unsigned _pc){ m_waitState = _pc < 100 ? MINER_WAIT_STATE_DAG : MINER_WAIT_STATE_WORK; cnote << "Creating DAG. " << _pc << "% done..."; return 0; })))
{
BOOST_THROW_EXCEPTION(DAGCreationFailure());
}
if (m_precompute)
{
EthashAux::computeFull(sha3(seedHash), true);
}
*/
if (headerHash != m_current.headerHash)
{

6
libstratum/EthStratumClient.h

@ -17,12 +17,6 @@ using boost::asio::ip::tcp;
using namespace dev;
using namespace dev::eth;
typedef struct {
string host;
string port;
string user;
string pass;
} cred_t;
class EthStratumClient
{

332
libstratum/EthStratumClientV2.cpp

@ -0,0 +1,332 @@
#include "EthStratumClientV2.h"
#include <libdevcore/Log.h>
using boost::asio::ip::tcp;
EthStratumClientV2::EthStratumClientV2(GenericFarm<EthashProofOfWork> * f, MinerType m, string const & host, string const & port, string const & user, string const & pass, int const & retries, int const & worktimeout)
: Worker("stratum"),
m_socket(m_io_service)
{
m_minerType = m;
m_primary.host = host;
m_primary.port = port;
m_primary.user = user;
m_primary.pass = pass;
p_active = &m_primary;
m_authorized = false;
m_connected = false;
m_maxRetries = retries;
m_worktimeout = worktimeout;
p_farm = f;
p_worktimer = nullptr;
startWorking();
}
EthStratumClientV2::~EthStratumClientV2()
{
}
void EthStratumClientV2::setFailover(string const & host, string const & port)
{
setFailover(host, port, p_active->user, p_active->pass);
}
void EthStratumClientV2::setFailover(string const & host, string const & port, string const & user, string const & pass)
{
m_failover.host = host;
m_failover.port = port;
m_failover.user = user;
m_failover.pass = pass;
}
void EthStratumClientV2::workLoop()
{
while (true)
{
if (!m_connected)
{
m_io_service.run();
connect();
}
read_until(m_socket, m_responseBuffer, "\n");
std::istream is(&m_responseBuffer);
std::string response;
getline(is, response);
if (response.front() == '{' && response.back() == '}')
{
Json::Value responseObject;
Json::Reader reader;
if (reader.parse(response.c_str(), responseObject))
{
processReponse(responseObject);
m_response = response;
}
else
{
cwarn << "Parse response failed: " << reader.getFormattedErrorMessages();
}
}
else
{
cwarn << "Discarding incomplete response";
}
}
}
void EthStratumClientV2::connect()
{
cnote << "Connecting to stratum server " << p_active->host + ":" + p_active->port;
tcp::resolver r(m_io_service);
tcp::resolver::query q(p_active->host, p_active->port);
tcp::resolver::iterator endpoint_iterator = r.resolve(q);
tcp::resolver::iterator end;
boost::system::error_code error = boost::asio::error::host_not_found;
while (error && endpoint_iterator != end)
{
m_socket.close();
m_socket.connect(*endpoint_iterator++, error);
}
if (error)
{
cerr << "Could not connect to stratum server " << p_active->host + ":" + p_active->port + ", " << error.message();
reconnect();
}
else
{
cnote << "Connected!";
m_connected = true;
if (!p_farm->isMining())
{
cnote << "Starting farm";
if (m_minerType == MinerType::CPU)
p_farm->start("cpu");
else if (m_minerType == MinerType::CL)
p_farm->start("opencl");
else if (m_minerType == MinerType::CUDA)
p_farm->start("cuda");
}
std::ostream os(&m_requestBuffer);
os << "{\"id\": 1, \"method\": \"mining.subscribe\", \"params\": []}\n";
write(m_socket, m_requestBuffer);
}
}
#define BOOST_ASIO_ENABLE_CANCELIO
void EthStratumClientV2::reconnect()
{
if (p_worktimer) {
p_worktimer->cancel();
p_worktimer = nullptr;
}
m_io_service.reset();
//m_socket.close(); // leads to crashes on Linux
m_authorized = false;
m_connected = false;
if (!m_failover.host.empty())
{
m_retries++;
if (m_retries > m_maxRetries)
{
if (m_failover.host == "exit") {
disconnect();
return;
}
else if (p_active == &m_primary)
{
p_active = &m_failover;
}
else {
p_active = &m_primary;
}
m_retries = 0;
}
}
cnote << "Reconnecting in 3 seconds...";
boost::asio::deadline_timer timer(m_io_service, boost::posix_time::seconds(3));
timer.wait();
connect();
}
void EthStratumClientV2::disconnect()
{
cnote << "Disconnecting";
m_connected = false;
m_running = false;
if (p_farm->isMining())
{
cnote << "Stopping farm";
p_farm->stop();
}
m_socket.close();
m_io_service.stop();
}
void EthStratumClientV2::processReponse(Json::Value& responseObject)
{
Json::Value error = responseObject.get("error", new Json::Value);
if (error.isArray())
{
string msg = error.get(1, "Unknown error").asString();
cnote << msg;
}
std::ostream os(&m_requestBuffer);
Json::Value params;
int id = responseObject.get("id", Json::Value::null).asInt();
switch (id)
{
case 1:
cnote << "Subscribed to stratum server";
os << "{\"id\": 2, \"method\": \"mining.authorize\", \"params\": [\"" << p_active->user << "\",\"" << p_active->pass << "\"]}\n";
write(m_socket, m_requestBuffer);
break;
case 2:
m_authorized = responseObject.get("result", Json::Value::null).asBool();
if (!m_authorized)
{
cnote << "Worker not authorized:" << p_active->user;
disconnect();
return;
}
cnote << "Authorized worker " << p_active->user;
break;
case 4:
if (responseObject.get("result", false).asBool()) {
cnote << "B-) Submitted and accepted.";
p_farm->acceptedSolution(m_stale);
}
else {
cwarn << ":-( Not accepted.";
p_farm->rejectedSolution(m_stale);
}
break;
default:
string method = responseObject.get("method", "").asString();
if (method == "mining.notify")
{
params = responseObject.get("params", Json::Value::null);
if (params.isArray())
{
string job = params.get((Json::Value::ArrayIndex)0, "").asString();
string sHeaderHash = params.get((Json::Value::ArrayIndex)1, "").asString();
string sSeedHash = params.get((Json::Value::ArrayIndex)2, "").asString();
string sShareTarget = params.get((Json::Value::ArrayIndex)3, "").asString();
//bool cleanJobs = params.get((Json::Value::ArrayIndex)4, "").asBool();
// coinmine.pl fix
int l = sShareTarget.length();
if (l < 66)
sShareTarget = "0x" + string(66 - l, '0') + sShareTarget.substr(2);
if (sHeaderHash != "" && sSeedHash != "" && sShareTarget != "")
{
cnote << "Received new job #" + job.substr(0,8);
//cnote << "Header hash: " + sHeaderHash;
//cnote << "Seed hash: " + sSeedHash;
//cnote << "Share target: " + sShareTarget;
h256 seedHash = h256(sSeedHash);
h256 headerHash = h256(sHeaderHash);
if (headerHash != m_current.headerHash)
{
//x_current.lock();
if (p_worktimer)
p_worktimer->cancel();
m_previous.headerHash = m_current.headerHash;
m_previous.seedHash = m_current.seedHash;
m_previous.boundary = m_current.boundary;
m_previousJob = m_job;
m_current.headerHash = h256(sHeaderHash);
m_current.seedHash = seedHash;
m_current.boundary = h256(sShareTarget);// , h256::AlignRight);
m_job = job;
p_farm->setWork(m_current);
//x_current.unlock();
p_worktimer = new boost::asio::deadline_timer(m_io_service, boost::posix_time::seconds(m_worktimeout));
p_worktimer->async_wait(boost::bind(&EthStratumClientV2::work_timeout_handler, this, boost::asio::placeholders::error));
}
}
}
}
else if (method == "mining.set_difficulty")
{
}
else if (method == "client.get_version")
{
os << "{\"error\": null, \"id\" : " << id << ", \"result\" : \"" << ETH_PROJECT_VERSION << "\"}\n";
write(m_socket, m_requestBuffer);
}
break;
}
}
void EthStratumClientV2::work_timeout_handler(const boost::system::error_code& ec) {
if (!ec) {
cnote << "No new work received in" << m_worktimeout << "seconds.";
reconnect();
}
}
bool EthStratumClientV2::submit(EthashProofOfWork::Solution solution) {
// x_current.lock();
EthashProofOfWork::WorkPackage tempWork(m_current);
string temp_job = m_job;
EthashProofOfWork::WorkPackage tempPreviousWork(m_previous);
string temp_previous_job = m_previousJob;
// x_current.unlock();
cnote << "Solution found; Submitting to" << p_active->host << "...";
cnote << " Nonce:" << "0x" + solution.nonce.hex();
if (EthashAux::eval(tempWork.seedHash, tempWork.headerHash, solution.nonce).value < tempWork.boundary)
{
string json = "{\"id\": 4, \"method\": \"mining.submit\", \"params\": [\"" + p_active->user + "\",\"" + temp_job + "\",\"0x" + solution.nonce.hex() + "\",\"0x" + tempWork.headerHash.hex() + "\",\"0x" + solution.mixHash.hex() + "\"]}\n";
std::ostream os(&m_requestBuffer);
os << json;
m_stale = false;
write(m_socket, m_requestBuffer);
return true;
}
else if (EthashAux::eval(tempPreviousWork.seedHash, tempPreviousWork.headerHash, solution.nonce).value < tempPreviousWork.boundary)
{
string json = "{\"id\": 4, \"method\": \"mining.submit\", \"params\": [\"" + p_active->user + "\",\"" + temp_previous_job + "\",\"0x" + solution.nonce.hex() + "\",\"0x" + tempPreviousWork.headerHash.hex() + "\",\"0x" + solution.mixHash.hex() + "\"]}\n";
std::ostream os(&m_requestBuffer);
os << json;
m_stale = true;
cwarn << "Submitting stale solution.";
write(m_socket, m_requestBuffer);
return true;
}
else {
m_stale = false;
cwarn << "FAILURE: GPU gave incorrect result!";
p_farm->failedSolution();
}
return false;
}

83
libstratum/EthStratumClientV2.h

@ -0,0 +1,83 @@
#include <iostream>
#include <boost/array.hpp>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <json/json.h>
#include <libdevcore/Log.h>
#include <libdevcore/FixedHash.h>
#include <libdevcore/Worker.h>
#include <libethcore/Farm.h>
#include <libethcore/EthashAux.h>
#include <libethcore/Miner.h>
#include "BuildInfo.h"
using namespace std;
using namespace boost::asio;
using boost::asio::ip::tcp;
using namespace dev;
using namespace dev::eth;
class EthStratumClientV2 : public Worker
{
public:
EthStratumClientV2(GenericFarm<EthashProofOfWork> * f, MinerType m, string const & host, string const & port, string const & user, string const & pass, int const & retries, int const & worktimeout);
~EthStratumClientV2();
void setFailover(string const & host, string const & port);
void setFailover(string const & host, string const & port, string const & user, string const & pass);
bool isRunning() { return m_running; }
bool isConnected() { return m_connected; }
h256 currentHeaderHash() { return m_current.headerHash; }
bool current() { return m_current; }
unsigned waitState() { return m_waitState; }
bool submit(EthashProofOfWork::Solution solution);
void reconnect();
private:
void workLoop() override;
void connect();
void disconnect();
void work_timeout_handler(const boost::system::error_code& ec);
void processReponse(Json::Value& responseObject);
MinerType m_minerType;
cred_t * p_active;
cred_t m_primary;
cred_t m_failover;
bool m_authorized;
bool m_connected;
bool m_running = true;
int m_retries = 0;
int m_maxRetries;
int m_worktimeout = 60;
int m_waitState = MINER_WAIT_STATE_WORK;
string m_response;
GenericFarm<EthashProofOfWork> * p_farm;
EthashProofOfWork::WorkPackage m_current;
EthashProofOfWork::WorkPackage m_previous;
bool m_stale = false;
string m_job;
string m_previousJob;
EthashAux::FullType m_dag;
boost::asio::io_service m_io_service;
tcp::socket m_socket;
boost::asio::streambuf m_requestBuffer;
boost::asio::streambuf m_responseBuffer;
boost::asio::deadline_timer * p_worktimer;
};
Loading…
Cancel
Save