commit a089227ca94121115f46e5998b497fa102787398
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Sun Mar 6 19:25:10 2011 +0100

    [fix/evol] work on better Interface/Task/Conversation/… sync and bot stop action in order to avoid races and locks

diff --git a/bin/librarian b/bin/librarian
index f04ba51..bf37ffe 100755
--- a/bin/librarian
+++ b/bin/librarian
@@ -70,10 +70,10 @@ end
 bot = reg.bot
 
 trap('INT') do
-  bot.ask_to_stop
+  bot.stop(:quickly)
 end
 trap('TERM') do
-  bot.ask_to_stop
+  bot.stop(:quickly)
 end
 
 bot.run
diff --git a/bin/mapmaker b/bin/mapmaker
index a5bfe44..88b2706 100755
--- a/bin/mapmaker
+++ b/bin/mapmaker
@@ -69,10 +69,10 @@ end
 bot = reg.bot
 
 trap('INT') do
-  bot.ask_to_stop
+  bot.stop(:quickly)
 end
 trap('TERM') do
-  bot.ask_to_stop
+  bot.stop(:quickly)
 end
 
 bot.run
diff --git a/bin/postman b/bin/postman
index d3b9157..1704880 100755
--- a/bin/postman
+++ b/bin/postman
@@ -60,13 +60,16 @@ module CyborgHood
         end
       end
 
-      def ask_to_stop
+      def stop(condition)
         super do
           self.services.imap.stop_mail_check
         end
       end
 
+      protected
+
       def ready_to_stop?
+        # TODO: also wait for messages being processed to be finished (or try to interrupt cleanly if possible in :quickly condition)
         super and not self.services.imap.connected?
       end
 
@@ -207,10 +210,10 @@ end
 bot = reg.bot
 
 trap('INT') do
-  bot.ask_to_stop
+  bot.stop(:quickly)
 end
 trap('TERM') do
-  bot.ask_to_stop
+  bot.stop(:quickly)
 end
 
 bot.run
diff --git a/bin/test_client b/bin/test_client
index 975e3d3..5aa719f 100755
--- a/bin/test_client
+++ b/bin/test_client
@@ -37,14 +37,14 @@ module CyborgHood
 
       def start_work
         task "compare stuff" do
-          ask "MapMaker", :info1, "/_cyborg_"
-          ask "Librarian", :info2, "/_cyborg_"
-          ask "Librarian", :library, "/Records"
-          ask "Librarian", :persons, "/Records/Persons"
+          #ask "MapMaker", :info1, "/_cyborg_"
+          #ask "Librarian", :info2, "/_cyborg_"
+          #ask "Librarian", :library, "/Records"
+          #ask "Librarian", :persons, "/Records/Persons"
           #ask "Librarian", :dns_domains, "/Records/DnsDomains"
-          ask "Librarian", :gorou, "/Records/Persons/gorou"
-          ask "Librarian", :guihome_net, "/Records/DnsDomains/guihome.net"
-          ask "Librarian", :person_search, "/Records/Persons/?", {:uid => "g*"}
+          #ask "Librarian", :gorou, "/Records/Persons/gorou"
+          #ask "Librarian", :guihome_net, "/Records/DnsDomains/guihome.net"
+          #ask "Librarian", :person_search, "/Records/Persons/?", {:uid => "g*"}
           #ask "Librarian", :person_search_null, "/Records/Persons/?"
           #ask "MapMaker", :zones, "/Zones"
           #ask "MapMaker", :wanted_failure, "/prout"
@@ -53,29 +53,31 @@ module CyborgHood
           #ask "MapMaker", :zone_mp, "/Zones/milkypond.org"
           #ask "MapMaker", :dns_check, "/Services/DNS/check_config"
           #ask "MapMaker", :search, "/Zones/?"
