From a079801cefb758fef27beec14da262080e5e3b49 Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Sat, 24 Nov 2012 18:05:28 +0400 Subject: [PATCH 01/13] =?UTF-8?q?=E2=80=94=20initializing=20is=20mutexed?= =?UTF-8?q?=20now=20because=20=E2=80=9Cunless=20@ready=E2=80=9D=20is=20not?= =?UTF-8?q?=20sufficient;=20=E2=80=94=C2=A0last=20Fiber=20removed=20from?= =?UTF-8?q?=20Connection=20class=20in=20honour=20of=20EventMachine::Iterat?= =?UTF-8?q?or=20;=20=E2=80=94=C2=A0=20some=20cosmetic=20changes.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Rakefile | 17 ++++++++++--- lib/vines/agent/connection.rb | 46 +++++++++++++++++++++++------------ 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/Rakefile b/Rakefile index b66de1e..9ae2847 100644 --- a/Rakefile +++ b/Rakefile @@ -22,8 +22,8 @@ component. Manage a server as easily as chatting with a friend." s.executables = %w[vines-agent] s.require_path = "lib" - s.add_dependency "blather", "~> 0.5.12" - s.add_dependency "ohai", "~> 0.6.10" + s.add_dependency "blather", "~> 0.8.1" + s.add_dependency "ohai", "~> 6.14.0" s.add_dependency "session", "~> 3.1.0" s.add_dependency "slave", "~> 1.3.0" s.add_dependency "vines", ">= 0.4.0" @@ -31,7 +31,7 @@ component. Manage a server as easily as chatting with a friend." s.add_development_dependency "minitest" s.add_development_dependency "rake" - s.required_ruby_version = '>= 1.9.2' + s.required_ruby_version = '>= 2.0.0' end Gem::PackageTask.new(spec) do |pkg| @@ -55,3 +55,14 @@ Rake::TestTask.new(:test) do |test| end task :default => [:clobber, :test, :gem] + +desc 'Helper task to be called from other rakefiles' +task :agent => [:clobber, :gem] + +# FIXME Remove from production +desc 'Runs an agent from command line' +task :run do + Dir.chdir("/home/am/NetBeansProjects/Repos/wonderland.lit/agent") do + sh "ruby -I/home/am/NetBeansProjects/vines-agent.git/lib /home/am/NetBeansProjects/vines-agent.git/bin/vines-agent start" + end +end diff --git a/lib/vines/agent/connection.rb b/lib/vines/agent/connection.rb index 769788d..20f34b8 100644 --- a/lib/vines/agent/connection.rb +++ b/lib/vines/agent/connection.rb @@ -1,5 +1,7 @@ # encoding: UTF-8 +require 'thread' + module Vines module Agent @@ -19,6 +21,7 @@ def initialize(options) certs = File.expand_path('certs', conf) @permissions, @services, @sessions, @component = {}, {}, {}, nil @ready = false + @mtx = Mutex.new jid = Blather::JID.new(fqdn, domain, 'vines') @stream = Blather::Client.setup(jid, password, host, port, certs) @@ -37,13 +40,16 @@ def initialize(options) end @stream.register_handler(:ready) do - # prevent handler called twice - unless @ready - log.info("Connected #{@stream.jid} agent to #{host}:#{port}") - log.warn("Agent must run as root user to allow user switching") unless root? - @ready = true - startup - end + # [AM] making sure we are to init once + # unless @ready is not enough for an obvious reason + @mtx.synchronize { + unless @ready + log.info("Connected #{@stream.jid} agent to #{host}:#{port}") + log.warn("Agent must run as root user to allow user switching") unless root? + @ready = true + startup + end + } end @stream.register_handler(:subscription, :request?) do |node| @@ -77,17 +83,20 @@ def start @stream.run end +# ————————————————————————————————————————————————————————————————————————— private +# ————————————————————————————————————————————————————————————————————————— # After the bot connects to the chat server, discover the component, send # our ohai system description data, and initialize permissions. def startup - cb = proc do |component| + cb = lambda do |component, iter| if component log.info("Found vines component at #{component}") - @component = component + @component = component.jid send_system_info request_permissions + iter.next else log.info("Vines component not found, rediscovering . . .") EM::Timer.new(10) { discover_component(&cb) } @@ -153,6 +162,7 @@ def send_system_info end iq = Blather::Stanza::Iq::Query.new(:set).tap do |node| node.to = @component + log.info "Sending system info to @component [#{@component}]" node.query.content = system.to_json node.query.namespace = SYSTEMS end @@ -172,19 +182,17 @@ def fqdn # server for its list of components, then ask each component for it's info. # The component broadcasting the http://getvines.com/protocol feature is our # Vines service. - def discover_component + def discover_component(&cb) disco = Blather::Stanza::DiscoItems.new disco.to = @stream.jid.domain @stream.write_with_handler(disco) do |result| - items = result.error? ? [] : result.items - Fiber.new do - # use fiber instead of EM::Iterator until EM 1.0.0 release - found = items.find {|item| component?(item.jid) } - yield found ? found.jid : nil - end.resume + unless result.error? + EM::Iterator.new(result.items).each &cb + end end end + # Return true if this JID is the Vines component with which we need to # communicate. This method suspends the Fiber that calls it in order to # turn the disco#info requests synchronous. @@ -195,6 +203,7 @@ def component?(jid) @stream.write_with_handler(info) do |reply| features = reply.error? ? [] : reply.features found = !!features.find {|f| f.var == NS } + log.info "Component found: " + found.to_s fiber.resume(found) end Fiber.yield @@ -209,14 +218,18 @@ def request_permissions node.query['name'] = @stream.jid.node node.query.namespace = SYSTEMS end + log.info "Request permissions with #{iq}" @stream.write_with_handler(iq) do |reply| + log.info "Reply is: " + reply.to_s update_permissions(reply) unless reply.error? end end def update_permissions(node) + log.info "Update permissions: " + node.to_s return unless node.from == @component obj = JSON.parse(node.content) rescue {} + log.info "Obj parsed: " + obj.to_s @permissions = obj['permissions'] || {} @services = (obj['services'] || {}).map {|s| s['jid'] } @sessions.values.each {|shell| shell.permissions = @permissions } @@ -265,6 +278,7 @@ def reply(message, body, forward_to) def valid_user?(jid) jid = jid.stripped.to_s if jid.respond_to?(:stripped) valid = !!@permissions.find {|unix, jids| jids.include?(jid) } + log.info("Trying to lookup perms " + @permissions.to_s) log.warn("Denied access to #{jid}") unless valid valid end From 4dda7a35779c5523729a33338c866ea26f0907a5 Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Sat, 24 Nov 2012 18:07:49 +0400 Subject: [PATCH 02/13] Changed extension so that github is able to render it. --- README => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README => README.md (100%) diff --git a/README b/README.md similarity index 100% rename from README rename to README.md From 732ee07132fb5e78123250b9d072e2b0be7cfec2 Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Sat, 24 Nov 2012 21:32:42 +0400 Subject: [PATCH 03/13] Setting version of Ruby to 2 is too early. --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 9ae2847..81b2b0c 100644 --- a/Rakefile +++ b/Rakefile @@ -31,7 +31,7 @@ component. Manage a server as easily as chatting with a friend." s.add_development_dependency "minitest" s.add_development_dependency "rake" - s.required_ruby_version = '>= 2.0.0' + s.required_ruby_version = '>= 1.9.3' end Gem::PackageTask.new(spec) do |pkg| From a69cf348b9e6f0c46e69e155af4820de29a23395 Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Sun, 25 Nov 2012 12:01:06 +0400 Subject: [PATCH 04/13] Removed unnecessary log messages (previously added to debug real process.) --- lib/vines/agent/connection.rb | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/vines/agent/connection.rb b/lib/vines/agent/connection.rb index 20f34b8..e5d8f58 100644 --- a/lib/vines/agent/connection.rb +++ b/lib/vines/agent/connection.rb @@ -1,7 +1,5 @@ # encoding: UTF-8 -require 'thread' - module Vines module Agent @@ -162,7 +160,6 @@ def send_system_info end iq = Blather::Stanza::Iq::Query.new(:set).tap do |node| node.to = @component - log.info "Sending system info to @component [#{@component}]" node.query.content = system.to_json node.query.namespace = SYSTEMS end @@ -203,7 +200,6 @@ def component?(jid) @stream.write_with_handler(info) do |reply| features = reply.error? ? [] : reply.features found = !!features.find {|f| f.var == NS } - log.info "Component found: " + found.to_s fiber.resume(found) end Fiber.yield @@ -218,18 +214,14 @@ def request_permissions node.query['name'] = @stream.jid.node node.query.namespace = SYSTEMS end - log.info "Request permissions with #{iq}" @stream.write_with_handler(iq) do |reply| - log.info "Reply is: " + reply.to_s update_permissions(reply) unless reply.error? end end def update_permissions(node) - log.info "Update permissions: " + node.to_s return unless node.from == @component obj = JSON.parse(node.content) rescue {} - log.info "Obj parsed: " + obj.to_s @permissions = obj['permissions'] || {} @services = (obj['services'] || {}).map {|s| s['jid'] } @sessions.values.each {|shell| shell.permissions = @permissions } @@ -278,7 +270,6 @@ def reply(message, body, forward_to) def valid_user?(jid) jid = jid.stripped.to_s if jid.respond_to?(:stripped) valid = !!@permissions.find {|unix, jids| jids.include?(jid) } - log.info("Trying to lookup perms " + @permissions.to_s) log.warn("Denied access to #{jid}") unless valid valid end From c7341589f2b0f30254d2484966f551cfd626a2df Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Sun, 25 Nov 2012 12:27:38 +0400 Subject: [PATCH 05/13] Prepared for pull request. --- README | 32 ++++++++++++++++++++++++++++++++ Rakefile | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 README diff --git a/README b/README new file mode 100644 index 0000000..5b2d70b --- /dev/null +++ b/README @@ -0,0 +1,32 @@ +== Welcome to Vines Agent + +Vines Agent executes shell commands sent by users on remote machines. Users are +authorized against an access control list before being allowed to run commands. +The agent is an XMPP bot that connects to the Vines chat server. It relies on +the component provided by the vines-services gem to send and receive commands. + +Users may run commands as any unix account, to which they've been granted access, +on the system. While the agent runs as root, user commands run as less privileged +accounts. + +Additional documentation can be found at www.getvines.org. + +== Usage + +1. gem install vines-agent +2. vines-agent init wonderland.lit +3. cd wonderland.lit && vines-agent start + +== Dependencies + +Vines Agent requires Ruby 1.9.2 or better. Instructions for installing the +needed OS packages, as well as Ruby itself, are available at +http://www.getvines.org/ruby. + +== Contact + +* David Graham + +== License + +Vines Agent is released under the MIT license. Check the LICENSE file for details. diff --git a/Rakefile b/Rakefile index 81b2b0c..9ae2847 100644 --- a/Rakefile +++ b/Rakefile @@ -31,7 +31,7 @@ component. Manage a server as easily as chatting with a friend." s.add_development_dependency "minitest" s.add_development_dependency "rake" - s.required_ruby_version = '>= 1.9.3' + s.required_ruby_version = '>= 2.0.0' end Gem::PackageTask.new(spec) do |pkg| From 09a341b5bd1e0c57d120417c4b7851c371349511 Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Sun, 25 Nov 2012 12:29:54 +0400 Subject: [PATCH 06/13] Prepared for pull request. --- README.md | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index 5b2d70b..0000000 --- a/README.md +++ /dev/null @@ -1,32 +0,0 @@ -== Welcome to Vines Agent - -Vines Agent executes shell commands sent by users on remote machines. Users are -authorized against an access control list before being allowed to run commands. -The agent is an XMPP bot that connects to the Vines chat server. It relies on -the component provided by the vines-services gem to send and receive commands. - -Users may run commands as any unix account, to which they've been granted access, -on the system. While the agent runs as root, user commands run as less privileged -accounts. - -Additional documentation can be found at www.getvines.org. - -== Usage - -1. gem install vines-agent -2. vines-agent init wonderland.lit -3. cd wonderland.lit && vines-agent start - -== Dependencies - -Vines Agent requires Ruby 1.9.2 or better. Instructions for installing the -needed OS packages, as well as Ruby itself, are available at -http://www.getvines.org/ruby. - -== Contact - -* David Graham - -== License - -Vines Agent is released under the MIT license. Check the LICENSE file for details. From 34c93b816475e88aa51d2f3a73c388f800bc252a Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Sun, 25 Nov 2012 12:30:23 +0400 Subject: [PATCH 07/13] Prepared for pull request. --- Rakefile | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Rakefile b/Rakefile index 9ae2847..b66de1e 100644 --- a/Rakefile +++ b/Rakefile @@ -22,8 +22,8 @@ component. Manage a server as easily as chatting with a friend." s.executables = %w[vines-agent] s.require_path = "lib" - s.add_dependency "blather", "~> 0.8.1" - s.add_dependency "ohai", "~> 6.14.0" + s.add_dependency "blather", "~> 0.5.12" + s.add_dependency "ohai", "~> 0.6.10" s.add_dependency "session", "~> 3.1.0" s.add_dependency "slave", "~> 1.3.0" s.add_dependency "vines", ">= 0.4.0" @@ -31,7 +31,7 @@ component. Manage a server as easily as chatting with a friend." s.add_development_dependency "minitest" s.add_development_dependency "rake" - s.required_ruby_version = '>= 2.0.0' + s.required_ruby_version = '>= 1.9.2' end Gem::PackageTask.new(spec) do |pkg| @@ -55,14 +55,3 @@ Rake::TestTask.new(:test) do |test| end task :default => [:clobber, :test, :gem] - -desc 'Helper task to be called from other rakefiles' -task :agent => [:clobber, :gem] - -# FIXME Remove from production -desc 'Runs an agent from command line' -task :run do - Dir.chdir("/home/am/NetBeansProjects/Repos/wonderland.lit/agent") do - sh "ruby -I/home/am/NetBeansProjects/vines-agent.git/lib /home/am/NetBeansProjects/vines-agent.git/bin/vines-agent start" - end -end From 77839beef617ca7c1c63dd2697b81d79c40a749f Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Sun, 25 Nov 2012 14:05:46 +0400 Subject: [PATCH 08/13] Prepared for pull request. --- lib/vines/agent/connection.rb | 41 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/lib/vines/agent/connection.rb b/lib/vines/agent/connection.rb index e5d8f58..6136d16 100644 --- a/lib/vines/agent/connection.rb +++ b/lib/vines/agent/connection.rb @@ -90,15 +90,30 @@ def start def startup cb = lambda do |component, iter| if component - log.info("Found vines component at #{component}") - @component = component.jid - send_system_info - request_permissions - iter.next + log.info("Found unknown component at #{component}, checking…") + info = Blather::Stanza::DiscoInfo.new + info.to = component.jid.domain + @stream.write_with_handler(info) do |reply| + unless reply.error? + EM::Iterator.new(reply.features).each { |f, it| + if f.var == NS + log.info("Found vines component at #{component}!") + # FIXME What if we have more than one component found? + # We do likely want to have a stack here instead + # of object for the “@component” + @component = component.jid + send_system_info + request_permissions + end + it.next + } + end + end else log.info("Vines component not found, rediscovering . . .") EM::Timer.new(10) { discover_component(&cb) } end + iter.next end discover_component(&cb) end @@ -189,22 +204,6 @@ def discover_component(&cb) end end - - # Return true if this JID is the Vines component with which we need to - # communicate. This method suspends the Fiber that calls it in order to - # turn the disco#info requests synchronous. - def component?(jid) - fiber = Fiber.current - info = Blather::Stanza::DiscoInfo.new - info.to = jid - @stream.write_with_handler(info) do |reply| - features = reply.error? ? [] : reply.features - found = !!features.find {|f| f.var == NS } - fiber.resume(found) - end - Fiber.yield - end - # Download the list of unix user accounts and the JID's that are allowed # to use them. This is used to determine if a change user command like # +v user root+ is allowed. From df30f8e240dca32155c34bb062766d302c9e97f8 Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Sun, 25 Nov 2012 14:47:32 +0400 Subject: [PATCH 09/13] Disco component unverified (w/o discoinfo request) --- lib/vines/agent/connection.rb | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/lib/vines/agent/connection.rb b/lib/vines/agent/connection.rb index 6136d16..0bf1bb0 100644 --- a/lib/vines/agent/connection.rb +++ b/lib/vines/agent/connection.rb @@ -90,25 +90,12 @@ def start def startup cb = lambda do |component, iter| if component - log.info("Found unknown component at #{component}, checking…") - info = Blather::Stanza::DiscoInfo.new - info.to = component.jid.domain - @stream.write_with_handler(info) do |reply| - unless reply.error? - EM::Iterator.new(reply.features).each { |f, it| - if f.var == NS - log.info("Found vines component at #{component}!") - # FIXME What if we have more than one component found? - # We do likely want to have a stack here instead - # of object for the “@component” - @component = component.jid - send_system_info - request_permissions - end - it.next - } - end - end + # Pray for we have no unknown components there + log.info("Found vines component at #{component}!") + # FIXME Deal with DiscoInfo to ensure this component is our + @component = component.jid + send_system_info + request_permissions else log.info("Vines component not found, rediscovering . . .") EM::Timer.new(10) { discover_component(&cb) } From a5f5894172aaf29e37cbe5c345f5f60f79ea3d1e Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Sun, 25 Nov 2012 17:26:06 +0400 Subject: [PATCH 10/13] =?UTF-8?q?Component=20discovery=20is=20turned=20to?= =?UTF-8?q?=20EventMachine,=20handling=20of=20possible=20multiple=20instan?= =?UTF-8?q?ces=20of=20Vines=E2=80=99=20services=20is=20done.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/vines/agent/connection.rb | 56 +++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/lib/vines/agent/connection.rb b/lib/vines/agent/connection.rb index 0bf1bb0..0add9de 100644 --- a/lib/vines/agent/connection.rb +++ b/lib/vines/agent/connection.rb @@ -17,7 +17,7 @@ def initialize(options) *options.values_at(:domain, :password, :host, :port, :download, :conf) certs = File.expand_path('certs', conf) - @permissions, @services, @sessions, @component = {}, {}, {}, nil + @permissions, @services, @sessions, @components, @component = {}, {}, {}, [], nil @ready = false @mtx = Mutex.new @@ -88,21 +88,7 @@ def start # After the bot connects to the chat server, discover the component, send # our ohai system description data, and initialize permissions. def startup - cb = lambda do |component, iter| - if component - # Pray for we have no unknown components there - log.info("Found vines component at #{component}!") - # FIXME Deal with DiscoInfo to ensure this component is our - @component = component.jid - send_system_info - request_permissions - else - log.info("Vines component not found, rediscovering . . .") - EM::Timer.new(10) { discover_component(&cb) } - end - iter.next - end - discover_component(&cb) + discover_component end def version(node) @@ -181,16 +167,48 @@ def fqdn # server for its list of components, then ask each component for it's info. # The component broadcasting the http://getvines.com/protocol feature is our # Vines service. - def discover_component(&cb) + def discover_component + @components = [] disco = Blather::Stanza::DiscoItems.new disco.to = @stream.jid.domain @stream.write_with_handler(disco) do |result| unless result.error? - EM::Iterator.new(result.items).each &cb + info = Blather::Stanza::DiscoInfo.new + # iterate through disco result and collect ’em all + EM::Iterator.new(result.items).map proc{ |comp, it_disco| + info.to = comp.jid.domain + @stream.write_with_handler(info) do |reply| + unless reply.error? + # iterate through info results and collect ’em all + EM::Iterator.new(reply.features).map proc{ |f, it_info| + it_info.return f.var == NS ? comp : nil; + }, proc{ |comps| + # we have collected all the info replies for the + # disco given, let’s proceed with the next + it_disco.return comps - [nil] + } + end + end + }, proc{ |compss| + # Well, we yielded all the discos, let's request perms etc + @components = compss.flatten.uniq + + if !@components || @components.length < 1 + log.info("Vines component not found, rediscovering…") + EM::Timer.new(30) { discover_component } + end + @component = @components[0].jid + log.info("Vines component(s) found #{@components}") + if @components.length > 1 + log.warn("Using one #{@component} out of #{@components.length} found") + end + send_system_info + request_permissions + } end end end - + # Download the list of unix user accounts and the JID's that are allowed # to use them. This is used to determine if a change user command like # +v user root+ is allowed. From 27e4619350b638e4dd8634a390a22a179b3e6894 Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Sun, 25 Nov 2012 17:31:07 +0400 Subject: [PATCH 11/13] discover should not perform any actions if no comp was found. --- lib/vines/agent/connection.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/vines/agent/connection.rb b/lib/vines/agent/connection.rb index 0add9de..eaddedd 100644 --- a/lib/vines/agent/connection.rb +++ b/lib/vines/agent/connection.rb @@ -196,14 +196,15 @@ def discover_component if !@components || @components.length < 1 log.info("Vines component not found, rediscovering…") EM::Timer.new(30) { discover_component } + else + @component = @components[0].jid + log.info("Vines component(s) found #{@components}") + if @components.length > 1 + log.warn("Using one #{@component} out of #{@components.length} found") + end + send_system_info + request_permissions end - @component = @components[0].jid - log.info("Vines component(s) found #{@components}") - if @components.length > 1 - log.warn("Using one #{@component} out of #{@components.length} found") - end - send_system_info - request_permissions } end end From d55fecd4927673806fa3cfc78c7141a55fbbd0f4 Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Mon, 26 Nov 2012 20:40:14 +0400 Subject: [PATCH 12/13] Asynchronous output from executed shell commands, exitstatus (printed if !=0) --- lib/vines/agent/connection.rb | 43 ++++++++++++++++++++++++++++------- lib/vines/agent/shell.rb | 22 +++++++++++++----- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/lib/vines/agent/connection.rb b/lib/vines/agent/connection.rb index eaddedd..1f2d6df 100644 --- a/lib/vines/agent/connection.rb +++ b/lib/vines/agent/connection.rb @@ -19,6 +19,7 @@ def initialize(options) certs = File.expand_path('certs', conf) @permissions, @services, @sessions, @components, @component = {}, {}, {}, [], nil @ready = false + @task_responses = {} @mtx = Mutex.new jid = Blather::JID.new(fqdn, domain, 'vines') @@ -39,15 +40,15 @@ def initialize(options) @stream.register_handler(:ready) do # [AM] making sure we are to init once - # unless @ready is not enough for an obvious reason - @mtx.synchronize { + # unless @ready is not enough for an obvious reason +# @mtx.synchronize { unless @ready log.info("Connected #{@stream.jid} agent to #{host}:#{port}") log.warn("Agent must run as root user to allow user switching") unless root? @ready = true startup end - } +# } end @stream.register_handler(:subscription, :request?) do |node| @@ -181,7 +182,7 @@ def discover_component unless reply.error? # iterate through info results and collect ’em all EM::Iterator.new(reply.features).map proc{ |f, it_info| - it_info.return f.var == NS ? comp : nil; + it_info.return f.var == NS ? comp : nil }, proc{ |comps| # we have collected all the info replies for the # disco given, let’s proceed with the next @@ -198,7 +199,7 @@ def discover_component EM::Timer.new(30) { discover_component } else @component = @components[0].jid - log.info("Vines component(s) found #{@components}") + log.info("Vines component found #{@component}") if @components.length > 1 log.warn("Using one #{@component} out of #{@components.length} found") end @@ -248,8 +249,34 @@ def process_message(message) return unless valid_user?(bare) session = @sessions[full] ||= Shell.new(bare, @permissions) - session.run(message.body.strip) do |output| - @stream.write(reply(message, output, forward_to)) + + # [AM] Create a TickLoop to collect shell output and send + # back to recipient by portions. This is needed to + # implement non-blocking, but not annoying on the other hand + # service. E. g. “ls” im most cases will return immediately, + # while “ping google.com” will send back stanzas every second + + session.on_output = lambda {|output| @task_responses[message.id] += output } + session.on_error = lambda {|error| @task_responses[message.id] += "⇒ #{error}" } + + @task_responses[message.id] = "" + task_response_tickloop = EM.tick_loop do + unless @task_responses[message.id].empty? + @stream.write(reply(message, @task_responses[message.id], forward_to)) + @task_responses[message.id] = "" + end + sleep 1 + end + session.run(message.body.strip) do |output, exitstatus| + task_response_tickloop.stop + task_response_tickloop = nil + unless @task_responses[message.id].empty? + @stream.write(reply(message, @task_responses[message.id], forward_to)) + end + @task_responses.delete message.id + if exitstatus && exitstatus != 0 + @stream.write(reply(message, "#{exitstatus} ↵ #{message.body}", forward_to)) + end end end @@ -260,7 +287,7 @@ def reply(message, body, forward_to) Blather::Stanza::Message.new(message.from, body).tap do |node| node << node.document.create_element('jid', forward_to, xmlns: NS) if forward_to node.thread = message.thread if message.thread - node.xhtml = '' + node.xhtml = '' span = node.xhtml_node.elements.first body.each_line do |line| span.add_child(Nokogiri::XML::Text.new(line.chomp, span.document)) diff --git a/lib/vines/agent/shell.rb b/lib/vines/agent/shell.rb index 5fbec63..268771d 100644 --- a/lib/vines/agent/shell.rb +++ b/lib/vines/agent/shell.rb @@ -10,20 +10,25 @@ module Agent class Shell include Vines::Log - attr_writer :permissions - + attr_writer :permissions, :on_output, :on_error + # Create a new shell session to asynchronously execute commands for this # JID. The JID is validated in the permissions Hash before executing # commands. def initialize(jid, permissions) @jid, @permissions = jid, permissions @user = allowed_users.first if allowed_users.size == 1 + @on_output = nil + @on_error = nil @commands = EM::Queue.new process_command_queue end # Queue the shell command to run as soon as the currently executing tasks # complete. Yields the shell output to the callback block. + # [AM] “v reset” command is supposed to be executed immediately + # to give an ability of interrupting + # (in general, interferring) the current shell task queue def run(command, &callback) if reset?(command) callback.call(run_built_in(command)) @@ -46,8 +51,8 @@ def process_command_queue run_in_slave(command[:command]) end end - cb = proc do |output| - command[:callback].call(output) + cb = proc do |output, exitstatus| + command[:callback].call(output, exitstatus) process_command_queue end EM.defer(op, cb) @@ -58,13 +63,18 @@ def run_in_slave(command) return "-> no user selected, run 'v user'" unless @user log.info("Running #{command} as #{@user}") - spawn(@user) unless @shell + unless @shell + spawn(@user) + end + @shell.outproc = @on_output if @on_output + @shell.errproc = @on_error if @on_error + out, err = @shell.execute(command) output = [].tap do |arr| arr << out if out && !out.empty? arr << err if err && !err.empty? end.join("\n") - output.empty? ? '-> command completed' : output + return [output.empty? ? '-> command completed' : output, @shell.exitstatus] rescue close '-> restarted shell' From a3e1683a2a14b95e10fc5eb55e8d691e24aee7aa Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Tue, 27 Nov 2012 11:18:29 +0400 Subject: [PATCH 13/13] Mutex for initialization was erroneously commented out. --- lib/vines/agent/connection.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/vines/agent/connection.rb b/lib/vines/agent/connection.rb index 1f2d6df..84a01eb 100644 --- a/lib/vines/agent/connection.rb +++ b/lib/vines/agent/connection.rb @@ -41,14 +41,14 @@ def initialize(options) @stream.register_handler(:ready) do # [AM] making sure we are to init once # unless @ready is not enough for an obvious reason -# @mtx.synchronize { + @mtx.synchronize { unless @ready log.info("Connected #{@stream.jid} agent to #{host}:#{port}") log.warn("Agent must run as root user to allow user switching") unless root? @ready = true startup end -# } + } end @stream.register_handler(:subscription, :request?) do |node|