flare-tools 0.1.4 → 0.4.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. data/.gemtest +0 -0
  2. data/Flare-tools.txt +0 -0
  3. data/History.txt +114 -2
  4. data/LICENSE +21 -0
  5. data/Manifest.txt +65 -8
  6. data/README.txt +356 -0
  7. data/Rakefile +90 -25
  8. data/Tutorial.txt +370 -0
  9. data/bin/flare-admin +6 -0
  10. data/bin/flare-argv0 +6 -0
  11. data/bin/flare-deploy +6 -0
  12. data/bin/flare-keychecker +6 -0
  13. data/bin/flare-part +6 -0
  14. data/bin/flare-ping +6 -0
  15. data/bin/flare-stats +4 -10
  16. data/bin/flare-zkadmin +6 -0
  17. data/lib/flare/net/connection.rb +98 -0
  18. data/lib/flare/test/cluster.rb +140 -0
  19. data/lib/flare/test/daemon.rb +144 -0
  20. data/lib/flare/test/node.rb +62 -0
  21. data/lib/flare/tools.rb +18 -16
  22. data/lib/flare/tools/cli.rb +32 -0
  23. data/lib/flare/tools/cli/activate.rb +106 -0
  24. data/lib/flare/tools/cli/balance.rb +83 -0
  25. data/lib/flare/tools/cli/cli_util.rb +77 -0
  26. data/lib/flare/tools/cli/deploy.rb +170 -0
  27. data/lib/flare/tools/cli/down.rb +85 -0
  28. data/lib/flare/tools/cli/dump.rb +219 -0
  29. data/lib/flare/tools/cli/dumpkey.rb +117 -0
  30. data/lib/flare/tools/cli/flare_admin.rb +81 -0
  31. data/lib/flare/tools/cli/flare_argv0.rb +60 -0
  32. data/lib/flare/tools/cli/flare_keychecker.rb +106 -0
  33. data/lib/flare/tools/cli/flare_zkadmin.rb +226 -0
  34. data/lib/flare/tools/cli/index.rb +54 -0
  35. data/lib/flare/tools/cli/list.rb +93 -0
  36. data/lib/flare/tools/cli/master.rb +143 -0
  37. data/lib/flare/tools/cli/part.rb +100 -0
  38. data/lib/flare/tools/cli/ping.rb +81 -0
  39. data/lib/flare/tools/cli/reconstruct.rb +164 -0
  40. data/lib/flare/tools/cli/remove.rb +119 -0
  41. data/lib/flare/tools/cli/restore.rb +180 -0
  42. data/lib/flare/tools/cli/slave.rb +125 -0
  43. data/lib/flare/tools/cli/stats.rb +229 -122
  44. data/lib/flare/tools/cli/sub_command.rb +73 -0
  45. data/lib/flare/tools/cli/summary.rb +97 -0
  46. data/lib/flare/tools/cli/threads.rb +78 -0
  47. data/lib/flare/tools/cli/verify.rb +202 -0
  48. data/lib/flare/tools/client.rb +267 -0
  49. data/lib/flare/tools/cluster.rb +319 -0
  50. data/lib/flare/tools/common.rb +196 -0
  51. data/lib/flare/tools/index_server.rb +51 -0
  52. data/lib/flare/tools/node.rb +162 -0
  53. data/lib/flare/tools/stats.rb +75 -0
  54. data/lib/flare/tools/zk_util.rb +28 -0
  55. data/lib/flare/util.rb +34 -0
  56. data/lib/flare/util/bwlimit.rb +132 -0
  57. data/lib/flare/util/command_line.rb +79 -0
  58. data/lib/flare/util/conf.rb +71 -0
  59. data/lib/flare/util/constant.rb +25 -0
  60. data/lib/flare/util/conversion.rb +26 -0
  61. data/lib/flare/util/default_logger.rb +52 -0
  62. data/lib/flare/util/exception.rb +19 -0
  63. data/lib/flare/util/filesystem.rb +30 -0
  64. data/lib/flare/util/flared_conf.rb +33 -0
  65. data/lib/flare/util/flarei_conf.rb +32 -0
  66. data/lib/flare/util/hash_function.rb +32 -0
  67. data/lib/flare/util/interruption.rb +70 -0
  68. data/lib/flare/util/key_resolver.rb +67 -0
  69. data/lib/flare/util/log4r_logger.rb +79 -0
  70. data/lib/flare/util/logger.rb +40 -0
  71. data/lib/flare/util/logging.rb +84 -0
  72. data/lib/flare/util/result.rb +53 -0
  73. data/test/test/experimental/cache_test.rb +113 -0
  74. data/test/test/experimental/key_distribution_test.rb +38 -0
  75. data/test/test/experimental/keychecker_test.rb +60 -0
  76. data/test/test/experimental/list_test.rb +108 -0
  77. data/test/test/extra/replication_test.rb +184 -0
  78. data/test/test/integration/cli_test.rb +348 -0
  79. data/test/test/integration/dump_expired_test.rb +103 -0
  80. data/test/test/integration/dump_test.rb +128 -0
  81. data/test/test/integration/index_server_test.rb +35 -0
  82. data/test/test/integration/node_test.rb +78 -0
  83. data/test/test/integration/partition_test.rb +235 -0
  84. data/test/test/integration/proxy_test.rb +54 -0
  85. data/test/test/integration/stats_test.rb +79 -0
  86. data/test/test/system/flare_admin_test.rb +191 -0
  87. data/test/test/unit/bwlimit_test.rb +52 -0
  88. data/test/test/unit/cluster_test.rb +96 -0
  89. data/test/test/unit/daemon_test.rb +30 -0
  90. data/test/test/unit/logger_test.rb +46 -0
  91. data/test/test/unit/tools_test.rb +25 -0
  92. data/test/test/unit/util_test.rb +70 -0
  93. metadata +239 -84
  94. data/README.rdoc +0 -83
  95. data/bin/flare-partition-setting +0 -12
  96. data/lib/flare/tools/cli/partition_setting.rb +0 -86
  97. data/lib/flare/tools/core.rb +0 -189
  98. data/lib/flare/tools/logger.rb +0 -31
  99. data/test/test_flare-tools.rb +0 -11
  100. data/test/test_helper.rb +0 -3
