passenger 4.0.0.rc6 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of passenger might be problematic. Click here for more details.

Files changed (49) hide show
  1. data.tar.gz.asc +7 -7
  2. data/NEWS +14 -0
  3. data/build/basics.rb +2 -1
  4. data/build/packaging.rb +33 -0
  5. data/dev/run_travis.sh +1 -0
  6. data/doc/Architectural overview.html +1 -3
  7. data/doc/Packaging.txt.md +6 -6
  8. data/doc/Security of user switching support.html +1 -3
  9. data/doc/Users guide Apache.html +53 -20
  10. data/doc/Users guide Apache.idmap.txt +4 -0
  11. data/doc/Users guide Apache.txt +8 -0
  12. data/doc/Users guide Nginx.html +41 -18
  13. data/doc/Users guide Standalone.html +1 -3
  14. data/doc/users_guide_snippets/analysis_and_system_maintenance.txt +11 -5
  15. data/doc/users_guide_snippets/installation.txt +5 -1
  16. data/doc/users_guide_snippets/tips.txt +10 -7
  17. data/doc/users_guide_snippets/under_the_hood/page_caching_support.txt +2 -2
  18. data/ext/apache2/Configuration.cpp +5 -6
  19. data/ext/apache2/Configuration.hpp +0 -4
  20. data/ext/common/ApplicationPool2/Group.h +25 -43
  21. data/ext/common/ApplicationPool2/Implementation.cpp +51 -32
  22. data/ext/common/ApplicationPool2/Pool.h +6 -7
  23. data/ext/common/ApplicationPool2/Process.h +61 -44
  24. data/ext/common/ApplicationPool2/Spawner.h +5 -0
  25. data/ext/common/BackgroundEventLoop.cpp +5 -11
  26. data/ext/common/Constants.h +1 -1
  27. data/ext/common/Utils.cpp +1 -1
  28. data/ext/common/agents/HelperAgent/AgentOptions.h +1 -1
  29. data/ext/common/agents/HelperAgent/RequestHandler.h +2 -0
  30. data/ext/common/agents/LoggingAgent/LoggingServer.h +4 -1
  31. data/ext/common/agents/LoggingAgent/RemoteSender.h +58 -6
  32. data/lib/phusion_passenger.rb +2 -2
  33. data/lib/phusion_passenger/loader_shared_helpers.rb +6 -6
  34. data/lib/phusion_passenger/platform_info/compiler.rb +16 -0
  35. data/lib/phusion_passenger/platform_info/depcheck_specs/apache2.rb +1 -1
  36. data/lib/phusion_passenger/request_handler.rb +1 -0
  37. data/lib/phusion_passenger/standalone/start_command.rb +0 -4
  38. data/lib/phusion_passenger/utils/robust_interruption.rb +47 -28
  39. data/resources/templates/standalone/config.erb +4 -22
  40. data/test/config.json.example +1 -1
  41. data/test/cxx/ApplicationPool2/DirectSpawnerTest.cpp +5 -3
  42. data/test/cxx/ApplicationPool2/PoolTest.cpp +75 -2
  43. data/test/cxx/ApplicationPool2/SmartSpawnerTest.cpp +12 -7
  44. data/test/cxx/ApplicationPool2/SpawnerTestCases.cpp +18 -5
  45. data/test/cxx/RequestHandlerTest.cpp +3 -0
  46. data/test/ruby/shared/loader_sharedspec.rb +4 -0
  47. metadata +5 -6
  48. metadata.gz.asc +7 -7
  49. data/doc/Users guide Apache.index.sqlite3 +0 -0
@@ -428,7 +428,7 @@ public:
428
428
  }
429
429
 