-          ask "MapMaker", :search_master, "/Zones/?", {:is_master => true}
+          #ask "MapMaker", :search_master, "/Zones/?", {:is_master => true}
           #ask "MapMaker", :search_slave, "/Zones/?", {:is_master => false}
           #know? "MapMaker", :k1, "/Zones"
           #know? "MapMaker", :k2, "/prout"
+          set_user "duck"
+          ask "Clerk", :dns_info, "/Commands/DNS/INFO"
           on_error do
             puts "PLOUF"
             pp errors
             pp results
-            stop_bot :at_once
+            stop_bot :quickly
           end
           on_success do
             puts "OK"
             pp errors
             pp results
-            puts "Tadam: " + (results[:info1][:api_version] == results[:info2][:api_version] ? "same" : "different")
-            set_preferred_locales "fr;q=1,en-us,en"
+            #puts "Tadam: " + (results[:info1][:api_version] == results[:info2][:api_version] ? "same" : "different")
+            #set_preferred_locales "fr;q=1,en-us,en"
             meet "waiter", :zzz
             STDOUT.flush
             on_success do
               puts "OK compare stuff"
             end
             on_error do
-              stop_bot :at_once
+              stop_bot :quickly
             end
           end
         end
@@ -95,10 +97,10 @@ end
 bot = CyborgHood::TestClientHome::TestClientClient.new
 
 trap('INT') do
-  bot.ask_to_stop
+  bot.stop(:quickly)
 end
 trap('TERM') do
-  bot.ask_to_stop
+  bot.stop(:quickly)
 end
 
 bot.run
diff --git a/lib/cyborghood-postman/imap.rb b/lib/cyborghood-postman/imap.rb
index 988b02c..ff19428 100644
--- a/lib/cyborghood-postman/imap.rb
+++ b/lib/cyborghood-postman/imap.rb
@@ -17,7 +17,6 @@
 #++
 
 require 'net/imap'
-require 'thread'
 
 # IMAP IDLE support
 class Net::IMAP
diff --git a/lib/cyborghood/cyborg.rb b/lib/cyborghood/cyborg.rb
index c1d6a43..e60eaa5 100644
--- a/lib/cyborghood/cyborg.rb
+++ b/lib/cyborghood/cyborg.rb
@@ -18,6 +18,7 @@
 
 require 'cyborghood'
 require 'eventmachine'
+require 'thread'
 require 'cyborghood/cyborg/dsl'
 
 
@@ -104,40 +105,55 @@ module CyborgHood
       # use "aspects" Modules to define behaviors
     end
 
-    def ask_to_stop
+    def stop(condition)
       logger.info "Bot was asked to stop..."
+
       yield if block_given?
-      try_stop
+
+      case condition
+      when :when_finished
+        wait_until_ready_to_stop
+      when :quickly
+        # try to stop gracefully
+        DSL::Task.stop_all
+        wait_until_ready_to_stop
+      when :at_once
+        # it won't wait for anything to finish…
+        exit
+      end
     end
 
-    def ready_to_stop?
-      DSL::Task.idle?
+    # core capabilities
+    def capabilities
+      []
+    end
+
+    protected
+
+    def stop_gracefully
     end
 
-    def stop
+    def try_stop
       logger.info "Bot stopping"
       EventMachine.next_tick { EventMachine.stop_event_loop }
     end
 
-    def try_stop
+    def ready_to_stop?
+      DSL::Task.idle?
+    end
+
+    def wait_until_ready_to_stop
       if ready_to_stop?
         @system_notification.unsubscribe(@system_notification_processing)
         @system_notification_processing = nil
         drop_channel(@system_notification_name)
 
-        stop
+        try_stop
       else
-        EventMachine.next_tick { try_stop }
+        EventMachine.next_tick { wait_until_ready_to_stop }
       end
     end
 
-    # core capabilities
-    def capabilities
-      []
-    end
-
-    protected
-
     def process_system_notification(msg)
     end
   end
