site24x7_apminsight 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,242 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'uri'
4
+ require 'json'
5
+ require "agent/server/instrument/am_instrumenter"
6
+
7
+ module ManageEngine
8
+ class APMConnector
9
+
10
+ def initialize
11
+ @obj = ManageEngine::APMObjectHolder.instance
12
+ @pretry =0
13
+ @gretry =0
14
+ end
15
+
16
+ def post uri,data
17
+ @pretry = @pretry +1
18
+ begin
19
+
20
+ u = url uri
21
+ #@obj.log.info "[connector] [ POST] START"
22
+ @obj.log.debug "[connector] [ POST] : \n\n#{u}\n\n#{data}\n\n"
23
+ con = connection(u)
24
+ req = Net::HTTP::Post.new(u.request_uri,initheader = {'Content-Type' =>'application/json'})
25
+ req.body=data.to_json
26
+ resp = con.request(req)
27
+ @obj.log.debug "[connector] [POST ] \n Response : #{resp} \nResponse Code : #{resp.code}\nMessage : #{resp.message}\nBody : #{resp.body}"
28
+ rdata = responseParser resp
29
+ @pretry = 0
30
+ #@obj.log.info "[connector] [ POST] END"
31
+ return rdata
32
+ rescue Exception=>e
33
+ @obj.log.logException "[connector] Exception while connecting server- Data not sent \n",e
34
+ if @pretry >=@obj.config.connection_retry
35
+ #@obj.shutdown= true
36
+ return nil
37
+ else
38
+ @obj.log.info "[connector] Exception found in Post request - Retrying - Count - #{@pretry}"
39
+ return post uri,data
40
+ end
41
+
42
+ end
43
+ @pretry = 0
44
+ end
45
+
46
+ def get uri
47
+ @gretry = @gretry +1
48
+ begin
49
+ u = url uri
50
+ #@obj.log.info "[connector] [ GET ] START"
51
+ @obj.log.debug "[connector] [ GET] : \n#{u}\n"
52
+ req = Net::HTTP::Get.new(u.request_uri)
53
+ resp = con.request(req)
54
+ #@obj.log.info "[connector] [ GET ] END"
55
+ rescue Exception=>e
56
+ @obj.log.logException "[connector] [ GET] Exception while connecting server - Data not sent ",e
57
+ if @pretry >=@obj.config.connection_retry
58
+ #@obj.shutdown= true
59
+ else
60
+ @obj.log.info "[connector] Exception found in Get request - Retrying - Count - #{@gretry}"
61
+ return get uri
62
+ end
63
+ end
64
+ @gretry = 0
65
+ end
66
+
67
+ def url(uri)
68
+ ru=nil
69
+ p="https"
70
+ if(!@obj.config.is_secured)
71
+ p="http"
72
+ end
73
+ if(@obj.config.license_key != nil)
74
+ if(!@obj.config.license_key.empty?)
75
+ if(@obj.config.apmhost != nil && !@obj.config.apmhost.empty?)
76
+ u = @obj.config.apmhost+uri
77
+ else
78
+ u = @obj.constants.site24x7url+uri
79
+ end
80
+ else
81
+ #empty license key - print error
82
+ @obj.log.info "license key is present, but empty"
83
+ end
84
+ else
85
+ @obj.log.info "license key is null"
86
+ u = p+"://"+@obj.config.apmhost+":#{@obj.config.apmport}/"+uri
87
+ end
88
+ begin
89
+ ru = URI.parse(u)
90
+ rescue
91
+ raise URI::InvalidURIError, "Invalid url '#{ru}'"
92
+ end
93
+
94
+ if (ru.class != URI::HTTP && ru.class != URI::HTTPS)
95
+ raise URI::InvalidURIError, "Invalid url '#{u}'"
96
+ end
97
+ ru
98
+ end
99
+
100
+ def connection(url)
101
+
102
+ if (@obj.config.proxyneeded)
103
+ @obj.log.debug "[connect] Through Proxy"
104
+ con = Net::HTTP::Proxy(@obj.config.proxy_host, @obj.config.proxy_port,@obj.config.proxy_user,@obj.config.proxy_pass).new(url.host, url.port)
105
+ else
106
+ #@obj.log.info "Proxy Not Needed #{url.host} #{url.port}"
107
+ con = Net::HTTP.new(url.host, url.port)
108
+ con.use_ssl=true
109
+ con.verify_mode=OpenSSL::SSL::VERIFY_NONE
110
+ #@obj.log.info "connection = #{con}"
111
+ end
112
+ con=getScheme(con)
113
+ con.open_timeout = @obj.constants.connection_open_timeout
114
+ con.read_timeout = @obj.constants.connection_read_timeout
115
+ con
116
+ end
117
+
118
+ def getScheme(con)
119
+ if(@obj.config.is_secured)
120
+ #@obj.log.info "[connect] Secured"
121
+ con = Net::HTTP::Proxy(@obj.config.proxy_host, @obj.config.proxy_port,@obj.config.proxy_user,@obj.config.proxy_pass).new(url.host, url.port)
122
+ con.use_ssl=true
123
+ con.verify_mode=OpenSSL::SSL::VERIFY_NONE
124
+ end
125
+ con
126
+ end
127
+
128
+ def responseParser resp
129
+ if resp == Net::HTTPSuccess || Net::HTTPOK
130
+ rawData = resp.body
131
+ if rawData.length>=2
132
+ rBody = JSON.parse(rawData)
133
+ result = rBody["result"]
134
+ data = rBody["data"]
135
+ if !@obj.util.getBooleanValue result
136
+ if data!=nil
137
+ if data.has_key?("exception")
138
+ raise Exception.new("Exception from server - "+data["exception"])
139
+ end
140
+ end
141
+
142
+ end
143
+ if data!=nil
144
+ if data.has_key?(@obj.constants.response_code)
145
+ srCode = data[@obj.constants.response_code]
146
+ response_action srCode
147
+ end
148
+ if data.has_key?(@obj.constants.custom_config_info)
149
+ config_info = data[@obj.constants.custom_config_info]
150
+ update_config config_info
151
+ end
152
+ end
153
+ return data
154
+ end
155
+ return rawData
156
+ else
157
+ raise Exception.new("Http Connection Response Error #{resp.to_s}")
158
+ end
159
+ end
160
+
161
+
162
+
163
+ def response_action rCode
164
+ case rCode
165
+ when @obj.constants.licence_expired then
166
+ @obj.log.info "License Expired. Going to shutdown"
167
+ raise Exception.new("License Expired. Going to shutdown")
168
+ when @obj.constants.licence_exceeds then
169
+ @obj.log.info "License Exceeds. Going to shutdown"
170
+ raise Exception.new("License Exceeds. Going to shutdown")
171
+ when @obj.constants.delete_agent then
172
+ @obj.log.info "Action from Server - Delete the Agent. Going to shutdown and remove the Agent"
173
+ deleteAgent
174
+ raise Exception.new("Action from Server - Delete the Agent. Going to shutdown and remove the Agent")
175
+ when @obj.constants.unmanage_agent then
176
+ @obj.log.info "Action from Server - Unmanage the Agent. Going to Stop the DC - Disabling the Agent"
177
+ unManage
178
+ when @obj.constants.manage_agent then
179
+ @obj.log.info "Action from Server - Manage the Agent. Going to Sart the DC - Enabling the Agent"
180
+ manage
181
+ end
182
+ end
183
+
184
+ def update_config configInfo
185
+ existingConfigInfo = @obj.config.getAgentConfigData
186
+ sendUpdate = "false"
187
+ existingConfigInfo.each do|key,value|
188
+ if key != "last.modified.time"
189
+ newValue = configInfo[key]
190
+ if key == "sql.capture.enabled" || key == "transaction.trace.enabled" || key == "transaction.trace.sql.parametrize"
191
+ if newValue
192
+ newValue = 1
193
+ else
194
+ newValue = 0
195
+ end
196
+ end
197
+ if value != newValue
198
+ sendUpdate = "true"
199
+ end
200
+ end
201
+ end
202
+ if sendUpdate == "true"
203
+ @obj.log.info "Action from Server - Agent configuration updated from UI. Going to update the same in apminsight.conf file"
204
+ @obj.log.info "config info = #{configInfo}"
205
+ @obj.config.update_config configInfo
206
+ end
207
+ end
208
+
209
+ def unManage
210
+ @obj.instrumenter.doUnSubscribe
211
+ @obj.instrumenter =nil
212
+ @obj.instrumenter = ManageEngine::APMInstrumenter.new
213
+ uManage = Hash.new
214
+ uManage["agent.id"]=@obj.config.instance_id
215
+ uManage["agent.enabled"]=false
216
+ @obj.config.updateAgentInfoFile uManage
217
+ end
218
+
219
+ def manage
220
+ @obj.instrumenter.doSubscribe
221
+ uManage = Hash.new
222
+ uManage["agent.id"]=@obj.config.instance_id
223
+ uManage["agent.enabled"]=true
224
+ @obj.config.updateAgentInfoFile uManage
225
+ end
226
+
227
+ def deleteAgent
228
+ @obj.instrumenter.doUnSubscribe
229
+ @obj.instrumenter =nil
230
+ uManage = Hash.new
231
+ uManage["agent.id"]=@obj.config.instance_id
232
+ uManage["agent.enabled"]=false
233
+ @obj.config.updateAgentInfoFile uManage
234
+ begin
235
+ File.delete(@obj.constants.agent_conf)
236
+ rescue Exceptione=>e
237
+ @obj.log.logException "#{e.message}",e
238
+ end
239
+ end
240
+
241
+ end#c
242
+ end#m
@@ -0,0 +1,99 @@
1
+ require 'agent/am_objectholder'
2
+ @obj = ManageEngine::APMObjectHolder.instance
3
+ class Class
4
+ alias old_new new
5
+ def new(*args, &block)
6
+ result =nil;
7
+ begin
8
+ if(block==nil || block=="")
9
+ result = old_new(*args)
10
+ elsif
11
+ result = old_new(*args,&block)
12
+ end
13
+ rescue Excetion=>exe
14
+ raise exe
15
+ result = self
16
+ end
17
+ me_apm_injector(self,result)
18
+
19
+ return result
20
+ end
21
+ end
22
+
23
+ def me_apm_injector(s,result)
24
+ begin
25
+ if(ManageEngine::APMObjectHolder.instance.config.include_packages.index(s.name)!=nil)
26
+ ms =s.instance_methods(false)
27
+ cms = s.methods(false)
28
+ begin
29
+ ms.each do |m|
30
+ if( m.to_s.index("APMTEST"))
31
+ return;
32
+ end
33
+ end
34
+ cms.each do |m|
35
+ if( m.to_s.index("APMTEST"))
36
+ return;
37
+ end
38
+ end
39
+ rescue Exception=>e
40
+ return;
41
+ end
42
+ ManageEngine::APMObjectHolder.instance.log.debug "Injection Method : #{ms} "
43
+ ManageEngine::APMObjectHolder.instance.log.debug "Injection Class Method : #{cms} "
44
+ ms.each do |m|
45
+ mn = m.to_s
46
+ #ManageEngine::APMObjectHolder.instance.log.info "ManageEngine Monitor Method : #{s.name} # #{m.to_s}"
47
+ omn = "APMTEST"+mn+"APMTEST"
48
+ s.class_eval %{
49
+ alias_method :#{omn}, :#{mn}
50
+ def #{mn} *args, &block
51
+ begin
52
+ ActiveSupport::Notifications.instrument("apm.methodstart", {:method=>"#{mn}",:args=>args})
53
+ res = #{omn} *args, &block
54
+ ActiveSupport::Notifications.instrument("apm.methodend", {:method=>"#{mn}",:args=>args})
55
+ return res
56
+ rescue Exception => exe
57
+ puts "error in calling method"
58
+ raise exe
59
+ ensure
60
+ end
61
+ end
62
+ }
63
+ end#do
64
+ default_methods = Array.new
65
+ default_methods.push("_helpers");
66
+ default_methods.push("middleware_stack");
67
+ default_methods.push("helpers_path");
68
+ default_methods.push("_wrapper_options");
69
+ cms.each do |m|
70
+ if(default_methods.index(m.to_s)==nil)
71
+ mn = m.to_s
72
+ #ManageEngine::APMObjectHolder.instance.log.debug "ManageEngine Monitor Singleton Method : #{s.name} ---> #{m.to_s}"
73
+ omn = "APMTEST"+mn+"APMTEST"
74
+ s.instance_eval %{
75
+ class << self
76
+ alias_method :#{omn}, :#{mn}
77
+ end
78
+ def self.#{mn} *args, &block
79
+ begin
80
+ ActiveSupport::Notifications.instrument("apm.methodstart", {:method=>"#{mn}",:args=>args})
81
+ res = #{omn} *args, &block
82
+ ActiveSupport::Notifications.instrument("apm.methodend", {:method=>"#{mn}",:args=>args})
83
+ return res
84
+ rescue Exception=>exe
85
+ puts "Instrument : error in calling class method"
86
+ raise exe
87
+ ensure
88
+ end
89
+ end
90
+ }
91
+ end
92
+ end#do
93
+ end#if
94
+ rescue Exception=>e
95
+ puts "Exception in instrument : #{e}"
96
+ ensure
97
+ end
98
+ end
99
+
@@ -0,0 +1,43 @@
1
+ require 'agent/am_objectholder'
2
+ require 'socket'
3
+ module ManageEngine
4
+ class APMInstrumenter
5
+ @t =nil;
6
+ def initialize
7
+ @obj=ManageEngine::APMObjectHolder.instance
8
+ end
9
+
10
+ def doSubscribe
11
+ @obj=ManageEngine::APMObjectHolder.instance
12
+ @obj.log.debug "[ instrumenter ] [ Subscriber for Agent ]"
13
+ @subscriber = ActiveSupport::Notifications.subscribe do |name, start, finish, id, payload|
14
+ if(ManageEngine::APMObjectHolder.instance.config.agent_enabled)
15
+ rt = (finish-start).to_i
16
+ ManageEngine::APMWorker.getInstance.start
17
+ ManageEngine::APMObjectHolder.instance.log.debug "[ Notifications for Agent ] #{Thread.current} #{id} #{name} - #{rt} - #{payload}"
18
+ trace= caller;
19
+ id = "#{Thread.current}"
20
+ stats = Hash.new
21
+ stats["name"] = name;
22
+ stats["start"] = start.to_f * 1000;
23
+ stats["end"] = finish.to_f * 1000;
24
+ stats["id"] = id;
25
+ stats["payload"] = payload;
26
+ if (name=="sql.active_record" && (finish-start)>=(ManageEngine::APMObjectHolder.instance.config.sql_trace_t * 1000 ).to_i)
27
+ stats["trace"] = trace;
28
+ end
29
+ stats["ctime"] =ManageEngine::APMObjectHolder.instance.util.currenttimemillis;
30
+ ManageEngine::APMObjectHolder.instance.collector.updateTransaction(id,stats);
31
+ else
32
+ ActiveSupport::Notifications.unsubscribe @subscriber
33
+ @obj.log.info "[ instrumenter ] [ RETURNING NO METRICS] "
34
+ end
35
+ end
36
+ end
37
+
38
+ def doUnSubscribe
39
+ ActiveSupport::Notifications.unsubscribe @subscriber
40
+ end
41
+
42
+ end #class
43
+ end#module
@@ -0,0 +1,271 @@
1
+ require 'json'
2
+ require 'thread'
3
+
4
+ module ManageEngine
5
+ class APMWorker
6
+ @work =nil;
7
+ @status = 'not_init'
8
+ @id = 0
9
+ attr_accessor :id
10
+ def initialize
11
+ @status = "initialized"
12
+ @id = Process.pid
13
+ end
14
+
15
+ def start
16
+ @obj = ManageEngine::APMObjectHolder.instance
17
+
18
+ if @status=="working"
19
+ @obj.log.debug "woker thread already started"
20
+ elsif @status == "initialized"
21
+ @obj.log.info "start worker thread for - #{Process.pid} :: #{@status} "
22
+ #@obj.log.info "Starting APMWorker Thread #{Process.pid} "
23
+ @apm = Thread.new do
24
+ @status = 'working'
25
+ while !@obj.shutdown do
26
+ checkforagentstatus
27
+ updateConfig
28
+ dc
29
+ sleep (@obj.config.connect_interval).to_i
30
+ end#w
31
+ @status= "end"
32
+ @obj.log.debug "Worker thread ends"
33
+ end
34
+ end
35
+ end
36
+
37
+ def self.getInstance
38
+ if(@work==nil || @work.id!=Process.pid)
39
+ @work = ManageEngine::APMWorker.new
40
+ end
41
+ return @work
42
+ end
43
+
44
+ def updateConfig
45
+ if(@obj.config.lastupdatedtime!=File.mtime(@obj.constants.apm_conf).to_i)
46
+ @obj.log.info "Configuration File Changed... So Updating Configuration."
47
+ agent_config_data = @obj.config.getAgentConfigData
48
+ @obj.config.lastupdatedtime=File.mtime(@obj.constants.apm_conf).to_i
49
+ @obj.config.configureFile
50
+ @obj.config.assignConfig
51
+ new_agent_config_data = @obj.config.getAgentConfigData
52
+ sendUpdate = "false"
53
+ agent_config_data.each do|key,value|
54
+ if key != "last.modified.time"
55
+ newValue = new_agent_config_data[key]
56
+ if value != newValue
57
+ sendUpdate = "true"
58
+ end
59
+ end
60
+ end
61
+ if sendUpdate == "true"
62
+ @obj.log.info "sending update to server #{new_agent_config_data}"
63
+ data1 = Hash.new
64
+ data1["custom_config_info"]=new_agent_config_data
65
+ resp = @obj.connector.post @obj.constants.connect_config_update_uri+@obj.config.instance_id,data1
66
+ end
67
+ end
68
+ end
69
+
70
+ def checkforagentstatus
71
+ prevState = @obj.config.agent_enabled
72
+ @obj.config.checkAgentInfo
73
+ if !@obj.config.agent_enabled
74
+ @obj.log.info "Agent in Disabled State."
75
+ if prevState
76
+ @obj.log.info "Agent in Disabled State. Going to unsubscribe"
77
+ @obj.instrumenter.doUnSubscribe
78
+ end
79
+ else
80
+ if !prevState
81
+ @obj.log.info "Agent in Active State."
82
+ @obj.instrumenter.doSubscribe
83
+ end
84
+ end
85
+ end
86
+
87
+ def stop
88
+ dc
89
+ @obj.shutdown = true;
90
+ end
91
+
92
+ def dc
93
+ begin
94
+ @obj.log.debug "[dc] collecting..."
95
+ now = @obj.util.currenttimemillis
96
+ result = Array.new
97
+ result.push(@obj.last_dispatch_time)
98
+ result.push(now)
99
+ data = Array.new
100
+ trd= nil;
101
+ @last_dispatch_time = now
102
+ if @obj.config.agent_enabled
103
+ d = @obj.parser.parse @obj.store.metrics_dup
104
+ if(d!=nil && d.has_key?("trace-data"))
105
+ trd = d.delete("trace-data");
106
+ #@obj.log.info "[dc] [TRACE] : #{d}"
107
+ end
108
+ #@obj.log.info "[dc] Data - #{d}"
109
+ if(d.length>0)
110
+ data =@obj.formatter.format d
111
+ #@obj.log.debug "[dc] Formatted Data - #{data}"
112
+ end
113
+ @obj.store.remove @obj.formatter.keysToRemove
114
+ end #if
115
+ fd = Array.new
116
+ fd.push(data)
117
+ if(trd!=nil)
118
+ fd.push(trd)
119
+ end
120
+ @obj.log.debug "[dc] data to store : #{fd}"
121
+ send_save fd
122
+ @obj.log.debug "[dc] collecting ends"
123
+ rescue Exception=>e
124
+ @obj.log.logException "[dc] Exception during data Collection. #{e.message}",e
125
+ @obj.shutdown=true
126
+ end
127
+ end
128
+
129
+ def senddata d
130
+ # @obj.log.info("Send data --- #{d}")
131
+ result = Array.new
132
+ result.push( (File.mtime(@obj.constants.agent_lock).to_f*1000).to_i)
133
+ now = @obj.util.currenttimemillis
134
+ result.push(now)
135
+ write @obj.constants.agent_lock ,"#{Process.pid}"
136
+ data = read @obj.constants.agent_store
137
+ data.push(d);
138
+ tdata = Array.new;
139
+ trdata = Array.new;
140
+ data.each do |val|
141
+ case val.size
142
+ when 1
143
+ tdata.concat(val[0])
144
+ when 2
145
+ tdata.concat(val[0])
146
+ trdata.concat(val[1])
147
+ end
148
+ end
149
+ result.push(merge(tdata))
150
+ resp = @obj.connector.post @obj.constants.connect_data_uri+@obj.config.instance_id,result
151
+ if trdata.size>0
152
+ result[2]=trdata;
153
+ resp = @obj.connector.post @obj.constants.connect_trace_uri+@obj.config.instance_id,result
154
+ end
155
+ end
156
+
157
+ def save fd
158
+ begin
159
+ data = fd.to_json;
160
+ write @obj.constants.agent_store,data
161
+ rescue Exception=>e
162
+ @obj.log.logException "[dc] Exception during save. #{e.message}",e
163
+ end
164
+ end
165
+
166
+ def send_save data
167
+ begin
168
+ if FileTest.exist?(@obj.constants.agent_lock)
169
+ if Time.now.to_i - File.mtime(@obj.constants.agent_lock).to_i >= (@obj.config.connect_interval).to_i
170
+ @obj.log.debug "worker send signal"
171
+ senddata data
172
+ else
173
+ @obj.log.info "worker save signal"
174
+ save data
175
+ end
176
+ else
177
+ @obj.log.info "worker save signals"
178
+ save data
179
+ write @obj.constants.agent_lock,"#{Process.pid}"
180
+ end
181
+ rescue Exception=>e
182
+ @obj.log.logException "Exception in decision making send or save #{e.message}",e
183
+ end
184
+ end
185
+
186
+ def read p
187
+ data = Array.new
188
+ File.open( p, "r+" ) { |f|
189
+ f.flock(File::LOCK_EX)
190
+ begin
191
+ f.each_line do |line|
192
+ data.push(JSON.parse(line))
193
+ end
194
+ f.truncate 0
195
+ rescue Exception=>e
196
+ @obj.log.logException "Exception while reading data #{e}",e
197
+ ensure
198
+ f.flock(File::LOCK_UN)
199
+ end
200
+ }
201
+ data
202
+ end
203
+
204
+
205
+ def write (p, data )
206
+ File.open( p, "a+" ) { |f|
207
+ f.flock(File::LOCK_EX)
208
+ begin
209
+ f.write "#{data}\n"
210
+ rescue Exception=>e
211
+ @obj.log.logException "Exception while writing data #{e.message}",e
212
+ ensure
213
+ f.flock(File::LOCK_UN)
214
+ end
215
+ }
216
+ end
217
+
218
+ def merge data
219
+ # @obj.log.info "BEFORE MERGE : #{data}"
220
+ tdata =Hash.new ;
221
+ data.each do |sd|
222
+ name= sd[0]["ns"] + sd[0]["name"];
223
+ if tdata.has_key?(name)
224
+ if (sd[0]["name"]=="apdex")
225
+ tdata[name][1] = mapdx(tdata[name][1],sd[1])
226
+ else
227
+ tdata[name][1] = mapdb(tdata[name][1],sd[1])
228
+ end
229
+ else
230
+ tdata[name]=sd;
231
+ end
232
+ end
233
+ #@obj.log.info "MERGED DATA : #{tdata}"
234
+ res = Array.new;
235
+ tdata.each do|key,value|
236
+ res.push(value);
237
+ end
238
+ res
239
+ end
240
+
241
+
242
+ def mapdx res,dat
243
+ res[0] = res[0]+dat[0];
244
+ if dat[1]<res[1]
245
+ res[1]=dat[1]
246
+ end
247
+ if dat[2]>res[2]
248
+ res[2]=dat[2]
249
+ end
250
+ res[3] = res[3]+dat[3]
251
+ res[5] = res[5]+dat[5]
252
+ res[6] = res[6]+dat[6]
253
+ res[7] = res[7]+dat[7]
254
+ res[4] = (res[5].to_f + (res[6].to_f/2).to_f).to_f/res[3].to_f
255
+ res
256
+ end
257
+
258
+ def mapdb res,dat
259
+ res[0] = res[0]+dat[0];
260
+ if dat[1]<res[1]
261
+ res[1]=dat[1]
262
+ end
263
+ if dat[2]>res[2]
264
+ res[2]=dat[2]
265
+ end
266
+ res[3] = res[3]+dat[3]
267
+ res
268
+ end
269
+
270
+ end#c
271
+ end#m