430
430
  void inspectProcessList(const InspectOptions &options, stringstream &result,
431
- const ProcessList &processes) const
431
+ const Group *group, const ProcessList &processes) const
432
432
  {
433
433
  ProcessList::const_iterator p_it;
434
434
  for (p_it = processes.begin(); p_it != processes.end(); p_it++) {
@@ -453,8 +453,7 @@ public:
453
453
  result << " Disabling..." << endl;
454
454
  } else if (process->enabled == Process::DISABLED) {
455
455
  result << " DISABLED" << endl;
456
- }
457
- if (process->getLifeStatus() == Process::SHUTTING_DOWN) {
456
+ } else if (process->enabled == Process::DETACHED) {
458
457
  result << " Shutting down...";
459
458
  }
460
459
 
@@ -1388,10 +1387,10 @@ public:
1388
1387
  result << " (spawning new process...)" << endl;
1389
1388
  }
1390
1389
  result << " Requests in queue: " << group->getWaitlist.size() << endl;
1391
- inspectProcessList(options, result, group->enabledProcesses);
1392
- inspectProcessList(options, result, group->disablingProcesses);
1393
- inspectProcessList(options, result, group->disabledProcesses);
1394
- inspectProcessList(options, result, group->detachedProcesses);
1390
+ inspectProcessList(options, result, group, group->enabledProcesses);
1391
+ inspectProcessList(options, result, group, group->disablingProcesses);
1392
+ inspectProcessList(options, result, group, group->disabledProcesses);
1393
+ inspectProcessList(options, result, group, group->detachedProcesses);
1395
1394
  result << endl;
1396
1395
  }
1397
1396
  }
