puma 5.0.0.beta1-java → 5.0.3-java

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

Potentially problematic release.


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

Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1188 -559
  3. data/README.md +15 -8
  4. data/bin/puma-wild +3 -9
  5. data/docs/architecture.md +3 -3
  6. data/docs/deployment.md +10 -7
  7. data/docs/jungle/README.md +0 -4
  8. data/docs/jungle/rc.d/puma +2 -2
  9. data/docs/nginx.md +1 -1
  10. data/docs/restart.md +46 -23
  11. data/docs/signals.md +7 -7
  12. data/docs/systemd.md +1 -1
  13. data/ext/puma_http11/ext_help.h +1 -1
  14. data/ext/puma_http11/http11_parser.c +3 -1
  15. data/ext/puma_http11/http11_parser.rl +3 -1
  16. data/ext/puma_http11/mini_ssl.c +53 -38
  17. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  18. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
  19. data/ext/puma_http11/puma_http11.c +22 -11
  20. data/lib/puma.rb +16 -0
  21. data/lib/puma/app/status.rb +47 -44
  22. data/lib/puma/binder.rb +40 -12
  23. data/lib/puma/client.rb +68 -82
  24. data/lib/puma/cluster.rb +30 -187
  25. data/lib/puma/cluster/worker.rb +170 -0
  26. data/lib/puma/cluster/worker_handle.rb +83 -0
  27. data/lib/puma/commonlogger.rb +2 -2
  28. data/lib/puma/configuration.rb +9 -7
  29. data/lib/puma/const.rb +2 -1
  30. data/lib/puma/control_cli.rb +2 -0
  31. data/lib/puma/detect.rb +9 -0
  32. data/lib/puma/dsl.rb +77 -39
  33. data/lib/puma/error_logger.rb +97 -0
  34. data/lib/puma/events.rb +37 -31
  35. data/lib/puma/launcher.rb +20 -10
  36. data/lib/puma/minissl.rb +55 -10
  37. data/lib/puma/minissl/context_builder.rb +0 -3
  38. data/lib/puma/puma_http11.jar +0 -0
  39. data/lib/puma/queue_close.rb +26 -0
  40. data/lib/puma/reactor.rb +77 -373
  41. data/lib/puma/request.rb +438 -0
  42. data/lib/puma/runner.rb +7 -19
  43. data/lib/puma/server.rb +229 -506
  44. data/lib/puma/single.rb +3 -2
  45. data/lib/puma/state_file.rb +1 -1
  46. data/lib/puma/thread_pool.rb +32 -5
  47. data/lib/puma/util.rb +12 -0
  48. metadata +12 -10
  49. data/docs/jungle/upstart/README.md +0 -61
  50. data/docs/jungle/upstart/puma-manager.conf +0 -31
  51. data/docs/jungle/upstart/puma.conf +0 -69
  52. data/lib/puma/accept_nonblock.rb +0 -29
@@ -0,0 +1,15 @@
1
+ package puma;
2
+
3
+ import java.io.IOException;
4
+
5
+ import org.jruby.Ruby;
6
+ import org.jruby.runtime.load.BasicLibraryService;
7
+
8
+ import org.jruby.puma.Http11;
9
+
10
+ public class PumaHttp11Service implements BasicLibraryService {
11
+ public boolean basicLoad(final Ruby runtime) throws IOException {
12
+ Http11.createHttp11(runtime);
13
+ return true;
14
+ }
15
+ }
@@ -22,6 +22,7 @@ import javax.net.ssl.SSLException;
22
22
  import javax.net.ssl.SSLPeerUnverifiedException;
23
23
  import javax.net.ssl.SSLSession;
24
24
  import java.io.FileInputStream;
25
+ import java.io.InputStream;
25
26
  import java.io.IOException;
26
27
  import java.nio.Buffer;
27
28
  import java.nio.ByteBuffer;
@@ -32,6 +33,8 @@ import java.security.NoSuchAlgorithmException;
32
33
  import java.security.UnrecoverableKeyException;
