passenger 4.0.21 → 4.0.23

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 (39) hide show
  1. checksums.yaml +8 -8
  2. checksums.yaml.gz.asc +7 -7
  3. data.tar.gz.asc +7 -7
  4. data/NEWS +15 -0
  5. data/Rakefile +4 -2
  6. data/bin/passenger-install-apache2-module +10 -0
  7. data/build/debian.rb +9 -3
  8. data/build/test_basics.rb +2 -1
  9. data/debian.template/rules.template +7 -0
  10. data/doc/Users guide Apache.idmap.txt +65 -59
  11. data/doc/Users guide Apache.txt +169 -164
  12. data/doc/Users guide Standalone.idmap.txt +20 -14
  13. data/doc/Users guide Standalone.txt +38 -17
  14. data/doc/Users guide.txt +6 -6
  15. data/doc/users_guide_snippets/deployment_basics.txt +37 -0
  16. data/doc/users_guide_snippets/installation.txt +45 -26
  17. data/doc/users_guide_snippets/tips.txt +2 -2
  18. data/ext/boost/atomic/atomic.hpp +1 -1
  19. data/ext/common/ApplicationPool2/AppTypes.h +1 -1
  20. data/ext/common/ApplicationPool2/Spawner.h +17 -0
  21. data/ext/common/Constants.h +1 -1
  22. data/ext/common/Utils/Dechunker.h +12 -2
  23. data/ext/common/Utils/ProcessMetricsCollector.h +4 -2
  24. data/ext/common/Utils/VariantMap.h +21 -2
  25. data/ext/common/agents/HelperAgent/RequestHandler.cpp +8 -0
  26. data/ext/common/agents/HelperAgent/RequestHandler.h +13 -2
  27. data/ext/common/agents/Watchdog/Main.cpp +91 -2
  28. data/ext/ruby/extconf.rb +9 -0
  29. data/helper-scripts/meteor-loader.rb +10 -1
  30. data/lib/phusion_passenger.rb +1 -1
  31. data/lib/phusion_passenger/console_text_template.rb +4 -2
  32. data/lib/phusion_passenger/platform_info.rb +1 -1
  33. data/lib/phusion_passenger/platform_info/apache_detector.rb +17 -22
  34. data/test/cxx/DechunkerTest.cpp +34 -0
  35. data/test/cxx/ProcessMetricsCollectorTest.cpp +5 -3
  36. data/test/cxx/RequestHandlerTest.cpp +27 -0
  37. data/test/stub/wsgi/passenger_wsgi.py +16 -0
  38. metadata +3 -2
  39. metadata.gz.asc +7 -7