@@ -128,7 +128,7 @@ class Process: public enable_shared_from_this<Process> {
128
128
  private:
129
129
  friend class Group;
130
130
 
131
- /** A mutex to protect access to `m_shutDown`. */
131
+ /** A mutex to protect access to `lifeStatus`. */
132
132
  mutable boost::mutex lifetimeSyncher;
133
133
 
134
134
  /** Group inside the Pool that this Process belongs to.
@@ -200,9 +200,10 @@ public:
200
200
  * the admin socket need not be closed, etc.
201
201
  */
202
202
  bool dummy;
203
- /** Whether it is required that shutdown() must be called before destroying
204
- * this Process. Normally true, except for dummy Process objects created by
205
- * Pool::asyncGet() with options.noop == true.
203
+ /** Whether it is required that triggerShutdown() and cleanup() must be called
204
+ * before destroying this Process. Normally true, except for dummy Process
205
+ * objects created by Pool::asyncGet() with options.noop == true, because those
206
+ * processes are never added to Group.enabledProcesses.
206
207
  */
207
208
  bool requiresShutdown;
208
209
 
@@ -224,19 +225,21 @@ public:
224
225
  int sessions;
225
226
  /** Number of sessions opened so far. */
226
227
  unsigned int processed;
227
- /** Do not access directly, always use `isAlive()`/`isShutDown()`/`getLifeStatus()` or
228
+ /** Do not access directly, always use `isAlive()`/`isDead()`/`getLifeStatus()` or
228
229
  * through `lifetimeSyncher`. */
229
230
  enum LifeStatus {
230
231
  /** Up and operational. */
231
232
  ALIVE,
232
- /** Being shut down. The containing Group has just detached this
233
- * Process and is now waiting for it to be shutdownable.
234
- */
235
- SHUTTING_DOWN,
233
+ /** This process has been detached, and the detached processes checker has
234
+ * verified that there are no active sessions left and has told the process
235
+ * to shut down. In this state we're supposed to wait until the process
236
+ * has actually shutdown, after which clean() must be called. */
237
+ SHUTDOWN_TRIGGERED,
236
238
  /**
237
- * Shut down. Object no longer usable. No more sessions are active.
239
+ * The process has exited and clean() has been called. In this state,
240
+ * this object is no longer usable.
238
241
  */
239
- SHUT_DOWN
242
+ DEAD
240
243
  } lifeStatus;
241
244
  enum EnabledStatus {
242
245
  /** Up and operational. */
@@ -250,7 +253,14 @@ public:
250
253
  * requests. It *may* still handle some requests, e.g. by
251
254
  * the Out-of-Band-Work trigger.
252
255
  */
253
- DISABLED
256
+ DISABLED,
257
+ /**
258
+ * Process has been detached. It will be removed from the Group
259
+ * as soon we have detected that the OS process has exited. Detached
260
+ * processes are allowed to finish their requests, but are not
261
+ * eligible for new requests.
262
+ */
263
+ DETACHED
254
264
  } enabled;
255
265
  enum OobwStatus {
256
266
  /** Process is not using out-of-band work. */
@@ -330,15 +340,19 @@ public:
330
340
  }
331
341
 
332
342
  ~Process() {
333
- if (OXT_UNLIKELY(!isShutDown() && requiresShutdown)) {
334
- P_BUG("You must call Process::shutdown() before actually "
343
+ if (OXT_UNLIKELY(!isDead() && requiresShutdown)) {
344
+ P_BUG("You must call Process::triggerShutdown() and Process::cleanup() before actually "
335
345
  "destroying the Process object.");
336
346
  }
337
347
  }
338
348
 
339
- static void maybeShutdown(ProcessPtr process) {
349
+ static void forceTriggerShutdownAndCleanup(ProcessPtr process) {
340
350
  if (process != NULL) {
341
- process->shutdown();
351
+ process->triggerShutdown();
352
+ // Pretend like the OS process has exited so
353
+ // that the canCleanup() precondition is true.
354
+ process->m_osProcessExists = false;
355
+ process->cleanup();
342
356
  }
343
357
  }
344
358
 
@@ -348,7 +362,7 @@ public:
348
362
  * @post result != NULL
349
363
  */
350
364
  const GroupPtr getGroup() const {
351
- assert(!isShutDown());
365
+ assert(!isDead());
352
366
  return group.lock();
353
367
  }
354
368
 
@@ -359,7 +373,7 @@ public:
359
373
 
360
374
  /**
361
375
  * Thread-safe.
362
- * @pre getLifeState() != SHUT_DOWN
376
+ * @pre getLifeState() != DEAD
363
377
  * @post result != NULL
364
378
  */
365
379
  SuperGroupPtr getSuperGroup() const;
@@ -371,9 +385,15 @@ public:
371
385
  }
372
386
 
373
387
  // Thread-safe.
374
- bool isShutDown() const {
388
+ bool hasTriggeredShutdown() const {
375
389
  lock_guard<boost::mutex> lock(lifetimeSyncher);
376
- return lifeStatus == SHUT_DOWN;
390
+ return lifeStatus == SHUTDOWN_TRIGGERED;
391
+ }
392
+
393
+ // Thread-safe.
394
+ bool isDead() const {
395
+ lock_guard<boost::mutex> lock(lifetimeSyncher);
396
+ return lifeStatus == DEAD;
377
397
  }
378
398
 
379
399
  // Thread-safe.
@@ -382,32 +402,30 @@ public:
382
402
  return lifeStatus;
383
403
  }
384
404
 
385
- void setShuttingDown() {
405
+ bool canTriggerShutdown() const {
406
+ return getLifeStatus() == ALIVE && sessions == 0;
407
+ }
408
+
409
+ void triggerShutdown() {
410
+ assert(canTriggerShutdown());
386
411
  {
387
412
  lock_guard<boost::mutex> lock(lifetimeSyncher);
388
413
  assert(lifeStatus == ALIVE);
389
- lifeStatus = SHUTTING_DOWN;
414
+ lifeStatus = SHUTDOWN_TRIGGERED;
390
415
  }
391
416
  if (!dummy) {
392
417
  syscalls::shutdown(adminSocket, SHUT_WR);
393
418
  }
394
419
  }
395
420
 
396
- void shutdown() {
397
- LifeStatus ls = getLifeStatus();
398
- if (ls == SHUT_DOWN || !requiresShutdown) {
399
- // Some code have guards that call process->shutdown().
400
- // Returning instead of enforcing !isShutdown() makes things easier.
401
- return;
402
- }
403
-
404
- assert(sessions == 0);
421
+ bool canCleanup() const {
422
+ return getLifeStatus() == SHUTDOWN_TRIGGERED && !osProcessExists();
423
+ }
405
424
 
406
- if (ls == ALIVE) {
407
- setShuttingDown();
408
- }
425
+ void cleanup() {
426
+ assert(canCleanup());
409
427
 
410
- P_TRACE(2, "Shutting down Process object " << inspect());
428
+ P_TRACE(2, "Cleaning up process " << inspect());
411
429
  if (!dummy) {
412
430
  if (OXT_LIKELY(sockets != NULL)) {
413
431
  SocketList::const_iterator it, end = sockets->end();
@@ -421,11 +439,7 @@ public:
421
439
  }
422
440
 
423
441
  lock_guard<boost::mutex> lock(lifetimeSyncher);
424
- lifeStatus = SHUT_DOWN;
425
- }
426
-
427
- bool canBeShutDown() const {
428
- return sessions == 0 && !osProcessExists();
442
+ lifeStatus = DEAD;
429
443
  }
430
444
 
431
445
  /** Checks whether the OS process exists.
@@ -535,11 +549,11 @@ public:
535
549
  case ALIVE:
536
550
  stream << "<life_status>ALIVE</life_status>";
537
551
  break;
538
- case SHUTTING_DOWN:
539
- stream << "<life_status>SHUTTING_DOWN</life_status>";
552
+ case SHUTDOWN_TRIGGERED:
553
+ stream << "<life_status>SHUTDOWN_TRIGGERED</life_status>";
540
554
  break;
541
- case SHUT_DOWN:
542
- stream << "<life_status>SHUT_DOWN</life_status>";
555
+ case DEAD:
556
+ stream << "<life_status>DEAD</life_status>";
543
557
  break;
544
558
  default:
545
559
  P_BUG("Unknown 'lifeStatus' state " << (int) lifeStatus);
@@ -554,6 +568,9 @@ public:
554
568
  case DISABLED:
555
569
  stream << "<enabled>DISABLED</enabled>";
556
570
  break;
571
+ case DETACHED:
572
+ stream << "<enabled>DETACHED</enabled>";
573
+ break;
557
574
  default:
558
575
  P_BUG("Unknown 'enabled' state " << (int) enabled);
559
576
  }
@@ -801,6 +801,7 @@ protected:
801
801
  return;
802
802
  }
803
803
 
804
+ UPDATE_TRACE_POINT();
804
805
  string defaultGroup;
805
806
  string startupFile = absolutizePath(options.getStartupFile(), info.appRoot);
806
807
  struct passwd *userInfo = NULL;
@@ -823,6 +824,7 @@ protected:
823
824
  defaultGroup = options.defaultGroup;
824
825
  }
825
826
 
827
+ UPDATE_TRACE_POINT();
826
828
  if (!options.user.empty()) {
827
829
  userInfo = getpwnam(options.user.c_str());
828
830
  } else {
@@ -838,6 +840,7 @@ protected:
838
840
  userInfo = getpwnam(options.defaultUser.c_str());
839
841
  }
840
842
 
843
+ UPDATE_TRACE_POINT();
841
844
  if (!options.group.empty()) {
842
845
  if (options.group == "!STARTUP_FILE!") {
843
846
  struct stat buf;
@@ -857,6 +860,7 @@ protected:
857
860
  groupInfo = getgrnam(defaultGroup.c_str());
858
861
  }
859
862
 
863
+ UPDATE_TRACE_POINT();
860
864
  if (userInfo == NULL) {
861
865
  throw RuntimeException("Cannot determine a user to lower privilege to");
862
866
  }
@@ -864,6 +868,7 @@ protected:
864
868
  throw RuntimeException("Cannot determine a group to lower privilege to");
865
869
  }
866
870
 
871
+ UPDATE_TRACE_POINT();
867
872
  #ifdef __APPLE__
868
873
  int groups[1024];
869
874
  info.ngroups = sizeof(groups) / sizeof(int);
@@ -63,17 +63,7 @@ startBackgroundLoop(BackgroundEventLoop *bg) {
63
63
 
64
64
  BackgroundEventLoop::BackgroundEventLoop(bool scalable) {
65
65
  TRACE_POINT();
66
- loop = ev_loop_new(EVBACKEND_EPOLL);
67
- if (loop == NULL) {
68
- loop = ev_loop_new(EVBACKEND_KQUEUE);
69
- }
70
- if (loop == NULL) {
71
- loop = ev_loop_new(0);
72
- }
73
- if (loop == NULL) {
74
- throw RuntimeException("Cannot create an event loop");
75
- }
76
-
66
+
77
67
  if (scalable) {
78
68
  loop = ev_loop_new(EVBACKEND_KQUEUE);
79
69
  if (loop == NULL) {
@@ -85,6 +75,10 @@ BackgroundEventLoop::BackgroundEventLoop(bool scalable) {
85
75
  } else {
86
76
  loop = ev_loop_new(EVBACKEND_POLL);
87
77
  }
78
+ if (loop == NULL) {
79
+ throw RuntimeException("Cannot create an event loop");
80
+ }
81
+
88
82
  async = (ev_async *) malloc(sizeof(ev_async));
89
83
  async->data = this;
90
84
  ev_async_init(async, signalBackgroundEventLoopExit);
@@ -26,7 +26,7 @@
26
26
  #define _PASSENGER_CONSTANTS_H_
27
27
 
28
28
  /* Don't forget to update lib/phusion_passenger.rb too. */
29
- #define PASSENGER_VERSION "4.0.0.rc6"
29
+ #define PASSENGER_VERSION "4.0.1"
30
30
 
31
31
  #define FEEDBACK_FD 3
32
32
 
@@ -391,7 +391,7 @@ getProcessUsername() {
391
391
  result = (struct passwd *) NULL;
392
392
  }
393
393
 
394
- if (result == (struct passwd *) NULL) {
394
+ if (result == (struct passwd *) NULL || result->pw_name == NULL || result->pw_name[0] == '\0') {
395
395
  snprintf(strings, sizeof(strings), "UID %lld", (long long) getuid());
396
396
  strings[sizeof(strings) - 1] = '\0';
397
397
  return strings;
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * Phusion Passenger - https://www.phusionpassenger.com/
3
- * Copyright (c) 2011, 2012 Phusion
3
+ * Copyright (c) 2011-2013 Phusion
4
4
  *
5
5
  * "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
6
6
  *
@@ -1646,6 +1646,8 @@ private:
1646
1646
  options.loggingAgentAddress = this->options.loggingAgentAddress;
1647
1647
  options.loggingAgentUsername = "logging";
1648
1648
  options.loggingAgentPassword = this->options.loggingAgentPassword;
1649
+ options.defaultUser = this->options.defaultUser;
1650
+ options.defaultGroup = this->options.defaultGroup;
1649
1651
  fillPoolOption(client, options.appGroupName, "PASSENGER_APP_GROUP_NAME");
1650
1652
  fillPoolOption(client, options.appType, "PASSENGER_APP_TYPE");
1651
1653
  fillPoolOption(client, options.environment, "PASSENGER_ENV");
@@ -1129,7 +1129,10 @@ public:
1129
1129
  TransactionMap::const_iterator end = transactions.end();
1130
1130
 
1131
1131
  stream << "Number of clients : " << getClients().size() << "\n";
1132
- stream << "RemoteSender queue: " << remoteSender.queued() << " items\n";
1132
+ stream << "\n";
1133
+
1134
+ stream << "RemoteSender:\n";
1135
+ remoteSender.inspect(stream);
1133
1136
  stream << "\n";
1134
1137
 
1135
1138
  LogSinkCache::const_iterator sit;
@@ -33,12 +33,14 @@
33
33
 
34
34
  #include <boost/shared_ptr.hpp>
35
35
  #include <boost/bind.hpp>
36
+ #include <boost/foreach.hpp>
36
37
  #include <oxt/thread.hpp>
37
38
  #include <string>
38
39
  #include <list>
39
40
 
40
41
  #include <Logging.h>
41
42
  #include <StaticString.h>
43
+ #include <Utils.h>
42
44
  #include <Utils/BlockingQueue.h>
43
45
  #include <Utils/SystemTime.h>
44
46
  #include <Utils/ScopeGuard.h>
@@ -164,6 +166,10 @@ private:
164
166
  }
165
167
  curl_slist_free_all(headers);
166
168
  }
169
+
170
+ string name() const {
171
+ return ip + ":" + toString(port);
172
+ }
167
173
 
168
174
  bool ping() {
169
175
  P_DEBUG("Pinging Union Station gateway " << ip << ":" << port);
@@ -258,12 +264,13 @@ private:
258
264
  BlockingQueue<Item> queue;
259
265
  oxt::thread *thr;
260
266
 
267
+ mutable boost::mutex syncher;
261
268
  list<ServerPtr> servers;
262
269
  time_t nextCheckupTime;
270
+ unsigned int packetsSent, packetsDropped;
263
271
 
264
272
  void threadMain() {
265
273
  ScopeGuard guard(boost::bind(&RemoteSender::freeThreadData, this));
266
- nextCheckupTime = 0;
267
274
 
268
275
  while (true) {
269
276
  Item item;
@@ -292,21 +299,22 @@ private:
292
299
  }
293
300
 
294
301
  bool firstStarted() const {
302
+ lock_guard<boost::mutex> l(syncher);
295
303
  return nextCheckupTime == 0;
296
304
  }
297
305
 
298
306
  void recheckServers() {
299
- P_DEBUG("Rechecking Union Station gateway servers (" << gatewayAddress << ")...");
307
+ P_INFO("Rechecking Union Station gateway servers (" << gatewayAddress << ")...");
300
308
 
301
309
  vector<string> ips;
302
310
  vector<string>::const_iterator it;
311
+ list<ServerPtr> servers;
303
312
  string hostName;
304
313
  bool someServersAreDown = false;
305
314
 
306
315
  ips = resolveHostname(gatewayAddress, gatewayPort);
307
- P_DEBUG(ips.size() << " Union Station gateway servers found");
316
+ P_INFO(ips.size() << " Union Station gateway servers found");
308
317
 
309
- servers.clear();
310
318
  for (it = ips.begin(); it != ips.end(); it++) {
311
319
  ServerPtr server = make_shared<Server>(*it, gatewayAddress, gatewayPort,
312
320
  certificate, &proxyInfo);
@@ -316,7 +324,7 @@ private:
316
324
  someServersAreDown = true;
317
325
  }
318
326
  }
319
- P_DEBUG(servers.size() << " Union Station gateway servers are up");
327
+ P_INFO(servers.size() << " Union Station gateway servers are up");
320
328
 
321
329
  if (servers.empty()) {
322
330
  scheduleNextCheckup(5 * 60);
@@ -325,9 +333,13 @@ private:
325
333
  } else {
326
334
  scheduleNextCheckup(3 * 60 * 60);
327
335
  }
336
+
337
+ lock_guard<boost::mutex> l(syncher);
338
+ this->servers = servers;
328
339
  }
329
340
 
330
341
  void freeThreadData() {
342
+ lock_guard<boost::mutex> l(syncher);
331
343
  servers.clear(); // Invoke destructors inside this thread.
332
344
  }
333
345
 
@@ -345,6 +357,7 @@ private:
345
357
  }
346
358
 
347
359
  unsigned int msecUntilNextCheckup() const {
360
+ lock_guard<boost::mutex> l(syncher);
348
361
  time_t now = SystemTime::get();
349
362
  if (now >= nextCheckupTime) {
350
363
  return 0;
@@ -354,10 +367,12 @@ private:
354
367
  }
355
368
 
356
369
  bool timeForCheckup() const {
370
+ lock_guard<boost::mutex> l(syncher);
357
371
  return SystemTime::get() >= nextCheckupTime;
358
372
  }
359
373
 
360
374
  void sendOut(const Item &item) {
375
+ unique_lock<boost::mutex> l(syncher);
361
376
  bool sent = false;
362
377
  bool someServersWentDown = false;
363
378
 
@@ -365,12 +380,18 @@ private:
365
380
  // Pick first available server and put it on the back of the list
366
381
  // for round-robin load balancing.
367
382
  ServerPtr server = servers.front();
368
- servers.pop_front();
383
+ l.unlock();
369
384
  if (server->send(item)) {
385
+ l.lock();
386
+ servers.pop_front();
370
387
  servers.push_back(server);
371
388
  sent = true;
389
+ packetsSent++;
372
390
  } else {
391
+ l.lock();
392
+ servers.pop_front();
373
393
  someServersWentDown = true;
394
+ packetsDropped++;
374
395
  }
375
396
  }
376
397
 
@@ -386,6 +407,13 @@ private:
386
407
  * effectively dropped until after the next checkup has detected
387
408
  * servers that are up.
388
409
  */
410
+ if (!sent) {
411
+ P_WARN("Dropping Union Station packet because no servers are available: "
412
+ "key=" << item.unionStationKey <<
413
+ ", node=" << item.nodeName <<
414
+ ", category=" << item.category <<
415
+ ", compressedDataSize=" << item.data.size());
416
+ }
389
417
  }
390
418
 
391
419
  bool compress(const StaticString data[], unsigned int count, string &output) {
@@ -443,6 +471,9 @@ public:
443
471
  throw RuntimeException("Invalid Union Station proxy address \"" +
444
472
  proxyAddress + "\": " + e.what());
445
473
  }
474
+ nextCheckupTime = 0;
475
+ packetsSent = 0;
476
+ packetsDropped = 0;
446
477
  thr = new oxt::thread(
447
478
  boost::bind(&RemoteSender::threadMain, this),
448
479
  "RemoteSender thread",
@@ -494,12 +525,33 @@ public:
494
525
 
495
526
  if (!queue.tryAdd(item)) {
496
527
  P_WARN("The Union Station gateway isn't responding quickly enough; dropping packet.");
528
+ lock_guard<boost::mutex> l(syncher);
529
+ packetsDropped++;
497
530
  }
498
531
  }
499
532
 
500
533
  unsigned int queued() const {
501
534
  return queue.size();
502
535
  }
536
+
537
+ template<typename Stream>
538
+ void inspect(Stream &stream) const {
539
+ lock_guard<boost::mutex> l(syncher);
540
+ stream << " Available servers (" << servers.size() << "): ";
541
+ foreach (const ServerPtr server, servers) {
542
+ stream << server->name() << " ";
543
+ }
544
+ stream << "\n";
545
+ stream << " Items in queue: " << queue.size() << "\n";
546
+ stream << " Packet sent out so far: " << packetsSent << "\n";
547
+ stream << " Packet dropped out so far: " << packetsDropped << "\n";
548
+ stream << " Next server checkup time: ";
549
+ if (nextCheckupTime == 0) {
550
+ stream << "not yet scheduled, waiting for first packet\n";
551
+ } else {
552
+ stream << "in " << distanceOfTimeInWords(nextCheckupTime) << "\n";
553
+ }
554
+ }
503
555
  };
504
556
 
505
557