diff --git a/lib/cyborghood/cyborg/botnet.rb b/lib/cyborghood/cyborg/botnet.rb
index fad28c7..98f8aeb 100644
--- a/lib/cyborghood/cyborg/botnet.rb
+++ b/lib/cyborghood/cyborg/botnet.rb
@@ -74,8 +74,10 @@ module CyborgHood
 
     def contact_peer(peer, block, dont_open_new_connection = false)
       if @comm_list.has_key? peer
-        logger.debug "Botnet: Reusing connection for peer #{peer}"
-        return block.call @comm_list[peer]
+        if @comm_list[peer].usuable?
+          logger.debug "Botnet: Reusing connection for peer #{peer}"
+          return block.call @comm_list[peer]
+        end
       end
 
       return if dont_open_new_connection
@@ -109,17 +111,20 @@ module CyborgHood
 
     # used to quit properly and later to reuse communication channels
     def register_communication(peer, conversation)
+      logger.debug "Botnet: conversation with '#{peer}' ready"
       @comm_list[peer] = conversation
     end
 
     def unregister_communication(peer)
+      logger.debug "Botnet: conversation with '#{peer}' closed"
       @comm_list.delete(peer)
       @pending_conversation_close.delete(peer)
     end
 
-    def ask_to_stop
-      @comm_list.values.each {|conv| conv.bye }
-      super
+    def stop(condition)
+      super do
+        @comm_list.each_value{|conv| conv.stop(condition) }
+      end
     end
 
     protected
diff --git a/lib/cyborghood/cyborg/botnet/conversation.rb b/lib/cyborghood/cyborg/botnet/conversation.rb
index 8cc9451..aca3874 100644
--- a/lib/cyborghood/cyborg/botnet/conversation.rb
+++ b/lib/cyborghood/cyborg/botnet/conversation.rb
@@ -127,10 +127,10 @@ module CyborgHood
       @sent
     end
 
-    def send
+    def send(go_quiet = false)
       raise CyberError.new(:unrecoverable, "bot/conversation", "Not sending twice the same message") if self.sent?
       @action_id = @conv_thread.next_action_id if @action_id.nil?
-      @conv_thread.conversation.send_message(self)
+      @conv_thread.conversation.send_message(self, go_quiet)
       @sent = true
 
       # return message (convenience)
@@ -180,6 +180,7 @@ module CyborgHood
       @split_data_message = nil
       @split_data = []
       @comm_stop = false
+      @comm_dead = false
 
       # associated conversation threads
       @auto_close_threads = 60   # max idle time before closing (nil => never close threads)
@@ -192,6 +193,8 @@ module CyborgHood
       @next_thread_id = 0
       @system_thread = self.thread('system')
 
+      @stop_when_finished = false
+
       # post-negociation peer info
       @peer_name = nil
       @peer_capabilities = []
@@ -275,16 +278,38 @@ module CyborgHood
 
     def unbind
       logger.info "Conversation finished with #{identifier} (#{@peer_name})"
+
+      # ensure we go quiet
+      @message_send.synchronize do
+        @comm_stop = true
+      end
+      @comm_dead = true
+
       @bot.unregister_communication @peer_name unless @peer_name.nil?
       @bot.drop_channel(@system_notification_name)
+
       @conv_threads.each_value {|s| s.close(false) }
       @conv_threads = {}
       @conv_threads_index = {}
+
       @comm_logic_block.call false unless @comm_logic_block.nil? or @protocol.negociation_ok?
     end
 
+    def usuable?
+      not (@comm_dead or @comm_stop)
+    end
+
     def bye
-      @protocol.send_quit_leaving
+      stop :quickly
+    end
+
+    def stop(condition)
+      case condition
+      when :when_finished
+        @stop_when_finished = true
+      when :quickly
+        @protocol.send_quit_leaving
+      end
     end
 
     def set_peer_info(name, capabilities)
@@ -301,9 +326,19 @@ module CyborgHood
     end
 
     def set_comm_stop(peer_left = false)