@@ -93,6 +93,7 @@ struct ProcessMetrics {
93
93
  /** OS X Snow Leopard does not report the VM size correctly, so don't use this. */
94
94
  size_t vmsize;
95
95
  pid_t processGroupId;
96
+ uid_t uid;
96
97
  string command;
97
98
 
98
99
  ProcessMetrics() {
@@ -381,6 +382,7 @@ private:
381
382
  metrics.rss = (size_t) readNextWordAsLongLong(&start);
382
383
  metrics.vmsize = (size_t) readNextWordAsLongLong(&start);
383
384
  metrics.processGroupId = (pid_t) readNextWordAsLongLong(&start);
385
+ metrics.uid = (uid_t) readNextWordAsLongLong(&start);
384
386
  metrics.command = readRestOfLine(start);
385
387
 
386
388
  bool pidAllowed;
@@ -452,9 +454,9 @@ public:
452
454
  const char *command[] = {
453
455
  "ps", "-o",
454
456
  #if defined(sun) || defined(__sun)
455
- "pid,ppid,pcpu,rss,vsz,pgid,args",
457
+ "pid,ppid,pcpu,rss,vsz,pgid,uid,args",
456
458
  #else
457
- "pid,ppid,%cpu,rss,vsize,pgid,command",
459
+ "pid,ppid,%cpu,rss,vsize,pgid,uid,command",
458
460
  #endif
459
461
  #ifdef PS_SUPPORTS_MULTIPLE_PIDS
460
462
  pidsArg.c_str(),
@@ -110,7 +110,10 @@ public:
110
110
  return key;
111
111
  }
112
112
  };
113
-
113
+
114
+ typedef map<string, string>::iterator Iterator;
115
+ typedef map<string, string>::const_iterator ConstIterator;
116
+
114
117
  /**
115
118
  * Populates a VariantMap from the data in <em>argv</em>, which
116
119
  * consists of <em>argc</em> elements.
@@ -413,7 +416,23 @@ public:
413
416
  }
414
417
  writeArrayMessage(fd, args);
415
418
  }
416
-
419
+
420
+ Iterator begin() {
421
+ return store.begin();
422
+ }
423
+
424
+ ConstIterator begin() const {
425
+ return store.begin();
426
+ }
427
+
428
+ Iterator end() {
429
+ return store.end();
430
+ }
431
+
432
+ ConstIterator end() const {
433
+ return store.end();
434
+ }
435
+
417
436
  string inspect() const {
418
437
  map<string, string>::const_iterator it;
419
438
  map<string, string>::const_iterator end = store.end();
@@ -161,6 +161,14 @@ Client::onAppInputChunk(const char *data, size_t size, void *userData) {
161
161
  client->requestHandler->onAppInputChunk(client->shared_from_this(), StaticString(data, size));
162
162
  }
163
163
 
164
+ void
165
+ Client::onAppInputChunkEnd(void *userData) {
166
+ Client *client = (Client *) userData;
167
+ assert(client != NULL);
168
+ assert(client->requestHandler != NULL);
169
+ client->requestHandler->onAppInputChunkEnd(client->shared_from_this());
170
+ }
171
+
164
172
  void
165
173
  Client::onAppInputError(const EventedBufferedInputPtr &source, const char *message, int errnoCode) {
166
174
  Client *client = (Client *) source->userData;
@@ -161,6 +161,7 @@ private:
161
161
 
162
162
  static size_t onAppInputData(const EventedBufferedInputPtr &source, const StaticString &data);
163
163
  static void onAppInputChunk(const char *data, size_t size, void *userData);
164
+ static void onAppInputChunkEnd(void *userData);
164
165
  static void onAppInputError(const EventedBufferedInputPtr &source, const char *message, int errnoCode);
165
166
 
166
167
  void onAppOutputWritable(ev::io &io, int revents);
@@ -326,6 +327,7 @@ public:
326
327
 
327
328
 
328
329
  responseDechunker.onData = onAppInputChunk;
330
+ responseDechunker.onEnd = onAppInputChunkEnd;
329
331
  responseDechunker.userData = this;
330
332
 
331
333
 
@@ -466,6 +468,8 @@ public:
466
468
  }
467
469
 
468
470
  bool shouldHalfCloseWrite() const {
471
+ // Many broken HTTP servers consider a half close to be a full close, so don't
472
+ // half close HTTP sessions.
469
473
  return session->getProtocol() == "session";
470
474
  }
471
475
 
@@ -1130,9 +1134,16 @@ private:
1130
1134
  writeToClientOutputPipe(client, data);
1131
1135
  }
1132
1136
 
1137
+ void onAppInputChunkEnd(const ClientPtr &client) {
1138
+ RH_LOG_EVENT(client, "onAppInputChunkEnd");
1139
+ onAppInputEof(client);
1140
+ }
1141
+
1133
1142
  void onAppInputEof(const ClientPtr &client) {
1134
1143
  RH_LOG_EVENT(client, "onAppInputEof");
1135
- if (!client->connected()) {
1144
+ // Check for session == NULL in order to avoid executing the code twice on
1145
+ // responses with chunked encoding.
1146
+ if (!client->connected() || client->session == NULL) {
1136
1147
  return;
1137
1148
  }
1138
1149
 
@@ -2200,7 +2211,7 @@ private:
2200
2211
 
2201
2212
  /******* State: FORWARDING_BODY_TO_APP *******/
2202
2213
 
2203
- void state_forwardingBodyToApp_verifyInvariants(const ClientPtr &client) {
2214
+ void state_forwardingBodyToApp_verifyInvariants(const ClientPtr &client) const {
2204
2215
  assert(client->state == Client::FORWARDING_BODY_TO_APP);
2205
2216
  }
2206
2217
 
@@ -30,6 +30,8 @@
30
30
  #include <boost/foreach.hpp>
31
31
  #include <boost/enable_shared_from_this.hpp>
32
32
  #include <string>
33
+ #include <utility>
34
+ #include <vector>
33
35
 
34
36
  #include <sys/select.h>
35
37
  #include <sys/types.h>
@@ -425,6 +427,85 @@ inferDefaultGroup(const string &defaultUser) {
425
427
  return getGroupName(userEntry->pw_gid);
426
428
  }
427
429
 
430
+ static vector< pair<string, string> >
431
+ agentsOptionsToEnvVars(const VariantMap &agentsOptions) {
432
+ vector< pair<string, string> > result;
433
+ VariantMap::ConstIterator it, end = agentsOptions.end();
434
+
435
+ result.reserve(agentsOptions.size());
436
+ for (it = agentsOptions.begin(); it != end; it++) {
437
+ result.push_back(make_pair("passenger_" + it->first, it->second));
438
+ }
439
+
440
+ return result;
441
+ }
442
+
443
+ static void
444
+ setEnvVarsFromVector(const vector< pair<string, string> > &envvars) {
445
+ vector< pair<string, string> >::const_iterator it;
446
+
447
+ for (it = envvars.begin(); it != envvars.end(); it++) {
448
+ setenv(it->first.c_str(), it->second.c_str(), 1);
449
+ }
450
+ }
451
+
452
+ static bool
453
+ runHookScript(const char *name) {
454
+ TRACE_POINT();
455
+ string value = agentsOptions.get(string("hook_") + name, false);
456
+ if (value.empty()) {
457
+ return true;
458
+ }
459
+
460
+ vector< pair<string, string> > envvars = agentsOptionsToEnvVars(agentsOptions);
461
+ pid_t pid;
462
+ int e, status;
463
+
464
+ P_INFO("Running " << name << " hook script: " << value);
465
+
466
+ pid = fork();
467
+ if (pid == 0) {
468
+ resetSignalHandlersAndMask();
469
+ disableMallocDebugging();
470
+ closeAllFileDescriptors(2);
471
+ setEnvVarsFromVector(envvars);
472
+
473
+ execlp(value.c_str(), value.c_str(), (const char * const) 0);
474
+ e = errno;
475
+ fprintf(stderr, "*** ERROR: Cannot execute %s hook script %s: %s (errno=%d)\n",
476
+ name, value.c_str(), strerror(e), e);
477
+ fflush(stderr);
478
+ _exit(1);
479
+ return true; // Never reached.
480
+
481
+ } else if (pid == -1) {
482
+ e = errno;
483
+ P_ERROR("Cannot fork a process for hook script " << value <<
484
+ ": " << strerror(e) << " (errno=" << e << ")");
485
+ return false;
486
+
487
+ } else if (waitpid(pid, &status, 0) == -1) {
488
+ e = errno;
489
+ P_ERROR("Unable to wait for hook script " << value <<
490
+ " (PID " << pid << "): " << strerror(e) << " (errno=" <<
491
+ e << ")");
492
+ return false;
493
+
494
+ } else {
495
+ P_INFO("Hook script " << value << " (PID " << pid <<
496
+ ") exited with status " << WEXITSTATUS(status));
497
+ return WEXITSTATUS(status) == 0;
498
+ }
499
+ }
500
+
501
+ static void
502
+ runHookScriptAndThrowOnError(const char *name) {
503
+ TRACE_POINT();
504
+ if (!runHookScript(name)) {
505
+ throw RuntimeException(string("Hook script ") + name + " failed");
506
+ }
507
+ }
508
+
428
509
  static void
429
510
  initializeBareEssentials(int argc, char *argv[]) {
430
511
  /*
@@ -555,6 +636,7 @@ initializeWorkingObjects(WorkingObjectsPtr &wo, ServerInstanceDirToucherPtr &ser
555
636
  defaultGroup, webServerWorkerUid, webServerWorkerGid);
556
637
  agentsOptions.set("server_instance_dir", wo->serverInstanceDir->getPath());
557
638
  agentsOptions.setInt("generation_number", wo->generation->getNumber());
639
+ agentsOptions.set("generation_path", wo->generation->getPath());
558
640
 
559
641
  UPDATE_TRACE_POINT();
560
642
  serverInstanceDirToucher = boost::make_shared<ServerInstanceDirToucher>(wo);
@@ -660,6 +742,8 @@ main(int argc, char *argv[]) {
660
742
  maybeSetsid();
661
743
  initializeWorkingObjects(wo, serverInstanceDirToucher);
662
744
  initializeAgentWatchers(wo, watchers);
745
+ UPDATE_TRACE_POINT();
746
+ runHookScriptAndThrowOnError("before_watchdog_initialization");
663
747
  } catch (const std::exception &e) {
664
748
  writeArrayMessage(FEEDBACK_FD,
665
749
  "Watchdog startup error",
@@ -675,6 +759,8 @@ main(int argc, char *argv[]) {
675
759
  beginWatchingAgents(wo, watchers);
676
760
  reportAgentsInformation(wo, watchers);
677
761
  P_INFO("All Phusion Passenger agents started!");
762
+ UPDATE_TRACE_POINT();
763
+ runHookScriptAndThrowOnError("after_watchdog_initialization");
678
764
 
679
765
  UPDATE_TRACE_POINT();
680
766
  this_thread::disable_interruption di;
@@ -690,16 +776,19 @@ main(int argc, char *argv[]) {
690
776
  P_DEBUG("Web server did not exit gracefully, forcing shutdown of all agents...");
691
777
  }
692
778
  UPDATE_TRACE_POINT();
779
+ runHookScriptAndThrowOnError("after_watchdog_shutdown");
780
+ UPDATE_TRACE_POINT();
693
781
  AgentWatcher::stopWatching(watchers);
694
782
  if (exitGracefully) {
695
783
  UPDATE_TRACE_POINT();
696
784
  cleanupAgentsInBackground(wo, watchers, argv);
697
- return 0;
698
785
  } else {
699
786
  UPDATE_TRACE_POINT();
700
787
  forceAllAgentsShutdown(watchers);
701
- return 1;
702
788
  }
789
+ UPDATE_TRACE_POINT();
790
+ runHookScriptAndThrowOnError("after_watchdog_shutdown");
791
+ return exitGracefully ? 0 : 1;
703
792
  } catch (const tracable_exception &e) {
704
793
  P_ERROR(e.what() << "\n" << e.backtrace());
705
794
  return 1;
@@ -40,4 +40,13 @@ have_func('rb_thread_io_blocking_region')
40
40
 
41
41
  with_cflags($CFLAGS) do
42
42
  create_makefile('passenger_native_support')
43
+ if RUBY_PLATFORM =~ /solaris/
44
+ # Fix syntax error in Solaris /usr/ccs/bin/make.
45
+ # https://code.google.com/p/phusion-passenger/issues/detail?id=999
46
+ makefile = File.read("Makefile")
47
+ makefile.sub!(/^ECHO = .*/, "ECHO = echo")
48
+ File.open("Makefile", "w") do |f|
49
+ f.write(makefile)
50
+ end
51
+ end
43
52
  end
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # encoding: binary
3
3
  # Phusion Passenger - https://www.phusionpassenger.com/
4
- # Copyright (c) 2013 Phusion
4
+ # Copyright (c) 2010-2013 Phusion
5
5
  #
6
6
  # "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
7
7
  #
@@ -93,6 +93,13 @@ module App
93
93
 
94
94
  production = options["environment"] == "production" ? "production" : ""
95
95
  pid = fork do
96
+ # Meteor is quite !@#$% here: if we kill its start script
97
+ # with *any* signal, it'll leave a ton of garbage processes
98
+ # around. Apparently it expects the user to press Ctrl-C in a
99
+ # terminal which happens to send a signal to all processes
100
+ # in the session. We emulate that behavior here by giving
101
+ # Meteor its own process group, and sending signals to the
102
+ # entire process group.
96
103
  Process.setpgrp
97
104
  exec("meteor run -p #{port} #{production}")
98
105
  end
@@ -113,6 +120,7 @@ module App
113
120
  end
114
121
  puts "!> Ready"
115
122
  puts "!> socket: main;tcp://127.0.0.1:#{port};http_session;0"
123
+ puts "!> pid: #{pid}"
116
124
  puts "!> "
117
125
  begin
118
126
  STDIN.readline
@@ -121,6 +129,7 @@ module App
121
129
  ensure
122
130
  Process.kill('INT', -pid) rescue nil
123
131
  Process.waitpid(pid) rescue nil
132
+ Process.kill('INT', -pid) rescue nil
124
133
  end
125
134
 
126
135
  end # module App
@@ -30,7 +30,7 @@ module PhusionPassenger
30
30
 
31
31
  PACKAGE_NAME = 'passenger'
32
32
  # Run 'rake ext/common/Constants.h' after changing this number.
33
- VERSION_STRING = '4.0.21'
33
+ VERSION_STRING = '4.0.23'
34
34
 
35
35
  PREFERRED_NGINX_VERSION = '1.4.3'
36
36
  NGINX_SHA256_CHECKSUM = 'ae123885c923a6c3f5bab0a8b7296ef21c4fdf6087834667ebbc16338177de84'
@@ -1,5 +1,5 @@
1
1
  # Phusion Passenger - https://www.phusionpassenger.com/
2
- # Copyright (c) 2010 Phusion
2
+ # Copyright (c) 2010-2013 Phusion
3
3
  #
4
4
  # "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
5
5
  #
@@ -30,12 +30,14 @@ class ConsoleTextTemplate
30
30
  def initialize(input, options = {})
31
31
  @buffer = ''
32
32
  if input[:file]
33
- data = File.read("#{PhusionPassenger.resources_dir}/templates/#{input[:file]}.txt.erb")
33
+ filename = "#{PhusionPassenger.resources_dir}/templates/#{input[:file]}.txt.erb"
34
+ data = File.read(filename)
34
35
  else
35
36
  data = input[:text]
36
37
  end
37
38
  @template = ERB.new(Utils::AnsiColors.ansi_colorize(data),
38
39
  nil, '-', '@buffer')
40
+ @template.filename = filename if filename
39
41
  options.each_pair do |name, value|
40
42
  self[name] = value
41
43
  end
@@ -360,7 +360,7 @@ public
360
360
  def self.find_all_commands(name)
361
361
  search_dirs = ENV['PATH'].to_s.split(File::PATH_SEPARATOR)
362
362
  search_dirs.concat(%w(/bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin))
363
- ["/opt/*/bin", "/usr/local/*/bin"].each do |glob|
363
+ ["/opt/*/bin", "/opt/*/sbin", "/usr/local/*/bin", "/usr/local/*/sbin"].each do |glob|
364
364
  search_dirs.concat(Dir[glob])
365
365
  end
366
366
  search_dirs.delete("")
@@ -22,10 +22,12 @@
22
22
  # THE SOFTWARE.
23
23
 
24
24
  require 'phusion_passenger'
25
+ require 'phusion_passenger/constants'
25
26
  require 'phusion_passenger/platform_info'
26
27
  require 'phusion_passenger/platform_info/ruby'
27
28
  require 'phusion_passenger/platform_info/apache'
28
- require 'phusion_passenger/utils/ansi_colors'
29
+ require 'phusion_passenger/utils/ansi_colors'
30
+ require 'pathname'
29
31
 
30
32
  module PhusionPassenger
31
33
  module PlatformInfo
@@ -194,6 +196,8 @@ class ApacheDetector
194
196
  end
195
197
 
196
198
  def result_for(apxs2)
199
+ # All the results use realpaths, so the input must too.
200
+ apxs2 = Pathname.new(apxs2).realpath
197
201
  return @results.find { |r| r.apxs2 == apxs2 }
198
202
  end
199
203
 
@@ -206,30 +210,21 @@ private
206
210
  end
207
211
  end
208
212
 
209
- # On Ubuntu, /usr/bin/apxs2 is a symlink to /usr/bin/apxs. We're only
210
- # supposed to detect one Apache in that case so we need to resolve symlinks.
213
+ # On Ubuntu, /usr/bin/apxs2 is a symlink to /usr/bin/apxs.
214
+ # On recent Arch Linux releases, /bin, /sbin etc are symlinks to
215
+ # /usr/bin and /usr/sbin.
216
+ # We're only supposed to detect one Apache in that case so we need to
217
+ # resolve symlinks.
211
218
  def remove_symlink_duplications(filenames)
212
- result = []
213
- symlink_files = []
214
-
215
- filenames.each do |filename|
216
- if File.symlink?(filename)
217
- symlink_files << filename
218
- else
219
- result << filename
220
- end
219
+ old_size = filenames.size
220
+ filenames = filenames.map do |filename|
221
+ Pathname.new(filename).realpath
221
222
  end
222
-
223
- symlink_files.each do |filename|
224
- full_filename = File.expand_path(File.readlink(filename), File.dirname(filename))
225
- if result.include?(full_filename)
226
- log "#{filename} is a symlink to #{full_filename}. Ignoring it."
227
- else
228
- result << full_filename
229
- end
223
+ filenames.uniq!
224
+ if old_size != filenames.size
225
+ log "#{old_size - filenames.size} symlink duplicate(s) detected; ignoring them."
230
226
  end
231
-
232
- return result
227
+ return filenames
233
228
  end
234
229
 
235
230
  def add_result
@@ -12,10 +12,13 @@ namespace tut {
12
12
  Dechunker dechunker;
13
13
  string input;
14
14
  vector<string> chunks;
15
+ bool ended;
15
16
 
16
17
  DechunkerTest() {
17
18
  dechunker.onData = onData;
19
+ dechunker.onEnd = onEnd;
18
20
  dechunker.userData = this;
21
+ ended = false;
19
22
  }
20
23
 
21
24
  void addChunk(const string &data) {
@@ -29,6 +32,11 @@ namespace tut {
29
32
  DechunkerTest *self = (DechunkerTest *) userData;
30
33
  self->chunks.push_back(string(data, len));
31
34
  }
35
+
36
+ static void onEnd(void *userData) {
37
+ DechunkerTest *self = (DechunkerTest *) userData;
38
+ self->ended = true;
39
+ }
32
40
  };
33
41
 
34
42
  DEFINE_TEST_GROUP(DechunkerTest);
@@ -37,6 +45,7 @@ namespace tut {
37
45
  // Test initial state.
38
46
  ensure(dechunker.acceptingInput());
39
47
  ensure(!dechunker.hasError());
48
+ ensure(!ended);
40
49
  ensure_equals(dechunker.getErrorMessage(), (const char *) NULL);
41
50
  }
42
51
 
@@ -51,6 +60,7 @@ namespace tut {
51
60
  ensure_equals(chunks.size(), 2u);
52
61
  ensure_equals(chunks[0], "hello");
53
62
  ensure_equals(chunks[1], "world");
63
+ ensure(ended);
54
64
  }
55
65
 
56
66
  TEST_METHOD(3) {
@@ -76,6 +86,7 @@ namespace tut {
76
86
  ensure_equals(chunks[2], "l");
77
87
  ensure_equals(chunks[3], "l");
78
88
  ensure_equals(chunks[4], "o");
89
+ ensure(ended);
79
90
  }
80
91
 
81
92
  TEST_METHOD(4) {
@@ -103,6 +114,7 @@ namespace tut {
103
114
  ensure_equals(chunks[3], "w");
104
115
  ensure_equals(chunks[4], "or");
105
116
  ensure_equals(chunks[5], "ld");
117
+ ensure(ended);
106
118
  }
107
119
 
108
120
  TEST_METHOD(5) {
@@ -128,6 +140,7 @@ namespace tut {
128
140
  ensure_equals(chunks[1], "lo");
129
141
  ensure_equals(chunks[2], "wo");
130
142
  ensure_equals(chunks[3], "rld");
143
+ ensure(ended);
131
144
  }
132
145
 
133
146
  TEST_METHOD(6) {
@@ -141,6 +154,7 @@ namespace tut {
141
154
  ensure(!dechunker.hasError());
142
155
  ensure_equals(chunks.size(), 1u);
143
156
  ensure_equals(chunks[0], "xy");
157
+ ensure(ended);
144
158
  }
145
159
 
146
160
  TEST_METHOD(20) {
@@ -158,6 +172,7 @@ namespace tut {
158
172
  ensure_equals(chunks.size(), 2u);
159
173
  ensure_equals(chunks[0], "hello");
160
174
  ensure_equals(chunks[1], "hello");
175
+ ensure(ended);
161
176
  }
162
177
 
163
178
  TEST_METHOD(21) {
@@ -166,6 +181,7 @@ namespace tut {
166
181
  ensure_equals(dechunker.feed(input.data(), input.size()), 2u);
167
182
  ensure(!dechunker.acceptingInput());
168
183
  ensure(dechunker.hasError());
184
+ ensure(!ended);
169
185
  }
170
186
 
171
187
  TEST_METHOD(22) {
@@ -174,6 +190,7 @@ namespace tut {
174
190
  ensure_equals(dechunker.feed(input.data(), input.size()), 3u);
175
191
  ensure(!dechunker.acceptingInput());
176
192
  ensure(dechunker.hasError());
193
+ ensure(!ended);
177
194
  }
178
195
 
179
196
  TEST_METHOD(23) {
@@ -182,6 +199,7 @@ namespace tut {
182
199
  ensure_equals(dechunker.feed(input.data(), input.size()), 7u);
183
200
  ensure(!dechunker.acceptingInput());
184
201
  ensure(dechunker.hasError());
202
+ ensure(!ended);
185
203
  }
186
204
 
187
205
  TEST_METHOD(24) {
@@ -191,6 +209,7 @@ namespace tut {
191
209
  ensure_equals(dechunker.feed(input.data(), input.size()), 5u);
192
210
  ensure(!dechunker.acceptingInput());
193
211
  ensure(dechunker.hasError());
212
+ ensure(!ended);
194
213
  }
195
214
 
196
215
  TEST_METHOD(25) {
@@ -202,6 +221,7 @@ namespace tut {
202
221
  ensure_equals(dechunker.feed(input.data(), input.size()), 11u);
203
222
  ensure(!dechunker.acceptingInput());
204
223
  ensure(dechunker.hasError());
224
+ ensure(!ended);
205
225
  }
206
226
 
207
227
  TEST_METHOD(26) {
@@ -212,5 +232,19 @@ namespace tut {
212
232
  dechunker.feed(input.data(), input.size());
213
233
  ensure(!dechunker.acceptingInput());
214
234
  ensure(dechunker.hasError());
235
+ ensure(!ended);
236
+ }
237
+
238
+ TEST_METHOD(27) {
239
+ // Test feeding a partial stream.
240
+ addChunk("hello");
241
+ addChunk("world");
242
+ ensure_equals(dechunker.feed(input.data(), input.size()), input.size());
243
+ ensure(dechunker.acceptingInput());
244
+ ensure(!dechunker.hasError());
245
+ ensure_equals(chunks.size(), 2u);
246
+ ensure_equals(chunks[0], "hello");
247
+ ensure_equals(chunks[1], "world");
248
+ ensure(!ended);
215
249
  }
216
250
  }