toys-core 0.10.4 → 0.11.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +64 -34
- data/README.md +5 -5
- data/docs/guide.md +2 -0
- data/lib/toys/acceptor.rb +1 -1
- data/lib/toys/compat.rb +5 -1
- data/lib/toys/core.rb +1 -1
- data/lib/toys/dsl/flag.rb +2 -2
- data/lib/toys/dsl/flag_group.rb +2 -2
- data/lib/toys/dsl/tool.rb +27 -6
- data/lib/toys/loader.rb +71 -31
- data/lib/toys/source_info.rb +22 -23
- data/lib/toys/standard_mixins/bundler.rb +113 -56
- data/lib/toys/utils/exec.rb +126 -34
- data/lib/toys/utils/gems.rb +82 -31
- data/lib/toys/utils/help_text.rb +54 -59
- metadata +4 -5
- data/lib/toys/utils/gems/gemfile.rb +0 -6
data/lib/toys/utils/exec.rb
CHANGED
@@ -250,13 +250,14 @@ module Toys
|
|
250
250
|
exec_opts = Opts.new(@default_opts).add(opts)
|
251
251
|
spawn_cmd =
|
252
252
|
if cmd.is_a?(::Array)
|
253
|
-
if cmd.size
|
254
|
-
|
253
|
+
if cmd.size > 1
|
254
|
+
binary = canonical_binary_spec(cmd.first, exec_opts)
|
255
|
+
[binary] + cmd[1..-1].map(&:to_s)
|
255
256
|
else
|
256
|
-
cmd
|
257
|
+
[canonical_binary_spec(Array(cmd.first), exec_opts)]
|
257
258
|
end
|
258
259
|
else
|
259
|
-
[cmd]
|
260
|
+
[cmd.to_s]
|
260
261
|
end
|
261
262
|
executor = Executor.new(exec_opts, spawn_cmd, block)
|
262
263
|
executor.execute
|
@@ -492,7 +493,8 @@ module Toys
|
|
492
493
|
#
|
493
494
|
class Controller
|
494
495
|
## @private
|
495
|
-
def initialize(name, controller_streams, captures, pid, join_threads,
|
496
|
+
def initialize(name, controller_streams, captures, pid, join_threads,
|
497
|
+
result_callback, mutex)
|
496
498
|
@name = name
|
497
499
|
@in = controller_streams[:in]
|
498
500
|
@out = controller_streams[:out]
|
@@ -508,6 +510,7 @@ module Toys
|
|
508
510
|
end
|
509
511
|
@join_threads = join_threads
|
510
512
|
@result_callback = result_callback
|
513
|
+
@mutex = mutex
|
511
514
|
@result = nil
|
512
515
|
end
|
513
516
|
|
@@ -547,7 +550,7 @@ module Toys
|
|
547
550
|
##
|
548
551
|
# The process ID.
|
549
552
|
#
|
550
|
-
# Exactly one of
|
553
|
+
# Exactly one of {#exception} and {#pid} will be non-nil.
|
551
554
|
#
|
552
555
|
# @return [Integer] if the process start was successful
|
553
556
|
# @return [nil] if the process could not be started.
|
@@ -557,7 +560,7 @@ module Toys
|
|
557
560
|
##
|
558
561
|
# The exception raised when the process failed to start.
|
559
562
|
#
|
560
|
-
# Exactly one of
|
563
|
+
# Exactly one of {#exception} and {#pid} will be non-nil.
|
561
564
|
#
|
562
565
|
# @return [Exception] if the process failed to start.
|
563
566
|
# @return [nil] if the process start was successful.
|
@@ -575,7 +578,10 @@ module Toys
|
|
575
578
|
stream = stream_for(which)
|
576
579
|
@join_threads << ::Thread.new do
|
577
580
|
begin
|
578
|
-
|
581
|
+
data = stream.read
|
582
|
+
@mutex.synchronize do
|
583
|
+
@captures[which] = data
|
584
|
+
end
|
579
585
|
ensure
|
580
586
|
stream.close
|
581
587
|
end
|
@@ -722,15 +728,20 @@ module Toys
|
|
722
728
|
##
|
723
729
|
# Wait for the subcommand to complete, and return a result object.
|
724
730
|
#
|
731
|
+
# Closes the control streams if present. The stdin stream is always
|
732
|
+
# closed, even if the call times out. The stdout and stderr streams are
|
733
|
+
# closed only after the command terminates.
|
734
|
+
#
|
725
735
|
# @param timeout [Numeric,nil] The timeout in seconds, or `nil` to
|
726
736
|
# wait indefinitely.
|
727
737
|
# @return [Toys::Utils::Exec::Result] The result object
|
728
738
|
# @return [nil] if a timeout occurred.
|
729
739
|
#
|
730
740
|
def result(timeout: nil)
|
741
|
+
close_streams(:in)
|
731
742
|
return nil if @wait_thread && !@wait_thread.join(timeout)
|
732
743
|
@result ||= begin
|
733
|
-
close_streams
|
744
|
+
close_streams(:out)
|
734
745
|
@join_threads.each(&:join)
|
735
746
|
Result.new(name, @captures[:out], @captures[:err], @wait_thread&.value, @exception)
|
736
747
|
.tap { |result| @result_callback&.call(result) }
|
@@ -738,13 +749,13 @@ module Toys
|
|
738
749
|
end
|
739
750
|
|
740
751
|
##
|
741
|
-
# Close
|
752
|
+
# Close the controller's streams.
|
742
753
|
# @private
|
743
754
|
#
|
744
|
-
def close_streams
|
745
|
-
@in.close if @in && !@in.closed?
|
746
|
-
@out.close if @out && !@out.closed?
|
747
|
-
@err.close if @err && !@err.closed?
|
755
|
+
def close_streams(which)
|
756
|
+
@in.close if which != :out && @in && !@in.closed?
|
757
|
+
@out.close if which != :in && @out && !@out.closed?
|
758
|
+
@err.close if which != :in && @err && !@err.closed?
|
748
759
|
self
|
749
760
|
end
|
750
761
|
|
@@ -773,7 +784,21 @@ module Toys
|
|
773
784
|
end
|
774
785
|
|
775
786
|
##
|
776
|
-
# The result returned from a subcommand execution.
|
787
|
+
# The result returned from a subcommand execution. This includes the
|
788
|
+
# identifying name of the execution (if any), the result status of the
|
789
|
+
# execution, and any captured stream output.
|
790
|
+
#
|
791
|
+
# Possible result statuses are:
|
792
|
+
#
|
793
|
+
# * The process failed to start. {Result#failed?} will return true, and
|
794
|
+
# {Result#exception} will return an exception describing the failure
|
795
|
+
# (often an errno).
|
796
|
+
# * The process executed and exited with a normal exit code. Either
|
797
|
+
# {Result#success?} or {Result#error?} will return true, and
|
798
|
+
# {Result.exit_code} will return the numeric exit code.
|
799
|
+
# * The process executed but was terminated by an uncaught signal.
|
800
|
+
# {Result#signaled?} will return true, and {Result#term_signal} will
|
801
|
+
# return the numeric signal code.
|
777
802
|
#
|
778
803
|
class Result
|
779
804
|
## @private
|
@@ -809,11 +834,13 @@ module Toys
|
|
809
834
|
attr_reader :captured_err
|
810
835
|
|
811
836
|
##
|
812
|
-
# The status
|
837
|
+
# The Ruby process status object, providing various information about
|
838
|
+
# the ending state of the process.
|
813
839
|
#
|
814
|
-
# Exactly one of
|
840
|
+
# Exactly one of {#exception} and {#status} will be non-nil.
|
815
841
|
#
|
816
|
-
# @return [Process::Status] The status
|
842
|
+
# @return [Process::Status] The status, if the process was successfully
|
843
|
+
# spanwed and terminated.
|
817
844
|
# @return [nil] if the process could not be started.
|
818
845
|
#
|
819
846
|
attr_reader :status
|
@@ -821,7 +848,7 @@ module Toys
|
|
821
848
|
##
|
822
849
|
# The exception raised if a process couldn't be started.
|
823
850
|
#
|
824
|
-
# Exactly one of
|
851
|
+
# Exactly one of {#exception} and {#status} will be non-nil.
|
825
852
|
#
|
826
853
|
# @return [Exception] The exception raised from process start.
|
827
854
|
# @return [nil] if the process started successfully.
|
@@ -829,33 +856,76 @@ module Toys
|
|
829
856
|
attr_reader :exception
|
830
857
|
|
831
858
|
##
|
832
|
-
# The numeric status code
|
859
|
+
# The numeric status code for a process that exited normally,
|
833
860
|
#
|
834
|
-
#
|
835
|
-
#
|
861
|
+
# Exactly one of {#exception}, {#exit_code}, and {#term_signal} will be
|
862
|
+
# non-nil.
|
836
863
|
#
|
837
|
-
# @return [Integer]
|
864
|
+
# @return [Integer] the numeric status code, if the process started
|
865
|
+
# successfully and exited normally.
|
866
|
+
# @return [nil] if the process did not start successfully, or was
|
867
|
+
# terminated by an uncaught signal.
|
838
868
|
#
|
839
869
|
def exit_code
|
840
|
-
status
|
870
|
+
status&.exitstatus
|
871
|
+
end
|
872
|
+
|
873
|
+
##
|
874
|
+
# The numeric signal code that caused process termination.
|
875
|
+
#
|
876
|
+
# Exactly one of {#exception}, {#exit_code}, and {#term_signal} will be
|
877
|
+
# non-nil.
|
878
|
+
#
|
879
|
+
# @return [Integer] The signal that caused the process to terminate.
|
880
|
+
# @return [nil] if the process did not start successfully, or executed
|
881
|
+
# and exited with a normal exit code.
|
882
|
+
#
|
883
|
+
def term_signal
|
884
|
+
status&.termsig
|
885
|
+
end
|
886
|
+
|
887
|
+
##
|
888
|
+
# Returns true if the subprocess failed to start, or false if the
|
889
|
+
# process was able to execute.
|
890
|
+
#
|
891
|
+
# @return [Boolean]
|
892
|
+
#
|
893
|
+
def failed?
|
894
|
+
status.nil?
|
895
|
+
end
|
896
|
+
|
897
|
+
##
|
898
|
+
# Returns true if the subprocess terminated due to an unhandled signal,
|
899
|
+
# or false if the process failed to start or exited normally.
|
900
|
+
#
|
901
|
+
# @return [Boolean]
|
902
|
+
#
|
903
|
+
def signaled?
|
904
|
+
!term_signal.nil?
|
841
905
|
end
|
842
906
|
|
843
907
|
##
|
844
|
-
# Returns true if the subprocess terminated with a zero status
|
908
|
+
# Returns true if the subprocess terminated with a zero status, or
|
909
|
+
# false if the process failed to start, terminated due to a signal, or
|
910
|
+
# returned a nonzero status.
|
845
911
|
#
|
846
912
|
# @return [Boolean]
|
847
913
|
#
|
848
914
|
def success?
|
849
|
-
exit_code
|
915
|
+
code = exit_code
|
916
|
+
!code.nil? && code.zero?
|
850
917
|
end
|
851
918
|
|
852
919
|
##
|
853
|
-
# Returns true if the subprocess terminated with a nonzero status
|
920
|
+
# Returns true if the subprocess terminated with a nonzero status, or
|
921
|
+
# false if the process failed to start, terminated due to a signal, or
|
922
|
+
# returned a zero status.
|
854
923
|
#
|
855
924
|
# @return [Boolean]
|
856
925
|
#
|
857
926
|
def error?
|
858
|
-
|
927
|
+
code = exit_code
|
928
|
+
!code.nil? && !code.zero?
|
859
929
|
end
|
860
930
|
end
|
861
931
|
|
@@ -876,6 +946,7 @@ module Toys
|
|
876
946
|
@parent_streams = []
|
877
947
|
@block = block
|
878
948
|
@default_stream = @config_opts[:background] ? :null : :inherit
|
949
|
+
@mutex = ::Mutex.new
|
879
950
|
end
|
880
951
|
|
881
952
|
def execute
|
@@ -887,10 +958,10 @@ module Toys
|
|
887
958
|
return controller if @config_opts[:background]
|
888
959
|
begin
|
889
960
|
@block&.call(controller)
|
961
|
+
controller.result
|
890
962
|
ensure
|
891
|
-
controller.close_streams
|
963
|
+
controller.close_streams(:both)
|
892
964
|
end
|
893
|
-
controller.result
|
894
965
|
end
|
895
966
|
|
896
967
|
private
|
@@ -898,12 +969,19 @@ module Toys
|
|
898
969
|
def log_command
|
899
970
|
logger = @config_opts[:logger]
|
900
971
|
if logger && @config_opts[:log_level] != false
|
901
|
-
cmd_str = @config_opts[:log_cmd]
|
902
|
-
cmd_str ||= @spawn_cmd.size == 1 ? @spawn_cmd.first : @spawn_cmd.inspect if @spawn_cmd
|
972
|
+
cmd_str = @config_opts[:log_cmd] || default_log_str(@spawn_cmd)
|
903
973
|
logger.add(@config_opts[:log_level] || ::Logger::INFO, cmd_str) if cmd_str
|
904
974
|
end
|
905
975
|
end
|
906
976
|
|
977
|
+
def default_log_str(spawn_cmd)
|
978
|
+
return nil unless spawn_cmd
|
979
|
+
return spawn_cmd.first if spawn_cmd.size == 1 && spawn_cmd.first.is_a?(::String)
|
980
|
+
cmd_binary = spawn_cmd.first
|
981
|
+
cmd_binary = cmd_binary.first if cmd_binary.is_a?(::Array)
|
982
|
+
([cmd_binary] + spawn_cmd[1..-1]).inspect
|
983
|
+
end
|
984
|
+
|
907
985
|
def start_with_controller
|
908
986
|
pid =
|
909
987
|
begin
|
@@ -913,7 +991,7 @@ module Toys
|
|
913
991
|
end
|
914
992
|
@child_streams.each(&:close)
|
915
993
|
Controller.new(@config_opts[:name], @controller_streams, @captures, pid,
|
916
|
-
@join_threads, @config_opts[:result_callback])
|
994
|
+
@join_threads, @config_opts[:result_callback], @mutex)
|
917
995
|
end
|
918
996
|
|
919
997
|
def start_process
|
@@ -1220,13 +1298,27 @@ module Toys
|
|
1220
1298
|
stream = make_out_pipe(key)
|
1221
1299
|
@join_threads << ::Thread.new do
|
1222
1300
|
begin
|
1223
|
-
|
1301
|
+
data = stream.read
|
1302
|
+
@mutex.synchronize do
|
1303
|
+
@captures[key] = data
|
1304
|
+
end
|
1224
1305
|
ensure
|
1225
1306
|
stream.close
|
1226
1307
|
end
|
1227
1308
|
end
|
1228
1309
|
end
|
1229
1310
|
end
|
1311
|
+
|
1312
|
+
private
|
1313
|
+
|
1314
|
+
def canonical_binary_spec(cmd, exec_opts)
|
1315
|
+
config_argv0 = exec_opts.config_opts[:argv0]
|
1316
|
+
return cmd.to_s if !config_argv0 && !cmd.is_a?(::Array)
|
1317
|
+
cmd = Array(cmd)
|
1318
|
+
actual_cmd = cmd.first
|
1319
|
+
argv0 = cmd[1] || config_argv0 || actual_cmd
|
1320
|
+
[actual_cmd.to_s, argv0.to_s]
|
1321
|
+
end
|
1230
1322
|
end
|
1231
1323
|
end
|
1232
1324
|
end
|
data/lib/toys/utils/gems.rb
CHANGED
@@ -64,6 +64,19 @@ module Toys
|
|
64
64
|
class AlreadyBundledError < BundlerFailedError
|
65
65
|
end
|
66
66
|
|
67
|
+
##
|
68
|
+
# The bundle contained a toys or toys-core dependency that is
|
69
|
+
# incompatible with the currently running version.
|
70
|
+
#
|
71
|
+
class IncompatibleToysError < BundlerFailedError
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# The gemfile names that are searched by default.
|
76
|
+
# @return [Array<String>]
|
77
|
+
#
|
78
|
+
DEFAULT_GEMFILE_NAMES = [".gems.rb", "gems.rb", "Gemfile"].freeze
|
79
|
+
|
67
80
|
##
|
68
81
|
# Activate the given gem. If it is not present, attempt to install it (or
|
69
82
|
# inform the user to update the bundle).
|
@@ -142,16 +155,33 @@ module Toys
|
|
142
155
|
end
|
143
156
|
|
144
157
|
##
|
145
|
-
#
|
158
|
+
# Search for an appropriate Gemfile, and set up the bundle.
|
159
|
+
#
|
160
|
+
# @param groups [Array<String>] The groups to include in setup.
|
161
|
+
#
|
162
|
+
# @param gemfile_path [String] The path to the Gemfile to use. If `nil`
|
163
|
+
# or not given, the `:search_dirs` will be searched for a Gemfile.
|
164
|
+
#
|
165
|
+
# @param search_dirs [String,Array<String>] Directories in which to
|
166
|
+
# search for a Gemfile, if gemfile_path is not given. You can provide
|
167
|
+
# a single directory or an array of directories.
|
168
|
+
#
|
169
|
+
# @param gemfile_names [String,Array<String>] File names that are
|
170
|
+
# recognized as Gemfiles, when searching because gemfile_path is not
|
171
|
+
# given. Defaults to {DEFAULT_GEMFILE_NAMES}.
|
146
172
|
#
|
147
|
-
# @param groups [Array<String>] The groups to include in setup
|
148
|
-
# @param search_dirs [Array<String>] Directories to search for a Gemfile
|
149
173
|
# @return [void]
|
150
174
|
#
|
151
175
|
def bundle(groups: nil,
|
152
|
-
|
176
|
+
gemfile_path: nil,
|
177
|
+
search_dirs: nil,
|
178
|
+
gemfile_names: nil)
|
179
|
+
Array(search_dirs).each do |dir|
|
180
|
+
break if gemfile_path
|
181
|
+
gemfile_path = Gems.find_gemfile(dir, gemfile_names: gemfile_names)
|
182
|
+
end
|
183
|
+
raise GemfileNotFoundError, "Gemfile not found" unless gemfile_path
|
153
184
|
Gems.synchronize do
|
154
|
-
gemfile_path = find_gemfile(Array(search_dirs))
|
155
185
|
if configure_gemfile(gemfile_path)
|
156
186
|
activate("bundler", "~> 2.1")
|
157
187
|
require "bundler"
|
@@ -160,9 +190,19 @@ module Toys
|
|
160
190
|
end
|
161
191
|
end
|
162
192
|
|
193
|
+
# @private
|
194
|
+
def self.find_gemfile(search_dir, gemfile_names: nil)
|
195
|
+
gemfile_names ||= DEFAULT_GEMFILE_NAMES
|
196
|
+
Array(gemfile_names).each do |file|
|
197
|
+
gemfile_path = ::File.join(search_dir, file)
|
198
|
+
return gemfile_path if ::File.readable?(gemfile_path)
|
199
|
+
end
|
200
|
+
nil
|
201
|
+
end
|
202
|
+
|
163
203
|
@global_mutex = ::Monitor.new
|
164
204
|
|
165
|
-
|
205
|
+
# @private
|
166
206
|
def self.synchronize(&block)
|
167
207
|
@global_mutex.synchronize(&block)
|
168
208
|
end
|
@@ -230,14 +270,6 @@ module Toys
|
|
230
270
|
raise ActivationFailedError, err.message
|
231
271
|
end
|
232
272
|
|
233
|
-
def find_gemfile(search_dirs)
|
234
|
-
search_dirs.each do |dir|
|
235
|
-
gemfile_path = ::File.join(dir, "Gemfile")
|
236
|
-
return gemfile_path if ::File.readable?(gemfile_path)
|
237
|
-
end
|
238
|
-
raise GemfileNotFoundError, "Gemfile not found"
|
239
|
-
end
|
240
|
-
|
241
273
|
def configure_gemfile(gemfile_path)
|
242
274
|
old_path = ::ENV["BUNDLE_GEMFILE"]
|
243
275
|
if old_path
|
@@ -258,13 +290,13 @@ module Toys
|
|
258
290
|
def setup_bundle(gemfile_path, groups)
|
259
291
|
begin
|
260
292
|
modify_bundle_definition(gemfile_path)
|
261
|
-
::Bundler.setup(*groups)
|
262
|
-
rescue ::Bundler::GemNotFound
|
293
|
+
::Bundler.ui.silence { ::Bundler.setup(*groups) }
|
294
|
+
rescue ::Bundler::GemNotFound, ::Bundler::VersionConflict
|
263
295
|
restore_toys_libs
|
264
296
|
install_bundle(gemfile_path)
|
265
297
|
::Bundler.reset!
|
266
298
|
modify_bundle_definition(gemfile_path)
|
267
|
-
::Bundler.setup(*groups)
|
299
|
+
::Bundler.ui.silence { ::Bundler.setup(*groups) }
|
268
300
|
end
|
269
301
|
restore_toys_libs
|
270
302
|
end
|
@@ -272,24 +304,38 @@ module Toys
|
|
272
304
|
def modify_bundle_definition(gemfile_path)
|
273
305
|
builder = ::Bundler::Dsl.new
|
274
306
|
builder.eval_gemfile(gemfile_path)
|
275
|
-
begin
|
276
|
-
builder.eval_gemfile(::File.join(__dir__, "gems", "gemfile.rb"))
|
277
|
-
rescue ::Bundler::Dsl::DSLError
|
278
|
-
terminal.puts(
|
279
|
-
"WARNING: Unable to integrate your Gemfile into the Toys runtime.\n" \
|
280
|
-
"When using the Toys Bundler integration features, do NOT list\n" \
|
281
|
-
"the toys or toys-core gems directly in your Gemfile. They can be\n" \
|
282
|
-
"dependencies of another gem, but cannot be listed directly.",
|
283
|
-
:red
|
284
|
-
)
|
285
|
-
return
|
286
|
-
end
|
287
307
|
toys_gems = ["toys-core"]
|
288
|
-
|
308
|
+
remove_gem_from_definition(builder, "toys-core")
|
309
|
+
removed_toys = remove_gem_from_definition(builder, "toys")
|
310
|
+
add_gem_to_definition(builder, "toys-core")
|
311
|
+
if removed_toys || ::Toys.const_defined?(:VERSION)
|
312
|
+
add_gem_to_definition(builder, "toys")
|
313
|
+
toys_gems << "toys"
|
314
|
+
end
|
289
315
|
definition = builder.to_definition(gemfile_path + ".lock", { gems: toys_gems })
|
290
316
|
::Bundler.instance_variable_set(:@definition, definition)
|
291
317
|
end
|
292
318
|
|
319
|
+
def remove_gem_from_definition(builder, name)
|
320
|
+
existing_dep = builder.dependencies.find { |dep| dep.name == name }
|
321
|
+
return false unless existing_dep
|
322
|
+
unless existing_dep.requirement.satisfied_by?(::Gem::Version.new(::Toys::Core::VERSION))
|
323
|
+
raise IncompatibleToysError,
|
324
|
+
"The bundle lists #{name} #{existing_dep.requirement} as a dependency, which is" \
|
325
|
+
" incompatible with the current version #{::Toys::Core::VERSION}."
|
326
|
+
end
|
327
|
+
builder.dependencies.delete(existing_dep)
|
328
|
+
true
|
329
|
+
end
|
330
|
+
|
331
|
+
def add_gem_to_definition(builder, name)
|
332
|
+
if ::ENV["TOYS_DEV"] == "true"
|
333
|
+
path = ::File.join(::File.dirname(::File.dirname(::Toys::CORE_LIB_PATH)), name)
|
334
|
+
end
|
335
|
+
command = "gem #{name.inspect}, #{::Toys::Core::VERSION.inspect}, path: #{path.inspect}\n"
|
336
|
+
builder.eval_gemfile("current #{name}", command)
|
337
|
+
end
|
338
|
+
|
293
339
|
def restore_toys_libs
|
294
340
|
$LOAD_PATH.delete(::Toys::CORE_LIB_PATH)
|
295
341
|
$LOAD_PATH.unshift(::Toys::CORE_LIB_PATH)
|
@@ -319,7 +365,12 @@ module Toys
|
|
319
365
|
" `cd #{gemfile_dir} && bundle install`"
|
320
366
|
end
|
321
367
|
require "bundler/cli"
|
322
|
-
|
368
|
+
begin
|
369
|
+
::Bundler::CLI.start(["install"])
|
370
|
+
rescue ::Bundler::GemNotFound, ::Bundler::InstallError
|
371
|
+
terminal.puts("Failed to install. Trying update...")
|
372
|
+
::Bundler::CLI.start(["update"])
|
373
|
+
end
|
323
374
|
end
|
324
375
|
end
|
325
376
|
end
|