/
Process.h
709 lines (650 loc) · 21.9 KB
/
Process.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
/*
* Phusion Passenger - https://www.phusionpassenger.com/
* Copyright (c) 2011-2014 Phusion
*
* "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
*
* 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
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef _PASSENGER_APPLICATION_POOL_PROCESS_H_
#define _PASSENGER_APPLICATION_POOL_PROCESS_H_
#include <string>
#include <list>
#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>
#include <oxt/system_calls.hpp>
#include <oxt/macros.hpp>
#include <sys/types.h>
#include <cstdio>
#include <climits>
#include <cassert>
#include <ApplicationPool2/Common.h>
#include <ApplicationPool2/Socket.h>
#include <ApplicationPool2/Session.h>
#include <ApplicationPool2/PipeWatcher.h>
#include <Constants.h>
#include <FileDescriptor.h>
#include <SafeLibev.h>
#include <Logging.h>
#include <Utils/PriorityQueue.h>
#include <Utils/SystemTime.h>
#include <Utils/StrIntUtils.h>
#include <Utils/ProcessMetricsCollector.h>
namespace Passenger {
namespace ApplicationPool2 {
using namespace std;
using namespace boost;
class ProcessList: public list<ProcessPtr> {
public:
ProcessPtr &get(unsigned int index) {
iterator it = begin(), end = this->end();
unsigned int i = 0;
while (i != index && it != end) {
i++;
it++;
}
if (it == end) {
throw RuntimeException("Index out of bounds");
} else {
return *it;
}
}
ProcessPtr &operator[](unsigned int index) {
return get(index);
}
iterator last_iterator() {
if (empty()) {
return end();
} else {
iterator last = end();
last--;
return last;
}
}
};
/**
* Represents an application process, as spawned by a Spawner. Every Process has
* a PID, an admin socket and a list of sockets on which it listens for
* connections. A Process is usually contained inside a Group.
*
* The admin socket, an anonymous Unix domain socket, is mapped to the process's
* STDIN and STDOUT and has two functions.
*
* 1. It acts as the main communication channel with the process. Commands are
* sent to and responses are received from it.
* 2. It's used for garbage collection: closing the STDIN part causes the process
* to gracefully terminate itself.
*
* Except for the otherwise documented parts, this class is not thread-safe,
* so only use within the Pool lock.
*
* ## Normal usage
*
* 1. Create a session with newSession().
* 2. Initiate the session by calling initiate() on it.
* 3. Perform I/O through session->fd().
* 4. When done, close the session by calling close() on it.
* 5. Call process.sessionClosed().
*
* ## Life time
*
* A Process object lives until the containing Group calls `detach(process)`,
* which indicates that it wants this Process to shut down. This causes
* `signalDetached()` to be called, which may or may not send a message
* to the process. After this, the Process object is stored in the
* `detachedProcesses` collection in the Group and are no longer eligible for
* receiving requests. Once all requests on this Process have finished,
* `triggerShutdown()` will be called, which will send a message to the
* process telling it to shut down. Once the process is gone, `cleanup()` is
* called, and the Process object is removed from the collection.
*
* This means that a Group outlives all its Processes, a Process outlives all
* its Sessions, and a Process also outlives the OS process.
*/
class Process: public boost::enable_shared_from_this<Process> {
// Actually private, but marked public so that unit tests can access the fields.
public:
friend class Group;
/** A mutex to protect access to `lifeStatus`. */
mutable boost::mutex lifetimeSyncher;
/** Group inside the Pool that this Process belongs to.
* Should never be NULL because a Group should outlive all of its Processes.
* Read-only; only set once during initialization.
*/
boost::weak_ptr<Group> group;
/** A subset of 'sockets': all sockets that speak the
* "session" protocol, sorted by socket.busyness(). */
PriorityQueue<Socket> sessionSockets;
/** The iterator inside the associated Group's process list. */
ProcessList::iterator it;
/** The handle inside the associated Group's process priority queue. */
PriorityQueue<Process>::Handle pqHandle;
void sendAbortLongRunningConnectionsMessage(const string &address);
static void realSendAbortLongRunningConnectionsMessage(string address);
static bool
isZombie(pid_t pid) {
string filename = "/proc/" + toString(pid) + "/status";
FILE *f = fopen(filename.c_str(), "r");
if (f == NULL) {
// Don't know.
return false;
}
bool result = false;
while (!feof(f)) {
char buf[512];
const char *line;
line = fgets(buf, sizeof(buf), f);
if (line == NULL) {
break;
}
if (strcmp(line, "State: Z (zombie)\n") == 0) {
// Is a zombie.
result = true;
break;
}
}
fclose(f);
return result;
}
void indexSessionSockets() {
SocketList::iterator it;
concurrency = 0;
for (it = sockets->begin(); it != sockets->end(); it++) {
Socket *socket = &(*it);
if (socket->protocol == "session" || socket->protocol == "http_session") {
socket->pqHandle = sessionSockets.push(socket, socket->busyness());
if (concurrency != -1) {
if (socket->concurrency == 0) {
// If one of the sockets has a concurrency of
// 0 (unlimited) then we mark this entire Process
// as having a concurrency of 0.
concurrency = -1;
} else {
concurrency += socket->concurrency;
}
}
}
}
if (concurrency == -1) {
concurrency = 0;
}
}
public:
/*************************************************************
* Read-only fields, set once during initialization and never
* written to again. Reading is thread-safe.
*************************************************************/
/** The libev event loop to use. */
SafeLibev * const libev;
/** Process PID. */
pid_t pid;
/** An ID that uniquely identifies this Process in the Group, for
* use in implementing sticky sessions. Set by Group::attach(). */
unsigned int stickySessionId;
/** UUID for this process, randomly generated and extremely unlikely to ever
* appear again in this universe. */
string gupid;
string connectPassword;
/** Admin socket, see class description. */
FileDescriptor adminSocket;
/** The sockets that this Process listens on for connections. */
SocketListPtr sockets;
/** Time at which the Spawner that created this process was created.
* Microseconds resolution. */
unsigned long long spawnerCreationTime;
/** Time at which we started spawning this process. Microseconds resolution. */
unsigned long long spawnStartTime;
/** The maximum amount of concurrent sessions this process can handle.
* 0 means unlimited. */
int concurrency;
/** If true, then indicates that this Process does not refer to a real OS
* process. The sockets in the socket list are fake and need not be deleted,
* the admin socket need not be closed, etc.
*/
bool dummy;
/** Whether it is required that triggerShutdown() and cleanup() must be called
* before destroying this Process. Normally true, except for dummy Process
* objects created by Pool::asyncGet() with options.noop == true, because those
* processes are never added to Group.enabledProcesses.
*/
bool requiresShutdown;
/*************************************************************
* Information used by Pool. Do not write to these from
* outside the Pool. If you read these make sure the Pool
* isn't concurrently modifying.
*************************************************************/
/** Time at which we finished spawning this process, i.e. when this
* process was finished initializing. Microseconds resolution.
*/
unsigned long long spawnEndTime;
/** Last time when a session was opened for this Process. */
unsigned long long lastUsed;
/** Number of sessions currently open.
* @invariant session >= 0
*/
int sessions;
/** Number of sessions opened so far. */
unsigned int processed;
/** Do not access directly, always use `isAlive()`/`isDead()`/`getLifeStatus()` or
* through `lifetimeSyncher`. */
enum LifeStatus {
/** Up and operational. */
ALIVE,
/** This process has been detached, and the detached processes checker has
* verified that there are no active sessions left and has told the process
* to shut down. In this state we're supposed to wait until the process
* has actually shutdown, after which cleanup() must be called. */
SHUTDOWN_TRIGGERED,
/**
* The process has exited and cleanup() has been called. In this state,
* this object is no longer usable.
*/
DEAD
} lifeStatus;
enum EnabledStatus {
/** Up and operational. */
ENABLED,
/** Process is being disabled. The containing Group is waiting for
* all sessions on this Process to finish. It may in some corner
* cases still be selected for processing requests.
*/
DISABLING,
/** Process is fully disabled and should not be handling any
* requests. It *may* still handle some requests, e.g. by
* the Out-of-Band-Work trigger.
*/
DISABLED,
/**
* Process has been detached. It will be removed from the Group
* as soon we have detected that the OS process has exited. Detached
* processes are allowed to finish their requests, but are not
* eligible for new requests.
*/
DETACHED
} enabled;
enum OobwStatus {
/** Process is not using out-of-band work. */
OOBW_NOT_ACTIVE,
/** The process has requested out-of-band work. At some point, the code
* will see this and set the status to OOBW_IN_PROGRESS. */
OOBW_REQUESTED,
/** An out-of-band work is in progress. We need to wait until all
* sessions have ended and the process has been disabled before the
* out-of-band work can be performed. */
OOBW_IN_PROGRESS,
} oobwStatus;
/** Caches whether or not the OS process still exists. */
mutable bool m_osProcessExists;
bool longRunningConnectionsAborted;
/** Time at which shutdown began. */
time_t shutdownStartTime;
/** Collected by Pool::collectAnalytics(). */
ProcessMetrics metrics;
Process(const SafeLibevPtr _libev,
pid_t _pid,
const string &_gupid,
const string &_connectPassword,
const FileDescriptor &_adminSocket,
/** Pipe on which this process outputs errors. Mapped to the process's STDERR.
* Only Processes spawned by DirectSpawner have this set.
* SmartSpawner-spawned Processes use the same STDERR as their parent preloader processes.
*/
const FileDescriptor &_errorPipe,
const SocketListPtr &_sockets,
unsigned long long _spawnerCreationTime,
unsigned long long _spawnStartTime,
const SpawnerConfigPtr &_config = SpawnerConfigPtr())
: pqHandle(NULL),
libev(_libev.get()),
pid(_pid),
stickySessionId(0),
gupid(_gupid),
connectPassword(_connectPassword),
adminSocket(_adminSocket),
sockets(_sockets),
spawnerCreationTime(_spawnerCreationTime),
spawnStartTime(_spawnStartTime),
concurrency(0),
dummy(false),
requiresShutdown(true),
sessions(0),
processed(0),
lifeStatus(ALIVE),
enabled(ENABLED),
oobwStatus(OOBW_NOT_ACTIVE),
m_osProcessExists(true),
longRunningConnectionsAborted(false),
shutdownStartTime(0)
{
SpawnerConfigPtr config;
if (_config == NULL) {
config = boost::make_shared<SpawnerConfig>();
} else {
config = _config;
}
if (_adminSocket != -1) {
PipeWatcherPtr watcher = boost::make_shared<PipeWatcher>(_adminSocket,
"stdout", pid);
watcher->initialize();
watcher->start();
}
if (_errorPipe != -1) {
PipeWatcherPtr watcher = boost::make_shared<PipeWatcher>(_errorPipe,
"stderr", pid);
watcher->initialize();
watcher->start();
}
if (OXT_LIKELY(sockets != NULL)) {
indexSessionSockets();
}
lastUsed = SystemTime::getUsec();
spawnEndTime = lastUsed;
}
~Process() {
if (OXT_UNLIKELY(!isDead() && requiresShutdown)) {
P_BUG("You must call Process::triggerShutdown() and Process::cleanup() before actually "
"destroying the Process object.");
}
}
static void forceTriggerShutdownAndCleanup(ProcessPtr process) {
if (process != NULL) {
process->triggerShutdown();
// Pretend like the OS process has exited so
// that the canCleanup() precondition is true.
process->m_osProcessExists = false;
process->cleanup();
}
}
/**
* Thread-safe.
* @pre getLifeState() != SHUT_DOWN
* @post result != NULL
*/
const GroupPtr getGroup() const {
assert(!isDead());
return group.lock();
}
void setGroup(const GroupPtr &group) {
assert(this->group.lock() == NULL || this->group.lock() == group);
this->group = group;
}
/**
* Thread-safe.
* @pre getLifeState() != DEAD
* @post result != NULL
*/
PoolPtr getPool() const;
/**
* Thread-safe.
* @pre getLifeState() != DEAD
* @post result != NULL
*/
SuperGroupPtr getSuperGroup() const;
// Thread-safe.
bool isAlive() const {
boost::lock_guard<boost::mutex> lock(lifetimeSyncher);
return lifeStatus == ALIVE;
}
// Thread-safe.
bool hasTriggeredShutdown() const {
boost::lock_guard<boost::mutex> lock(lifetimeSyncher);
return lifeStatus == SHUTDOWN_TRIGGERED;
}
// Thread-safe.
bool isDead() const {
boost::lock_guard<boost::mutex> lock(lifetimeSyncher);
return lifeStatus == DEAD;
}
// Thread-safe.
LifeStatus getLifeStatus() const {
boost::lock_guard<boost::mutex> lock(lifetimeSyncher);
return lifeStatus;
}
bool abortLongRunningConnections() {
bool sent = false;
if (!longRunningConnectionsAborted) {
SocketList::iterator it, end = sockets->end();
for (it = sockets->begin(); it != end; it++) {
Socket *socket = &(*it);
if (socket->name == "control") {
sendAbortLongRunningConnectionsMessage(socket->address);
sent = true;
}
}
longRunningConnectionsAborted = true;
}
return sent;
}
bool canTriggerShutdown() const {
return getLifeStatus() == ALIVE && sessions == 0;
}
void triggerShutdown() {
assert(canTriggerShutdown());
{
boost::lock_guard<boost::mutex> lock(lifetimeSyncher);
assert(lifeStatus == ALIVE);
lifeStatus = SHUTDOWN_TRIGGERED;
shutdownStartTime = SystemTime::get();
}
if (!dummy) {
syscalls::shutdown(adminSocket, SHUT_WR);
}
}
bool shutdownTimeoutExpired() const {
return SystemTime::get() >= shutdownStartTime + PROCESS_SHUTDOWN_TIMEOUT;
}
bool canCleanup() const {
return getLifeStatus() == SHUTDOWN_TRIGGERED && !osProcessExists();
}
void cleanup() {
assert(canCleanup());
P_TRACE(2, "Cleaning up process " << inspect());
if (!dummy) {
if (OXT_LIKELY(sockets != NULL)) {
SocketList::const_iterator it, end = sockets->end();
for (it = sockets->begin(); it != end; it++) {
if (getSocketAddressType(it->address) == SAT_UNIX) {
string filename = parseUnixSocketAddress(it->address);
syscalls::unlink(filename.c_str());
}
}
}
}
boost::lock_guard<boost::mutex> lock(lifetimeSyncher);
lifeStatus = DEAD;
}
/** Checks whether the OS process exists.
* Once it has been detected that it doesn't, that event is remembered
* so that we don't accidentally ping any new processes that have the
* same PID.
*/
bool osProcessExists() const {
if (!dummy && m_osProcessExists) {
if (syscalls::kill(pid, 0) == 0) {
/* On some environments, e.g. Heroku, the init process does
* not properly reap adopted zombie processes, which can interfere
* with our process existance check. To work around this, we
* explicitly check whether or not the process has become a zombie.
*/
m_osProcessExists = !isZombie(pid);
} else {
m_osProcessExists = errno != ESRCH;
}
return m_osProcessExists;
} else {
return false;
}
}
/** Kill the OS process with the given signal. */
int kill(int signo) {
if (osProcessExists()) {
return syscalls::kill(pid, signo);
} else {
return 0;
}
}
int busyness() const {
/* Different processes within a Group may have different
* 'concurrency' values. We want:
* - Group.pqueue to sort the processes from least used to most used.
* - to give processes with concurrency == 0 more priority over processes
* with concurrency > 0.
* Therefore, we describe our busyness as a percentage of 'concurrency', with
* the percentage value in [0..INT_MAX] instead of [0..1].
*/
if (concurrency == 0) {
// Allows Group.pqueue to give idle sockets more priority.
if (sessions == 0) {
return 0;
} else {
return 1;
}
} else {
return (int) (((long long) sessions * INT_MAX) / (double) concurrency);
}
}
/**
* Whether we've reached the maximum number of concurrent sessions for this
* process.
*/
bool isTotallyBusy() const {
return concurrency != 0 && sessions >= concurrency;
}
/**
* Whether a get() request can be routed to this process, assuming that
* the sticky session ID (if any) matches. This is only not the case
* if this process is totally busy.
*/
bool canBeRoutedTo() const {
return !isTotallyBusy();
}
/**
* Create a new communication session with this process. This will connect to one
* of the session sockets or reuse an existing connection. See Session for
* more information about sessions.
*
* One SHOULD call sessionClosed() when one's done with the session.
* Failure to do so will mess up internal statistics but will otherwise
* not result in any harmful behavior.
*/
SessionPtr newSession() {
Socket *socket = sessionSockets.pop();
if (socket->isTotallyBusy()) {
return SessionPtr();
} else {
socket->sessions++;
this->sessions++;
socket->pqHandle = sessionSockets.push(socket, socket->busyness());
lastUsed = SystemTime::getUsec();
return boost::make_shared<Session>(shared_from_this(), socket);
}
}
void sessionClosed(Session *session) {
Socket *socket = session->getSocket();
assert(socket->sessions > 0);
assert(sessions > 0);
socket->sessions--;
this->sessions--;
processed++;
sessionSockets.decrease(socket->pqHandle, socket->busyness());
assert(!isTotallyBusy());
}
/**
* Returns the uptime of this process so far, as a string.
*/
string uptime() const {
return distanceOfTimeInWords(spawnEndTime / 1000000);
}
string inspect() const;
template<typename Stream>
void inspectXml(Stream &stream, bool includeSockets = true) const {
stream << "<pid>" << pid << "</pid>";
stream << "<sticky_session_id>" << stickySessionId << "</sticky_session_id>";
stream << "<gupid>" << gupid << "</gupid>";
stream << "<connect_password>" << connectPassword << "</connect_password>";
stream << "<concurrency>" << concurrency << "</concurrency>";
stream << "<sessions>" << sessions << "</sessions>";
stream << "<busyness>" << busyness() << "</busyness>";
stream << "<processed>" << processed << "</processed>";
stream << "<spawner_creation_time>" << spawnerCreationTime << "</spawner_creation_time>";
stream << "<spawn_start_time>" << spawnStartTime << "</spawn_start_time>";
stream << "<spawn_end_time>" << spawnEndTime << "</spawn_end_time>";
stream << "<last_used>" << lastUsed << "</last_used>";
stream << "<uptime>" << uptime() << "</uptime>";
switch (lifeStatus) {
case ALIVE:
stream << "<life_status>ALIVE</life_status>";
break;
case SHUTDOWN_TRIGGERED:
stream << "<life_status>SHUTDOWN_TRIGGERED</life_status>";
break;
case DEAD:
stream << "<life_status>DEAD</life_status>";
break;
default:
P_BUG("Unknown 'lifeStatus' state " << (int) lifeStatus);
}
switch (enabled) {
case ENABLED:
stream << "<enabled>ENABLED</enabled>";
break;
case DISABLING:
stream << "<enabled>DISABLING</enabled>";
break;
case DISABLED:
stream << "<enabled>DISABLED</enabled>";
break;
case DETACHED:
stream << "<enabled>DETACHED</enabled>";
break;
default:
P_BUG("Unknown 'enabled' state " << (int) enabled);
}
if (metrics.isValid()) {
stream << "<has_metrics>true</has_metrics>";
stream << "<cpu>" << (int) metrics.cpu << "</cpu>";
stream << "<rss>" << metrics.rss << "</rss>";
stream << "<pss>" << metrics.pss << "</pss>";
stream << "<private_dirty>" << metrics.privateDirty << "</private_dirty>";
stream << "<swap>" << metrics.swap << "</swap>";
stream << "<real_memory>" << metrics.realMemory() << "</real_memory>";
stream << "<vmsize>" << metrics.vmsize << "</vmsize>";
stream << "<process_group_id>" << metrics.processGroupId << "</process_group_id>";
stream << "<command>" << escapeForXml(metrics.command) << "</command>";
}
if (includeSockets) {
SocketList::const_iterator it;
stream << "<sockets>";
for (it = sockets->begin(); it != sockets->end(); it++) {
const Socket &socket = *it;
stream << "<socket>";
stream << "<name>" << escapeForXml(socket.name) << "</name>";
stream << "<address>" << escapeForXml(socket.address) << "</address>";
stream << "<protocol>" << escapeForXml(socket.protocol) << "</protocol>";
stream << "<concurrency>" << socket.concurrency << "</concurrency>";
stream << "<sessions>" << socket.sessions << "</sessions>";
stream << "</socket>";
}
stream << "</sockets>";
}
}
};
} // namespace ApplicationPool2
} // namespace Passenger
#endif /* _PASSENGER_APPLICATION_POOL2_PROCESS_H_ */