-      @comm_stop = true
+      return if @comm_stop
+
+      logger.debug "Conversation with '#{@peer_name}' on #{identifier} is closing"
       @system_notification.unsubscribe(@system_notification_processing)
+
+      # the block may use a <message>.send(true) to go quiet (set @comm_stop)
       yield if block_given?
+
+      # ensure we go quiet
+      @message_send.synchronize do
+        @comm_stop = true
+      end
+
       peer_left ? close_connection : close_connection_after_writing
     end
 
@@ -357,7 +392,7 @@ module CyborgHood
       @conv_threads_closing.delete(th.name)
     end
 
-    def send_message(message)
+    def send_message(message, quiet_after = false)
       raise CyberError.new(:unrecoverable, "bot/conversation", "Cannot send message without action id") if message.action_id.nil?
 
       reset_idle_thread_check(message.conv_thread)
@@ -366,10 +401,14 @@ module CyborgHood
       flags += "+" unless message.action_parameters.nil?
 
       @message_send.synchronize do
-        send_line sprintf("%s-%04d-%04d%s %s", @bot.name, message.conv_thread.id, message.action_id, flags, message.action_code)
-        unless message.action_parameters.nil?
-          message.action_parameters.to_yaml.each_line {|l| send_line l }
-          send_line EOD
+        unless @comm_stop
+          send_line sprintf("%s-%04d-%04d%s %s", @bot.name, message.conv_thread.id, message.action_id, flags, message.action_code)
+          unless message.action_parameters.nil?
+            message.action_parameters.to_yaml.each_line {|l| send_line l }
+            send_line EOD
+          end
+
+          @comm_stop = true if quiet_after
         end
       end
 
@@ -389,6 +428,7 @@ module CyborgHood
 
     def send_line(msg)
       return if error?
+      return if @comm_stop
 
       logger.debug "Sending data [#{identifier}]: #{msg}"
       send_data "#{msg}\n"
@@ -509,17 +549,25 @@ module CyborgHood
       @conv_threads_timers_manage.synchronize do
         # if a timer is running, do nothing
         return unless @conv_threads_timers[conv_thread.id].nil?
+      end
 
-        # test for idleness
-        if conv_thread.idle?
-          logger.debug "Thread '#{conv_thread.name}@#{@peer_name}' is currently idle, doing another check in #{@auto_close_threads}s"
+      # test for idleness
+      if conv_thread.idle?
+        if @stop_when_finished
+          logger.debug "Thread '#{conv_thread.name}@#{@peer_name}' is currently idle, considered finished, closing"
+          close_thread(conv_thread.name)
+          return
+        end
+
+        logger.debug "Thread '#{conv_thread.name}@#{@peer_name}' is currently idle, doing another check in #{@auto_close_threads}s"
 
-          # send notification
-          @bot.get_channel(@system_notification_name) << {
-            :topic => 'THREAD IDLE',
-            :thread => conv_thread.name
-          }
+        # send notification
+        @bot.get_channel(@system_notification_name) << {
+          :topic => 'THREAD IDLE',
+          :thread => conv_thread.name
+        }
 
+        @conv_threads_timers_manage.synchronize do
           # set timer for closing
           @conv_threads_timers[conv_thread.id] = EventMachine.add_timer(@auto_close_threads) do
             @conv_threads_timers[conv_thread.id] = nil
diff --git a/lib/cyborghood/cyborg/botnet/dsl.rb b/lib/cyborghood/cyborg/botnet/dsl.rb
index cbd9b46..7655005 100644
--- a/lib/cyborghood/cyborg/botnet/dsl.rb
+++ b/lib/cyborghood/cyborg/botnet/dsl.rb
@@ -21,7 +21,9 @@ module CyborgHood
   module DSL
     module BotnetTask
       def ask(peer, key, cmd, *args)