33
34
  import java.security.cert.CertificateEncodingException;
34
35
  import java.security.cert.CertificateException;
36
+ import java.util.concurrent.ConcurrentHashMap;
37
+ import java.util.Map;
35
38
 
36
39
  import static javax.net.ssl.SSLEngineResult.Status;
37
40
  import static javax.net.ssl.SSLEngineResult.HandshakeStatus;
@@ -120,6 +123,8 @@ public class MiniSSL extends RubyObject {
120
123
  }
121
124
 
122
125
  private SSLEngine engine;
126
+ private boolean closed;
127
+ private boolean handshake;
123
128
  private MiniSSLBuffer inboundNetData;
124
129
  private MiniSSLBuffer outboundAppData;
125
130
  private MiniSSLBuffer outboundNetData;
@@ -128,10 +133,39 @@ public class MiniSSL extends RubyObject {
128
133
  super(runtime, klass);
129
134
  }
130
135
 
136
+ private static Map<String, KeyManagerFactory> keyManagerFactoryMap = new ConcurrentHashMap<String, KeyManagerFactory>();
137
+ private static Map<String, TrustManagerFactory> trustManagerFactoryMap = new ConcurrentHashMap<String, TrustManagerFactory>();
138
+
131
139
  @JRubyMethod(meta = true)