@@ -0,0 +1,85 @@
1
+ # -*- coding: utf-8; -*-
2
+ # Authors:: Kiyoshi Ikehara <kiyoshi.ikehara@gree.net>
3
+ # Copyright:: Copyright (C) GREE, Inc. 2011.
4
+ # License:: MIT-style
5
+
6
+ require 'flare/tools/stats'
7
+ require 'flare/tools/node'
8
+ require 'flare/tools/index_server'
9
+ require 'flare/tools/common'
10
+ require 'flare/tools/cluster'
11
+ require 'flare/util/conversion'
12
+ require 'flare/util/constant'
13
+ require 'flare/tools/cli/sub_command'
14
+
15
+ module Flare
16
+ module Tools
17
+ module Cli
18
+
19
+ class Down < SubCommand
20
+ include Flare::Util::Conversion
21
+ include Flare::Util::Constant
22
+ include Flare::Tools::Common
23
+
24
+ myname :down
25
+ desc "turn down nodes and move them to proxy state."
26
+ usage "down [hostname:port] ..."
27
+
28
+ def setup(opt)
29
+ opt.on('--force', "commit changes without confirmation") {@force = true}
30
+ end
31
+
32
+ def initialize
33
+ super
34
+ @force = false
35
+ end
36
+
37
+ def execute(config, *args)
38
+ return S_NG if args.size < 1
39
+
40
+ hosts = args.map {|x| x.split(':')}
41
+ hosts.each do |x|
42
+ if x.size != 2
43
+ puts "invalid argument '#{x.join(':')}'."
44
+ return S_NG
45
+ end
46
+ end
47
+
48
+ Flare::Tools::IndexServer.open(config[:index_server_hostname], config[:index_server_port], config[:timeout]) do |s|
49
+ cluster = Flare::Tools::Cluster.new(s.host, s.port, s.stats_nodes)
50
+
51
+ hosts.each do |hostname,port|
52
+ down = 'down'
53
+ nodekey = nodekey_of hostname, port
54
+ ipaddr = address_of_hostname(hostname)
55
+
56
+ unless cluster.has_nodekey? nodekey
57
+ error "invalid 'hostname:port' pair: #{nodekey}"
58
+ return S_NG
59
+ end
60
+
61
+ node = cluster.node_stat(nodekey)
62
+
63
+ exec = @force
64
+ if exec
65
+ elsif node['state'] == down
66
+ puts "#{ipaddr}:#{port} is already down."
67
+ else
68
+ STDERR.print "turning node down (node=#{ipaddr}:#{port}, state=#{node['state']} -> #{down}) (y/n): "
69
+ exec = interruptible {(gets.chomp.upcase == "Y")}
70
+ end
71
+ if exec
72
+ s.set_state(hostname, port, down) unless config[:dry_run]
73
+ end
74
+ end
75
+
76
+ puts string_of_nodelist(s.stats_nodes, hosts.map {|x| "#{x[0]}:#{x[1]}"})
77
+ end
78
+
79
+ S_OK
80
+ end # execute()
81
+
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,219 @@
1
+ # -*- coding: utf-8; -*-
2
+ # Authors:: Kiyoshi Ikehara <kiyoshi.ikehara@gree.net>
3
+ # Copyright:: Copyright (C) GREE, Inc. 2011.
4
+ # License:: MIT-style
5
+
6
+ require 'flare/tools/node'
7
+ require 'flare/tools/index_server'
8
+ require 'flare/tools/common'
9
+ require 'flare/util/conversion'
10
+ require 'flare/util/constant'
11
+ require 'flare/util/bwlimit'
12
+ require 'flare/tools/cli/sub_command'
13
+ require 'csv'
14
+
15
+ begin
16
+ require 'tokyocabinet'
17
+ rescue LoadError => e
18
+ end
19
+
20
+ module Flare
21
+ module Tools
22
+ module Cli
23
+
24
+ class Dump < SubCommand
25
+
26
+ class Dumper
27
+ attr_reader :name
28
+ def write data, key, flag, len, version, expire
29
+ raise "internal error"
30
+ end
31
+ def close
32
+ raise "internal error"
33
+ end
34
+ end
35
+
36
+ class DefaultDumper < Dumper
37
+ def self.myname
38
+ "default"
39
+ end
40
+ def initialize filepath_or_writable
41
+ @output = if filepath_or_writable.kind_of?(String)
42
+ open(filepath_or_writable, 'w')
43
+ else
44
+ filepath_or_writable
45
+ end
46
+ end
47
+ def write data, key, flag, len, version, expire
48
+ @output.puts "#{key} #{flag} #{len} #{version} #{expire} '#{data}'"
49
+ end
50
+ def close
51
+ @output.close unless @output == STDOUT || @output == STDERR
52
+ end
53
+ end
54
+
55
+ class CsvDumper < Dumper
56
+ def self.myname
57
+ "csv"
58
+ end
59
+ def initialize filepath_or_writable
60
+ @output = if filepath_or_writable.kind_of?(String)
61
+ open(filepath_or_writable, 'w')
62
+ else
63
+ filepath_or_writable
64
+ end
65
+ @output.puts "# key, flag, len, version, expire, data"
66
+ @writer = CSV::Writer.generate(@output, ',')
67
+ end
68
+ def write data, key, flag, len, version, expire
69
+ @writer << [key, flag, len, version, expire, data]
70
+ end
71
+ def close
72
+ @output.close unless @output == STDOUT || @output == STDERR
73
+ end
74
+ end
75
+
76
+ class TchDumper < Dumper
77
+ def self.myname
78
+ "tch"
79
+ end
80
+ def initialize filepath
81
+ raise "output file not specified." if filepath.nil?
82
+ raise "#{filepath} isn't a path." unless filepath.kind_of?(String)
83
+ @hdb = TokyoCabinet::HDB.new
84
+ @hdb.open(filepath, TokyoCabinet::HDB::OCREAT|TokyoCabinet::HDB::OWRITER)
85
+ end
86
+ def write data, key, flag, size, version, expire
87
+ # uint32_t flag -> L // uint32_t
88
+ # time_t expire -> Q // unsigned long
89
+ # uint64_t size -> Q // uint64_t
90
+ # uint64_t version -> Q // uint64_t
91
+ # uint32_t option -> L // uint32_t
92
+ value = [flag, expire, size, version].pack("LQQQ")+data
93
+ @hdb.put(key, value)
94
+ end
95
+ def close
96
+ @hdb.close
97
+ end
98
+ end
99
+
100
+ Iterators = [DefaultDumper, CsvDumper]
101
+ Iterators << TchDumper if defined? TokyoCabinet
102
+ Formats = Iterators.map {|n| n.myname}
103
+ SizeOfByte = 8
104
+
105
+ include Flare::Util::Conversion
106
+ include Flare::Util::Constant
107
+ include Flare::Tools::Common
108
+
109
+ myname :dump
110
+ desc "dump data from nodes. (experimental)"
111
+ usage "dump [hostname:port] ..."
112
+
113
+ def setup(opt)
114
+ opt.on('-o', '--output=FILE', "output to file") {|v| @output = v}
115
+ opt.on('-f', '--format=FORMAT', "specify output format [#{Formats.join(',')}]") {|v| @format = v}
116
+ opt.on( '--bwlimit=BANDWIDTH', "specify bandwidth limit (bps)") {|v|
117
+ @bwlimit = Flare::Util::Bwlimit.bps(v)
118
+ }
119
+ opt.on('--all', "dump from all master nodes") {|v| @all = true}
120
+ opt.on('--raw', "raw dump mode (for debugging)") {|v| @raw = true}
121
+ end
122
+
123
+ def initialize
124
+ super
125
+ @output = nil
126
+ @format = nil
127
+ @bwlimit = 0
128
+ @all = false
129
+ @raw = false
130
+ @partition_size = 1
131
+ end
132
+
133
+ def execute(config, *args)
134
+ STDERR.puts "please install tokyocabinet via gem command." unless defined? TokyoCabinet
135
+
136
+ cluster = nil
137
+ Flare::Tools::IndexServer.open(config[:index_server_hostname], config[:index_server_port], config[:timeout]) do |s|
138
+ cluster = Flare::Tools::Cluster.new(s.host, s.port, s.stats_nodes)
139
+ end
140
+ return S_NG if cluster.nil?
141
+
142
+ partition_size = cluster.partition_size
143
+
144
+ if @all
145
+ if args.size > 0
146
+ STDERR.puts "don't specify any nodes with --all option."
147
+ return S_NG
148
+ else
149
+ args = cluster.master_nodekeys
150
+ end
151
+ else
152
+ if args.size == 0
153
+ STDERR.puts "please specify --all option to get complete dump."
154
+ return S_NG
155
+ end
156
+ end
157
+
158
+ unless Formats.include?(@format)
159
+ STDERR.puts "unknown format: #{@format}"
160
+ return S_NG
161
+ end
162
+
163
+ hosts = args.map {|x| x.split(':')}
164
+ hosts.each do |x|
165
+ if x.size == 2
166
+ x << cluster.partition_of_nodename("#{x[0]}:#{x[1]}")
167
+ elsif x.size == 4
168
+ if x[3] =~ /^\d+$/
169
+ STDERR.puts "invalid partition number '#{x.join(':')}'."
170
+ x[3] = x[3].to_i
171
+ else
172
+ STDERR.puts "invalid partition number '#{x.join(':')}'."
173
+ return S_NG
174
+ end
175
+ else
176
+ STDERR.puts "invalid argument '#{x.join(':')}'."
177
+ return S_NG
178
+ end
179
+ end
180
+
181
+ dumper = case @format
182
+ when CsvDumper.myname
183
+ CsvDumper.new(@output || STDOUT)
184
+ when TchDumper.myname
185
+ TchDumper.new @output
186
+ else
187
+ DefaultDumper.new(@output || STDOUT)
188
+ end
189
+
190
+ hosts.each do |hostname,port,partition|
191
+ Flare::Tools::Node.open(hostname, port.to_i, config[:timeout], 0, @bwlimit) do |n|
192
+ interval = 0
193
+ part, partsize = if @raw
194
+ [0, 1]
195
+ else
196
+ [partition, partition_size]
197
+ end
198
+ bwlimit = @bwlimit/1024/SizeOfByte
199
+ count = 0
200
+ STDERR.print "dumping from #{hostname}:#{port}::#{part} of #{partsize} partitions ... "
201
+ n.dump(interval, part, partsize, bwlimit) do |data, key, flag, len, version, expire|
202
+ dumper.write data, key, flag, len, version, expire
203
+ count += 1
204
+ false
205
+ end
206
+ STDERR.puts "#{count}"
207
+ end
208
+ end
209
+
210
+ dumper.close
211
+
212
+ S_OK
213
+ end # execute()
214
+
215
+ end
216
+ end
217
+ end
218
+ end
219
+
@@ -0,0 +1,117 @@
1
+ # -*- coding: utf-8; -*-
2
+ # Authors:: Kiyoshi Ikehara <kiyoshi.ikehara@gree.net>
3
+ # Copyright:: Copyright (C) GREE, Inc. 2011.
4
+ # License:: MIT-style
5
+
6
+ require 'flare/tools/node'
7
+ require 'flare/tools/index_server'
8
+ require 'flare/tools/common'
9
+ require 'flare/util/conversion'
10
+ require 'flare/util/constant'
11
+ require 'flare/util/bwlimit'
12
+ require 'flare/tools/cli/sub_command'
13
+
14
+ require 'csv'
15
+
16
+ module Flare
17
+ module Tools
18
+ module Cli
19
+
20
+ class Dumpkey < SubCommand
21
+ include Flare::Util::Conversion
22
+ include Flare::Util::Constant
23
+ include Flare::Tools::Common
24
+
25
+ myname :dumpkey
26
+ desc "dump key from nodes."
27
+ usage "dumpkey [hostname:port] ..."
28
+
29
+ def setup(opt)
30
+ opt.on('-o', '--output=FILE', "output to file" ) {|v| @output = v}
31
+ opt.on('-f', '--format=FORMAT', "output format [csv]" ) {|v| @format = v}
32
+ opt.on('-p', '--partition=NUMBER', "partition number" ) {|v| @part = v.to_i if v.to_i >= 0}
33
+ opt.on('-s', '--partition-size=SIZE', "partition size" ) {|v| @partsize = v.to_i if v.to_i > 0}
34
+ opt.on( '--bwlimit=BANDWIDTH', "bandwidth limit (bps)" ) {|v| @bwlimit = v if v.to_i > 0}
35
+ opt.on( '--all', "dump form all partitions") {@all = true}
36
+ end
37
+
38
+ def initialize
39
+ super
40
+ @output = nil
41
+ @format = nil
42
+ @part = nil
43
+ @partsize = nil
44
+ @bwlimit = nil
45
+ @all = false
46
+ end
47
+
48
+ def execute(config, *args)
49
+ cluster = nil
50
+ Flare::Tools::IndexServer.open(config[:index_server_hostname], config[:index_server_port], config[:timeout]) do |s|
51
+ cluster = Flare::Tools::Cluster.new(s.host, s.port, s.stats_nodes)
52
+ end
53
+ return S_NG if cluster.nil?
54
+
55
+ if @all
56
+ unless args.empty?
57
+ STDERR.puts "don't specify any nodes with --all option."
58
+ return S_NG
59
+ else
60
+ args = cluster.master_nodekeys
61
+ end
62
+ else
63
+ if args.empty?
64
+ STDERR.puts "please specify --all option to get complete dump."
65
+ return S_NG
66
+ end
67
+ end
68
+
69
+ unless @format.nil?
70
+ unless ["csv"].include? @format
71
+ puts "unknown format: #{@format}"
72
+ return S_NG
73
+ end
74
+ end
75
+
76
+ hosts = args.map {|x| x.split(':')}
77
+ hosts.each do |x|
78
+ if x.size != 2
79
+ puts "invalid argument '#{x.join(':')}'."
80
+ return S_NG
81
+ end
82
+ end
83
+
84
+ hosts.each do |hostname,port|
85
+ Flare::Tools::Node.open(hostname, port.to_i, config[:timeout], @bwlimit, @bwlimit) do |n|
86
+ output = STDOUT
87
+ unless @output.nil?
88
+ output = File.open(@output, "w")
89
+ end
90
+ case @format
91
+ when "csv"
92
+ writer = CSV::Writer.generate(output)
93
+ output.puts "# key"
94
+ end
95
+ interruptible {
96
+ n.dumpkey(@part, @partsize) do |key|
97
+ case @format
98
+ when "csv"
99
+ writer << [key]
100
+ else
101
+ output.puts "#{key}"
102
+ end
103
+ false
104
+ end
105
+ }
106
+ output.close if output != STDOUT
107
+ end
108
+ end
109
+
110
+ S_OK
111
+ end # execute()
112
+
113
+ end
114
+ end
115
+ end
116
+ end
117
+
@@ -0,0 +1,81 @@
1
+ # -*- coding: utf-8; -*-
2
+ # Authors:: Kiyoshi Ikehara <kiyoshi.ikehara@gree.net>
3
+ # Copyright:: Copyright (C) GREE, Inc. 2011.
4
+ # License:: MIT-style
5
+
6
+ require 'optparse'
7
+ require 'flare/util/logging'
8
+ require 'flare/util/constant'
9
+ require 'flare/tools'
10
+ require 'flare/tools/cli'
11
+ require 'flare/tools/cli/cli_util'
12
+
13
+ require 'flare/util/command_line'
14
+
15
+ Version = Flare::Tools::VERSION
16
+ include Flare::Util::Logging
17
+ include Flare::Util::Constant
18
+ include Flare::Tools::Cli::CliUtil
19
+ Cli = Flare::Tools::Cli
20
+
21
+ index_server_hostname = nil
22
+ index_server_port = nil
23
+ timeout = DefaultTimeout
24
+ dry_run = false
25
+ cluster = nil
26
+ scname = ''
27
+ subc = nil
28
+
29
+ scclasses = [Cli::List, Cli::Balance, Cli::Down, Cli::Slave, Cli::Reconstruct, Cli::Master, Cli::Threads, Cli::Ping, Cli::Remove, Cli::Index, Cli::Activate, Cli::Dump, Cli::Dumpkey, Cli::Verify, Cli::Stats, Cli::Restore, Cli::Summary]
30
+ unsupported = [Cli::Deploy]
31
+ scclasses.concat unsupported
32
+
33
+ subcommands = Hash[*scclasses.map {|x| [x.to_sym, x]}.flatten]
34
+
35
+ setup do |opt|
36
+ opt.banner = "#{Flare::Tools::TITLE}\nUsage: flare-admin [subcommand] [options] [arguments]"
37
+ opt.on("-n", '--dry-run', "dry run") {dry_run = true}
38
+ opt.on("-i HOSTNAME", '--index-server=HOSTNAME', "index server hostname(default:#{DefaultIndexServerName})") {|v| index_server_hostname = v}
39
+ opt.on("-p PORT", '--index-server-port=PORT', "index server port(default:#{DefaultIndexServerPort})") {|v| index_server_port = v.to_i}
40
+ opt.on( '--log-file=LOGFILE', "output log to LOGFILE") {|v| Flare::Util::Logging.set_logger(v)}
41
+ opt.on( '--cluster=NAME', "specify a cluster name") {|v| cluster = v}
42
+ opt.on( '--timeout=SECOND', "specify timeout") {|v| timeout = v.to_i}
43
+
44
+ preparsed = opt.order(ARGV)
45
+ scname = preparsed.shift.to_sym if preparsed.size > 0
46
+
47
+ if subcommands.include?(scname)
48
+ subc = subcommands[scname].new
49
+ opt.separator("#{scname} subcommand:")
50
+ subc.setup(opt)
51
+ else
52
+ error "unknown subcommand '#{scname}'" unless scname == ''
53
+ opt.separator("subcommands:")
54
+ puts opt.help
55
+ subcommands.each do |k,v|
56
+ next if unsupported.include?(v)
57
+ o = OptionParser.new
58
+ o.banner = "[#{k.to_s}] "+v.desc
59
+ o.separator(" Usage: flare-admin "+v.usage)
60
+ v.new.setup(o)
61
+ puts o.help
62
+ end
63
+ exit 1
64
+ end
65
+ end
66
+
67
+ status = execute do |args|
68
+ command = args.shift
69
+ ihostname, iport = get_index_server_from_nodekeys(args) ||
70
+ get_index_server_name_and_port(index_server_hostname, index_server_port)
71
+ ihostname, iport = get_index_server_from_cluster(cluster) unless cluster.nil?
72
+ subc.execute({ :command => command,
73
+ :index_server_hostname => ihostname,
74
+ :index_server_port => iport,
75
+ :dry_run => dry_run,
76
+ :timeout => timeout,
77
+ :cluster => cluster },
78
+ *args) if subc
79
+ end
80
+
81
+ exit status