-        _add_subtask_using_peer(peer) do |subtask, conv_thread|
+        action_name = ['ask', cmd, *args].hash
+
+        _add_subtask_using_peer(action_name, peer) do |subtask, conv_thread|
           conv_thread.call(cmd, *args) do |reply|
             case reply[:status]
             when :ok
@@ -38,7 +40,9 @@ module CyborgHood
       end
 
       def know?(peer, key, cmd)
-        _add_subtask_using_peer(peer) do |subtask, conv_thread|
+        action_name = ['know', cmd].hash
+
+        _add_subtask_using_peer(action_name, peer) do |subtask, conv_thread|
           conv_thread.exists?(cmd) do |reply|
             case reply[:status]
             when :ok
@@ -54,12 +58,30 @@ module CyborgHood
         end
       end
 
-      def _add_subtask_using_peer(peer)
-        _add_subtask("botnet/peer/#{peer}/out") do |subtask|
+      def _add_subtask_using_peer(action_name, peer)
+        subtask_name = "botnet/peer/#{peer}/#{action_name}/out"
+
+        _add_subtask(subtask_name) do |subtask|
           logger.debug "Task '#{@name}': Trying to contact peer '#{peer}'"
+
+          # callback to end peer action and subtask
+          # (used when shooting the task)
+          defuse_peer_action_cb = Proc.new do
+            logger.debug "Task '#{@name}': defusing subtask '#{subtask.name}'"
+
+            subtask.finish unless subtask.finished?
+
+            # we already own the mutex here
+            registered_resources.delete(subtask_name)
+          end
+          # register peer action in the task
+          tasks_info_mutex.synchronize do
+            registered_resources[subtask_name] = defuse_peer_action_cb
+          end
+
           @bot.contact_peer(peer) do |conv|
             if conv
-              logger.debug "Task '#{@name}': Peer '#{peer}' contacted, starting conversation"
+              logger.debug "Task '#{@name}': subtask '#{subtask.name}': peer '#{peer}' contacted, starting conversation"
 
               @peer_contacted << peer
 
@@ -85,10 +107,12 @@ module CyborgHood
       def _finished
         super
 
+        # close opened thread
         cb = Proc.new do |conv|
           conv.close_thread(@notification_name)
         end
 
+        # loop on all contacted peers to close the thread
         @peer_contacted.each do |peer|
           @bot.contact_peer(peer, true, &cb)
         end
diff --git a/lib/cyborghood/cyborg/botnet/protocol.rb b/lib/cyborghood/cyborg/botnet/protocol.rb
index 78a4012..80984b4 100644
--- a/lib/cyborghood/cyborg/botnet/protocol.rb
+++ b/lib/cyborghood/cyborg/botnet/protocol.rb
@@ -292,13 +292,13 @@ module CyborgHood
 
     def send_quit_decline(reason)
       @conversation.set_comm_stop do
-        @conversation.thread('system').new_message("QUIT DECLINE", { :reason => reason }).send
+        @conversation.thread('system').new_message("QUIT DECLINE", { :reason => reason }).send(true)
       end
     end
 
     def send_quit_leaving
       @conversation.set_comm_stop do
-        @conversation.thread('system').new_message("QUIT LEAVING").send
+        @conversation.thread('system').new_message("QUIT LEAVING").send(true)
       end
     end
   end
diff --git a/lib/cyborghood/cyborg/dsl.rb b/lib/cyborghood/cyborg/dsl.rb
index 9fceedc..05988bf 100644
--- a/lib/cyborghood/cyborg/dsl.rb
+++ b/lib/cyborghood/cyborg/dsl.rb
@@ -30,10 +30,32 @@ module CyborgHood
     class TaskBase < BaseDSL
       attr_reader :bot, :name, :errors, :results, :preferred_locales, :locale, :user
 
+      # mutex used for all class variables in this paragraph
+      @@tasks_info_mutex = Mutex.new
       @@task_wip = 0