132
- public static IRubyObject server(ThreadContext context, IRubyObject recv, IRubyObject miniSSLContext) {
133
- RubyClass klass = (RubyClass) recv;
140
+ public static synchronized IRubyObject server(ThreadContext context, IRubyObject recv, IRubyObject miniSSLContext)
141
+ throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException {
142
+ // Create the KeyManagerFactory and TrustManagerFactory for this server
143
+ String keystoreFile = miniSSLContext.callMethod(context, "keystore").convertToString().asJavaString();
144
+ char[] password = miniSSLContext.callMethod(context, "keystore_pass").convertToString().asJavaString().toCharArray();
134
145
 
146
+ KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
147
+ InputStream is = new FileInputStream(keystoreFile);
148
+ try {
149
+ ks.load(is, password);
150
+ } finally {
151
+ is.close();
152
+ }
153
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
154
+ kmf.init(ks, password);
155
+ keyManagerFactoryMap.put(keystoreFile, kmf);
156
+
157
+ KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
158
+ is = new FileInputStream(keystoreFile);
159
+ try {
160
+ ts.load(is, password);
161
+ } finally {
162
+ is.close();
163
+ }
164
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
165
+ tmf.init(ts);
166
+ trustManagerFactoryMap.put(keystoreFile, tmf);
167
+
168
+ RubyClass klass = (RubyClass) recv;
135
169
  return klass.newInstance(context,
136
170
  new IRubyObject[] { miniSSLContext },
137
171
  Block.NULL_BLOCK);
@@ -139,24 +173,22 @@ public class MiniSSL extends RubyObject {
139
173
 
140
174
  @JRubyMethod
141
175
  public IRubyObject initialize(ThreadContext threadContext, IRubyObject miniSSLContext)
142
- throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException {
176
+ throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
143
177
  KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
144
178
  KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
145
179
 
146
- char[] password = miniSSLContext.callMethod(threadContext, "keystore_pass").convertToString().asJavaString().toCharArray();
147
180
  String keystoreFile = miniSSLContext.callMethod(threadContext, "keystore").convertToString().asJavaString();
148
- ks.load(new FileInputStream(keystoreFile), password);
149
- ts.load(new FileInputStream(keystoreFile), password);
150
-
151
- KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
152
- kmf.init(ks, password);
153
-
154
- TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
155
- tmf.init(ts);
181
+ KeyManagerFactory kmf = keyManagerFactoryMap.get(keystoreFile);
182
+ TrustManagerFactory tmf = trustManagerFactoryMap.get(keystoreFile);
183
+ if(kmf == null || tmf == null) {
184
+ throw new KeyStoreException("Could not find KeyManagerFactory/TrustManagerFactory for keystore: " + keystoreFile);
185
+ }
156
186
 
157
187
  SSLContext sslCtx = SSLContext.getInstance("TLS");
158
188
 
159
189
  sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
190
+ closed = false;
191
+ handshake = false;
160
192
  engine = sslCtx.createSSLEngine();
161
193
 
162
194
  String[] protocols;
@@ -173,7 +205,7 @@ public class MiniSSL extends RubyObject {
173
205
  engine.setEnabledProtocols(protocols);
174
206
  engine.setUseClientMode(false);
175
207
 
176
- long verify_mode = miniSSLContext.callMethod(threadContext, "verify_mode").convertToInteger().getLongValue();
208
+ long verify_mode = miniSSLContext.callMethod(threadContext, "verify_mode").convertToInteger("to_i").getLongValue();
177
209
  if ((verify_mode & 0x1) != 0) { // 'peer'
178
210
  engine.setWantClientAuth(true);
179
211
  }
@@ -240,14 +272,21 @@ public class MiniSSL extends RubyObject {
240
272
  // need to wait for more data to come in before we retry
241
273
  retryOp = false;
242
274
  break;
275
+ case CLOSED:
276
+ closed = true;
277
+ retryOp = false;
278
+ break;
243
279
  default:
244
- // other cases are OK and CLOSED. We're done here.
280
+ // other case is OK. We're done here.
245
281
  retryOp = false;
246
282
  }
283
+ if (res.getHandshakeStatus() == HandshakeStatus.FINISHED) {
284
+ handshake = true;
285
+ }
247
286
  }
248
287
 
249
288
  // after each op, run any delegated tasks if needed
250
- if(engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
289
+ if(res.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
251
290
  Runnable runnable;
252
291
  while ((runnable = engine.getDelegatedTask()) != null) {
253
292
  runnable.run();
@@ -271,13 +310,14 @@ public class MiniSSL extends RubyObject {
271
310
 
272
311
  HandshakeStatus handshakeStatus = engine.getHandshakeStatus();
273
312
  boolean done = false;
313
+ SSLEngineResult res = null;
274
314
  while (!done) {
275
315
  switch (handshakeStatus) {
276
316
  case NEED_WRAP:
277
- doOp(SSLOperation.WRAP, inboundAppData, outboundNetData);
317
+ res = doOp(SSLOperation.WRAP, inboundAppData, outboundNetData);
278
318
  break;
279
319
  case NEED_UNWRAP:
280
- SSLEngineResult res = doOp(SSLOperation.UNWRAP, inboundNetData, inboundAppData);
320
+ res = doOp(SSLOperation.UNWRAP, inboundNetData, inboundAppData);
281
321
  if (res.getStatus() == Status.BUFFER_UNDERFLOW) {
282
322
  // need more data before we can shake more hands
283
323
  done = true;
@@ -286,7 +326,9 @@ public class MiniSSL extends RubyObject {
286
326
  default:
287
327
  done = true;
288
328
  }
289
- handshakeStatus = engine.getHandshakeStatus();
329
+ if (!done) {
330
+ handshakeStatus = res.getHandshakeStatus();
331
+ }
290
332
  }
291
333
 
292
334
  if (inboundNetData.hasRemaining()) {
@@ -360,4 +402,21 @@ public class MiniSSL extends RubyObject {
360
402
  return getRuntime().getNil();
361
403
  }
362
404
  }
405
+
406
+ @JRubyMethod(name = "init?")
407
+ public IRubyObject isInit(ThreadContext context) {
408
+ return handshake ? getRuntime().getFalse() : getRuntime().getTrue();
409
+ }
410
+
411
+ @JRubyMethod
412
+ public IRubyObject shutdown() {
413
+ if (closed || engine.isInboundDone() && engine.isOutboundDone()) {
414
+ if (engine.isOutboundDone()) {
415
+ engine.closeOutbound();
416
+ }
417
+ return getRuntime().getTrue();
418
+ } else {
419
+ return getRuntime().getFalse();
420
+ }
421
+ }
363
422
  }
@@ -54,7 +54,7 @@ DEF_MAX_LENGTH(FIELD_NAME, 256);
54
54
  DEF_MAX_LENGTH(FIELD_VALUE, 80 * 1024);
55
55
  DEF_MAX_LENGTH(REQUEST_URI, 1024 * 12);
56
56
  DEF_MAX_LENGTH(FRAGMENT, 1024); /* Don't know if this length is specified somewhere or not */
57
- DEF_MAX_LENGTH(REQUEST_PATH, 8196);
57
+ DEF_MAX_LENGTH(REQUEST_PATH, 8192);
58
58
  DEF_MAX_LENGTH(QUERY_STRING, (1024 * 10));
59
59
  DEF_MAX_LENGTH(HEADER, (1024 * (80 + 32)));
60
60
 
@@ -253,11 +253,18 @@ void HttpParser_free(void *data) {
253
253
  }
254
254
  }
255
255
 
256
- void HttpParser_mark(puma_parser* hp) {
256
+ void HttpParser_mark(void *ptr) {
257
+ puma_parser *hp = ptr;
257
258
  if(hp->request) rb_gc_mark(hp->request);
258
259
  if(hp->body) rb_gc_mark(hp->body);
259
260
  }
260
261
 
262
+ const rb_data_type_t HttpParser_data_type = {
263
+ "HttpParser",
264
+ { HttpParser_mark, HttpParser_free, 0 },
265
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
266
+ };
267
+
261
268
  VALUE HttpParser_alloc(VALUE klass)
262
269
  {
263
270
  puma_parser *hp = ALLOC_N(puma_parser, 1);
@@ -274,7 +281,7 @@ VALUE HttpParser_alloc(VALUE klass)
274
281
 
275
282
  puma_parser_init(hp);
276
283
 
277
- return Data_Wrap_Struct(klass, HttpParser_mark, HttpParser_free, hp);
284
+ return TypedData_Wrap_Struct(klass, &HttpParser_data_type, hp);
278
285
  }
279
286
 
280
287
  /**
@@ -286,7 +293,7 @@ VALUE HttpParser_alloc(VALUE klass)
286
293
  VALUE HttpParser_init(VALUE self)
287
294
  {
288
295
  puma_parser *http = NULL;
289
- DATA_GET(self, puma_parser, http);
296
+ DATA_GET(self, puma_parser, &HttpParser_data_type, http);
290
297
  puma_parser_init(http);
291
298
 
292
299
  return self;
@@ -303,7 +310,7 @@ VALUE HttpParser_init(VALUE self)
303
310
  VALUE HttpParser_reset(VALUE self)
304
311
  {
305
312
  puma_parser *http = NULL;
306
- DATA_GET(self, puma_parser, http);
313
+ DATA_GET(self, puma_parser, &HttpParser_data_type, http);
307
314
  puma_parser_init(http);
308
315
 
309
316
  return Qnil;
@@ -320,7 +327,7 @@ VALUE HttpParser_reset(VALUE self)
320
327
  VALUE HttpParser_finish(VALUE self)
321
328
  {
322
329
  puma_parser *http = NULL;
323
- DATA_GET(self, puma_parser, http);
330
+ DATA_GET(self, puma_parser, &HttpParser_data_type, http);
324
331
  puma_parser_finish(http);
325
332
 
326
333
  return puma_parser_is_finished(http) ? Qtrue : Qfalse;
@@ -351,7 +358,7 @@ VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data, VALUE start)
351
358
  char *dptr = NULL;
352
359
  long dlen = 0;
353
360
 
354
- DATA_GET(self, puma_parser, http);
361
+ DATA_GET(self, puma_parser, &HttpParser_data_type, http);
355
362
 
356
363
  from = FIX2INT(start);
357
364
  dptr = rb_extract_chars(data, &dlen);
@@ -385,7 +392,7 @@ VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data, VALUE start)
385
392
  VALUE HttpParser_has_error(VALUE self)
386
393
  {
387
394
  puma_parser *http = NULL;
388
- DATA_GET(self, puma_parser, http);
395
+ DATA_GET(self, puma_parser, &HttpParser_data_type, http);
389
396
 
390
397
  return puma_parser_has_error(http) ? Qtrue : Qfalse;
391
398
  }
@@ -400,7 +407,7 @@ VALUE HttpParser_has_error(VALUE self)
400
407
  VALUE HttpParser_is_finished(VALUE self)
401
408
  {
402
409
  puma_parser *http = NULL;
403
- DATA_GET(self, puma_parser, http);
410
+ DATA_GET(self, puma_parser, &HttpParser_data_type, http);
404
411
 
405
412
  return puma_parser_is_finished(http) ? Qtrue : Qfalse;
406
413
  }
@@ -416,7 +423,7 @@ VALUE HttpParser_is_finished(VALUE self)
416
423
  VALUE HttpParser_nread(VALUE self)
417
424
  {
418
425
  puma_parser *http = NULL;
419
- DATA_GET(self, puma_parser, http);
426
+ DATA_GET(self, puma_parser, &HttpParser_data_type, http);
420
427
 
421
428
  return INT2FIX(http->nread);
422
429
  }
@@ -429,12 +436,14 @@ VALUE HttpParser_nread(VALUE self)
429
436
  */
430
437
  VALUE HttpParser_body(VALUE self) {
431
438
  puma_parser *http = NULL;
432
- DATA_GET(self, puma_parser, http);
439
+ DATA_GET(self, puma_parser, &HttpParser_data_type, http);
433
440
 
434
441
  return http->body;
435
442
  }
436
443
 
444
+ #ifdef HAVE_OPENSSL_BIO_H
437
445
  void Init_mini_ssl(VALUE mod);
446
+ #endif
438
447
 
439
448
  void Init_puma_http11()
440
449
  {
@@ -463,5 +472,7 @@ void Init_puma_http11()
463
472
  rb_define_method(cHttpParser, "body", HttpParser_body, 0);
464
473
  init_common_fields();
465
474
 
475
+ #ifdef HAVE_OPENSSL_BIO_H
466
476
  Init_mini_ssl(mPuma);
477
+ #endif
467
478
  }
@@ -10,19 +10,27 @@ require 'stringio'
10
10
 
11
11
  require 'thread'
12
12
 
13
+ require 'puma/puma_http11'
14
+ require 'puma/detect'
15
+
13
16
  module Puma
14
17
  autoload :Const, 'puma/const'
15
18
  autoload :Server, 'puma/server'
16
19
  autoload :Launcher, 'puma/launcher'
17
20
 
21
+ # @!attribute [rw] stats_object=
18
22
  def self.stats_object=(val)
19
23
  @get_stats = val
20
24
  end
21
25
 
26
+ # @!attribute [rw] stats_object
22
27
  def self.stats
28
+ require 'json'
23
29
  @get_stats.stats.to_json
24
30
  end
25
31
 
32
+ # @!attribute [r] stats_hash
33
+ # @version 5.0.0
26
34
  def self.stats_hash
27
35
  @get_stats.stats
28
36
  end
@@ -32,4 +40,12 @@ module Puma
32
40
  return unless Thread.current.respond_to?(:name=)
33
41
  Thread.current.name = "puma #{name}"
34
42
  end
43
+
44
+ unless HAS_SSL
45
+ module MiniSSL
46
+ # this class is defined so that it exists when Puma is compiled
47
+ # without ssl support, as Server and Reactor use it in rescue statements.
48
+ class SSLError < StandardError ; end
49
+ end
50
+ end
35
51
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
-
5
3
  module Puma
6
4
  module App
7
5
  # Check out {#call}'s source code to see what actions this web application
@@ -9,67 +7,72 @@ module Puma
9
7
  class Status
10
8
  OK_STATUS = '{ "status": "ok" }'.freeze
11
9
 
12
- def initialize(cli, token = nil)
13
- @cli = cli
10
+ # @param launcher [::Puma::Launcher]
11
+ # @param token [String, nil] the token used for authentication
12
+ #
13
+ def initialize(launcher, token = nil)
14
+ @launcher = launcher
14
15
  @auth_token = token
15
16
  end
16
17
 
18
+ # most commands call methods in `::Puma::Launcher` based on command in
19
+ # `env['PATH_INFO']`
17
20
  def call(env)
18
21
  unless authenticate(env)
19
22
  return rack_response(403, 'Invalid auth token', 'text/plain')
20
23
  end
21
24
 
22
- case env['PATH_INFO']
23
- when /\/stop$/
24
- @cli.stop
25
- rack_response(200, OK_STATUS)
25
+ if env['PATH_INFO'] =~ /\/(gc-stats|stats|thread-backtraces)$/
26
+ require 'json'
27
+ end
26
28
 
27
- when /\/halt$/
28
- @cli.halt
29
- rack_response(200, OK_STATUS)
29
+ # resp_type is processed by following case statement, return
30
+ # is a number (status) or a string used as the body of a 200 response
31
+ resp_type =
32
+ case env['PATH_INFO'][/\/([^\/]+)$/, 1]
33
+ when 'stop'
34
+ @launcher.stop ; 200
30
35
 
31
- when /\/restart$/
32
- @cli.restart
33
- rack_response(200, OK_STATUS)
36
+ when 'halt'
37
+ @launcher.halt ; 200
34
38
 
35
- when /\/phased-restart$/
36
- if !@cli.phased_restart
37
- rack_response(404, '{ "error": "phased restart not available" }')
38
- else
39
- rack_response(200, OK_STATUS)
40
- end
39
+ when 'restart'
40
+ @launcher.restart ; 200
41
41
 
42
- when /\/reload-worker-directory$/
43
- if !@cli.send(:reload_worker_directory)
44
- rack_response(404, '{ "error": "reload_worker_directory not available" }')
45
- else
46
- rack_response(200, OK_STATUS)
47
- end
42
+ when 'phased-restart'
43
+ @launcher.phased_restart ? 200 : 404
48
44
 
49
- when /\/gc$/
50
- GC.start
51
- rack_response(200, OK_STATUS)
45
+ when 'reload-worker-directory'
46
+ @launcher.send(:reload_worker_directory) ? 200 : 404
52
47
 
53
- when /\/gc-stats$/
54
- rack_response(200, GC.stat.to_json)
48
+ when 'gc'
49
+ GC.start ; 200
55
50
 
56
- when /\/stats$/
57
- rack_response(200, @cli.stats.to_json)
51
+ when 'gc-stats'
52
+ GC.stat.to_json
58
53
 
59
- when /\/thread-backtraces$/
60
- backtraces = []
61
- @cli.thread_status do |name, backtrace|
62
- backtraces << { name: name, backtrace: backtrace }
63
- end
54
+ when 'stats'
55
+ @launcher.stats.to_json
64
56
 
65
- rack_response(200, backtraces.to_json)
57
+ when 'thread-backtraces'
58
+ backtraces = []
59
+ @launcher.thread_status do |name, backtrace|
60
+ backtraces << { name: name, backtrace: backtrace }
61
+ end
62
+ backtraces.to_json
66
63
 
67
- when /\/refork$/
68
- Process.kill "SIGURG", $$
69
- rack_response(200, OK_STATUS)
64
+ else
65
+ return rack_response(404, "Unsupported action", 'text/plain')
66
+ end
70
67
 
71
- else
72
- rack_response 404, "Unsupported action", 'text/plain'
68
+ case resp_type
69
+ when String
70
+ rack_response 200, resp_type
71
+ when 200
72
+ rack_response 200, OK_STATUS
73
+ when 404
74
+ str = env['PATH_INFO'][/\/(\S+)/, 1].tr '-', '_'
75
+ rack_response 404, "{ \"error\": \"#{str} not available\" }"
73
76
  end
74
77
  end
75
78