+      @@registered_resources = {}
+
+      @@stop_all_tasks = false
 
       def self.idle?
-        @@task_wip == 0
+        @@tasks_info_mutex.synchronize do
+          @@task_wip == 0
+        end
+      end
+
+      def self.stop_all
+        @@stop_all_tasks = true
+
+        # call defuse callback out of the sync block
+        # (which may need to synchronize later to the
+        #  same mutex in usual conditions)
+        resources_to_free = true
+        while not resources_to_free.nil?
+          @@tasks_info_mutex.synchronize do
+            resources_to_free = @@registered_resources.shift
+          end
+          resources_to_free[1].call unless resources_to_free.nil?
+        end
       end
 
       # the name MUST be unique
@@ -41,7 +63,9 @@ module CyborgHood
         @bot = bot
         @name = name
 
-        @@task_wip += 1
+        @@tasks_info_mutex.synchronize do
+          @@task_wip += 1
+        end
 
         @errors = {}.freeze
         @results = {}.freeze
@@ -110,18 +134,62 @@ module CyborgHood
       def wait_notification(name, criterias = {}, timeout = nil,  &cb)
         name = @notification_name if name == :task
 
-        _add_subtask("notification/#{name}/in") do |subtask|
-          logger.debug "Task '#{@name}': Waiting notification on '#{name}'"
+        subtask_name = "notification/#{name}/#{criterias.hash}/in"
+        _add_subtask(subtask_name) do |subtask|
+          logger.debug "Task '#{@name}': subtask '#{subtask.name}': waiting notification on '#{name}'"
+          subcription_id = nil
+          subcription_id_mutex = Mutex.new
+
+          # callback to end notification and subtask
+          # (used when shooting the task)
+          defuse_notification_cb = Proc.new do
+            logger.debug "Task '#{@name}': defusing subtask '#{subtask.name}'"
+
+            subcription_id_mutex.synchronize do
+              @bot.get_channel(name).unsubscribe(subcription_id) unless subcription_id.nil?
+              subcription_id = nil
+            end
+
+            subtask.finish unless subtask.finished?
+
+            # we already own the mutex here
+            @@registered_resources.delete(subtask_name)
+          end
+          # register notification in the task
+          @@tasks_info_mutex.synchronize do
+            @@registered_resources[subtask_name] = defuse_notification_cb
+          end
+
           subcription_id = @bot.get_channel(name).subscribe do |msg|
             if _notification_criterias_match(msg, criterias)
               cb.call(subtask, msg)
-              @bot.get_channel(name).unsubscribe(subcription_id) if subtask.finished?
+
+              if subtask.finished?
+                subcription_id_mutex.synchronize do
+                  @bot.get_channel(name).unsubscribe(subcription_id) unless subcription_id.nil?
+                  subcription_id = nil
+                end
+
+                # unregister the notification
+                @@tasks_info_mutex.synchronize do
+                  @@registered_resources.delete(subtask_name)
+                end
+              end
             end
           end
 
           if timeout
             EventMachine.add_timer(timeout) do
-              @bot.get_channel(name).unsubscribe(subcription_id)
+              subcription_id_mutex.synchronize do
+                @bot.get_channel(name).unsubscribe(subcription_id) unless subcription_id.nil?
+                subcription_id = nil
+              end
+
+              # unregister the notification
+              @@tasks_info_mutex.synchronize do
+                @@registered_resources.delete(subtask_name)
+              end
+
               subtask.finish
             end
           end
@@ -129,23 +197,56 @@ module CyborgHood
       end
 
       def wait_timer(timeout, repeat = false, &cb)
-        _add_subtask("timer/#{timeout}/#{cb.hash}") do |subtask|
-          periodic_timer_signature = nil
+        subtask_name = "timer/#{timeout}/#{cb.hash}"
+        _add_subtask(subtask_name) do |subtask|
+          timer_signature = nil
+          timer_signature_mutex = Mutex.new
+
+          # callback to end timer and subtask
+          # (used when shooting the task)
+          defuse_timer_cb = Proc.new do
+            logger.debug "Task '#{@name}': defusing subtask '#{subtask.name}'"
+
+            timer_signature_mutex.synchronize do
+              EventMachine.cancel_timer(timer_signature) unless timer_signature.nil?
+              timer_signature = nil
+            end
+
+            subtask.finish unless subtask.finished?
+
+            # we already own the mutex here
+            @@registered_resources.delete(subtask_name)
+          end
+          # register timer in the task
+          @@tasks_info_mutex.synchronize do
+            @@registered_resources[subtask_name] = defuse_timer_cb
+          end
 
           timer_cb = Proc.new do
             if repeat
               cb.call(subtask)
-              EventMachine.cancel_timer(periodic_timer_signature) if subtask.finished?
+              timer_signature_mutex.synchronize do
+                if subtask.finished? and not timer_signature.nil?
+                  EventMachine.cancel_timer(timer_signature)
+                  timer_signature = nil
+                end
+              end
             else
               cb.call(subtask)
-              subtask.finish
+
+              # unregister the timer
+              @@tasks_info_mutex.synchronize do
+                @@registered_resources.delete(subtask_name)
+              end
+
+              subtask.finish unless subtask.finished?
             end
           end
 
           if repeat
-            periodic_timer_signature = EventMachine.add_periodic_timer(timeout, timer_cb)
+            timer_signature = EventMachine.add_periodic_timer(timeout, timer_cb)
           else
-            EventMachine.add_timer(timeout, timer_cb)
+            timer_signature = EventMachine.add_timer(timeout, timer_cb)
           end
         end
       end
@@ -177,7 +278,8 @@ module CyborgHood
           # in this order: send a last message before leaving
           # (so we are sure the peer received a reply to its message)
           send_notification notification_name, {:topic => "MEET", :from => @name}
-          subtask.finish if meet_ok
+
+          subtask.finish if meet_ok or @@stop_all_tasks
         end
       end
 
@@ -197,14 +299,7 @@ module CyborgHood
 
       def stop_bot(condition)
         _add_subtask('stop') do |subtask|
-          case condition
-          when :when_finished
-            # try to stop gracefully
-            @bot.try_stop
-          when :at_once
-            # it won't wait for anything to finish…
-            @bot.stop
-          end
+          @bot.stop(condition)
 
           subtask.finish
         end
@@ -238,7 +333,7 @@ module CyborgHood
 
         def finish
           if @finished
-            raise CyberError.new(:unrecoverable, "botnet/client/dsl", "Task '#{@task.name}': subtask should have ended, but it lied")
+            raise CyberError.new(:unrecoverable, "botnet/client/dsl", "Task '#{@task.name}': subtask '#{@name}' should have ended, but it lied")
           end
           @finished = true
           logger.debug "Task '#{@task.name}': subtask '#{@name}' finished"
@@ -276,6 +371,13 @@ module CyborgHood
 
         logger.debug "Task '#{@name}': step finished"
 
+        if @@stop_all_tasks
+          _finished
+
+          # running no more steps will end the task
+          return
+        end
+
         # compute step result
         @errors = {}
         @results = {}
@@ -343,8 +445,22 @@ module CyborgHood
       end
 
       def _finished
-        @@task_wip -= 1
-        logger.debug "Task '#{@name}': finished (#{@@task_wip} remaining)"
+        @@tasks_info_mutex.synchronize do
+          @@task_wip -= 1
+          logger.debug "Task '#{@name}': finished (#{@@task_wip} tasks remaining)"
+        end
+      end
+
+      # needed for mixin
+      # TODO: find a cleaner solution
+      def tasks_info_mutex
+        @@tasks_info_mutex
+      end
+
+      # needed for mixin
+      # TODO: find a cleaner solution
+      def registered_resources
+        @@registered_resources
       end
     end
 
