entangler 0.1.1 → 0.1.2
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.
- checksums.yaml +4 -4
- data/lib/entangler/entangled_file.rb +17 -16
- data/lib/entangler/executor/background/base.rb +108 -0
- data/lib/entangler/executor/background/master.rb +24 -0
- data/lib/entangler/executor/base.rb +15 -202
- data/lib/entangler/executor/master.rb +12 -19
- data/lib/entangler/executor/processing/base.rb +122 -0
- data/lib/entangler/version.rb +1 -1
- data/lib/entangler.rb +2 -2
- metadata +5 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: e910a8be4617a7df89c5a0d5f94b51617a26deb6
         | 
| 4 | 
            +
              data.tar.gz: 1d083cf56c15e7f332c703eb36381d31e717b7f6
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 829fd8b9cc788a049e1daab942a3eff9d911351f580b71f276610c0c48b758638698868066ceb39e3bac63a6bebc7e295915d19fd95d7f93e4a66a0bfe057b65
         | 
| 7 | 
            +
              data.tar.gz: b08f880f5e249658f83b906e3789adf84b999d9922b068dc6eec037aa069475a62434212287f3a3898f56661a1a01c43911eab4c9b70fd6c59a12f153edd92ef
         | 
| @@ -28,6 +28,23 @@ module Entangler | |
| 28 28 | 
             
                  File.exists?(full_path)
         | 
| 29 29 | 
             
                end
         | 
| 30 30 |  | 
| 31 | 
            +
                def export
         | 
| 32 | 
            +
                  raise "Delta file doesn't exist when creaing patched file" unless delta_exists?
         | 
| 33 | 
            +
                  tempfile = Tempfile.new('final_file')
         | 
| 34 | 
            +
                  if File.exists?(full_path)
         | 
| 35 | 
            +
                    LibRubyDiff.patch(full_path, delta_file.path, tempfile.path)
         | 
| 36 | 
            +
                  else
         | 
| 37 | 
            +
                    temp_empty_file = Tempfile.new('empty_file')
         | 
| 38 | 
            +
                    LibRubyDiff.patch(temp_empty_file.path, delta_file.path, tempfile.path)
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                  tempfile.rewind
         | 
| 41 | 
            +
                  File.open(full_path, 'w'){|f| f.write(tempfile.read)}
         | 
| 42 | 
            +
                  tempfile.close
         | 
| 43 | 
            +
                  tempfile.unlink
         | 
| 44 | 
            +
                  File.utime(File.atime(full_path), @desired_modtime, full_path)
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                private
         | 
| 31 48 | 
             
                def signature_exists?
         | 
| 32 49 | 
             
                  defined?(@signature_tempfile)
         | 
| 33 50 | 
             
                end
         | 
| @@ -79,22 +96,6 @@ module Entangler | |
| 79 96 | 
             
                  delta_file.read
         | 
| 80 97 | 
             
                end
         | 
| 81 98 |  | 
| 82 | 
            -
                def export
         | 
| 83 | 
            -
                  raise "Delta file doesn't exist when creaing patched file" unless delta_exists?
         | 
| 84 | 
            -
                  tempfile = Tempfile.new('final_file')
         | 
| 85 | 
            -
                  if File.exists?(full_path)
         | 
| 86 | 
            -
                    LibRubyDiff.patch(full_path, delta_file.path, tempfile.path)
         | 
| 87 | 
            -
                  else
         | 
| 88 | 
            -
                    temp_empty_file = Tempfile.new('empty_file')
         | 
| 89 | 
            -
                    LibRubyDiff.patch(temp_empty_file.path, delta_file.path, tempfile.path)
         | 
| 90 | 
            -
                  end
         | 
| 91 | 
            -
                  tempfile.rewind
         | 
| 92 | 
            -
                  File.open(full_path, 'w'){|f| f.write(tempfile.read)}
         | 
| 93 | 
            -
                  tempfile.close
         | 
| 94 | 
            -
                  tempfile.unlink
         | 
| 95 | 
            -
                  File.utime(File.atime(full_path), @desired_modtime, full_path)
         | 
| 96 | 
            -
                end
         | 
| 97 | 
            -
             | 
| 98 99 | 
             
                def close_and_unlink_files
         | 
| 99 100 | 
             
                  if signature_exists?
         | 
| 100 101 | 
             
                    @signature_tempfile.close
         | 
| @@ -0,0 +1,108 @@ | |
| 1 | 
            +
            module Entangler
         | 
| 2 | 
            +
              module Executor
         | 
| 3 | 
            +
                module Background
         | 
| 4 | 
            +
                  module Base
         | 
| 5 | 
            +
                    protected
         | 
| 6 | 
            +
                    def wait_for_threads
         | 
| 7 | 
            +
                      @consumer_thread.join
         | 
| 8 | 
            +
                      @remote_io_thread.join
         | 
| 9 | 
            +
                      @local_io_thread.join
         | 
| 10 | 
            +
                      Process.wait @notify_daemon_pid
         | 
| 11 | 
            +
                    end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    def kill_off_threads
         | 
| 14 | 
            +
                      Process.kill("TERM", @notify_daemon_pid) rescue nil
         | 
| 15 | 
            +
                      @consumer_thread.terminate
         | 
| 16 | 
            +
                      @remote_io_thread.terminate
         | 
| 17 | 
            +
                      @local_io_thread.terminate
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    def start_notify_daemon
         | 
| 21 | 
            +
                      logger.info('starting notify daemon')
         | 
| 22 | 
            +
                      r,w = IO.pipe
         | 
| 23 | 
            +
                      @notify_daemon_pid = spawn(start_notify_daemon_cmd, out: w)
         | 
| 24 | 
            +
                      w.close
         | 
| 25 | 
            +
                      @notify_reader = r
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    def start_remote_io
         | 
| 29 | 
            +
                      logger.info('starting remote IO')
         | 
| 30 | 
            +
                      @remote_io_thread = Thread.new do
         | 
| 31 | 
            +
                        begin
         | 
| 32 | 
            +
                          loop do
         | 
| 33 | 
            +
                            msg = Marshal.load(@remote_reader)
         | 
| 34 | 
            +
                            next if msg.nil?
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                            case msg[:type]
         | 
| 37 | 
            +
                            when :new_changes
         | 
| 38 | 
            +
                              process_new_changes(msg[:content])
         | 
| 39 | 
            +
                            when :entangled_files
         | 
| 40 | 
            +
                              process_entangled_files(msg[:content])
         | 
| 41 | 
            +
                            end
         | 
| 42 | 
            +
                          end
         | 
| 43 | 
            +
                        rescue => e
         | 
| 44 | 
            +
                          $stderr.puts e.message
         | 
| 45 | 
            +
                          $stderr.puts e.backtrace.join("\n")
         | 
| 46 | 
            +
                          kill_off_threads
         | 
| 47 | 
            +
                        end
         | 
| 48 | 
            +
                      end
         | 
| 49 | 
            +
                    end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    def start_local_io
         | 
| 52 | 
            +
                      logger.info('starting local IO')
         | 
| 53 | 
            +
                      @local_action_queue = Queue.new
         | 
| 54 | 
            +
                      @local_io_thread = Thread.new do
         | 
| 55 | 
            +
                        begin
         | 
| 56 | 
            +
                          msg = []
         | 
| 57 | 
            +
                          loop do
         | 
| 58 | 
            +
                            ready = IO.select([@notify_reader]).first
         | 
| 59 | 
            +
                            next unless ready && ready.any?
         | 
| 60 | 
            +
                            break if ready.first.eof?
         | 
| 61 | 
            +
                            line = ready.first.gets
         | 
| 62 | 
            +
                            next if line.nil? || line.empty?
         | 
| 63 | 
            +
                            line = line.strip
         | 
| 64 | 
            +
                            next if line == '-'
         | 
| 65 | 
            +
                            @local_action_queue.push line
         | 
| 66 | 
            +
                          end
         | 
| 67 | 
            +
                        rescue => e
         | 
| 68 | 
            +
                          $stderr.puts e.message
         | 
| 69 | 
            +
                          $stderr.puts e.backtrace.join("\n")
         | 
| 70 | 
            +
                          kill_off_threads
         | 
| 71 | 
            +
                        end
         | 
| 72 | 
            +
                      end
         | 
| 73 | 
            +
                    end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    def start_local_consumer
         | 
| 76 | 
            +
                      @consumer_thread = Thread.new do
         | 
| 77 | 
            +
                        loop do
         | 
| 78 | 
            +
                          msg = [@local_action_queue.pop]
         | 
| 79 | 
            +
                          loop do
         | 
| 80 | 
            +
                            sleep 0.2
         | 
| 81 | 
            +
                            break if @local_action_queue.empty?
         | 
| 82 | 
            +
                            while !@local_action_queue.empty?
         | 
| 83 | 
            +
                              msg << @local_action_queue.pop
         | 
| 84 | 
            +
                            end
         | 
| 85 | 
            +
                          end
         | 
| 86 | 
            +
                          while Time.now.to_f <= @notify_sleep
         | 
| 87 | 
            +
                            sleep 0.5
         | 
| 88 | 
            +
                            while !@local_action_queue.empty?
         | 
| 89 | 
            +
                              msg << @local_action_queue.pop
         | 
| 90 | 
            +
                            end
         | 
| 91 | 
            +
                          end
         | 
| 92 | 
            +
                          process_lines(msg.uniq)
         | 
| 93 | 
            +
                          msg = []
         | 
| 94 | 
            +
                          sleep 0.5
         | 
| 95 | 
            +
                        end
         | 
| 96 | 
            +
                      end
         | 
| 97 | 
            +
                    end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                    def start_notify_daemon_cmd
         | 
| 100 | 
            +
                      uname = `uname`.strip.downcase
         | 
| 101 | 
            +
                      raise 'Unsupported OS' unless ['darwin', 'linux'].include?(uname)
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                      "#{File.join(File.dirname(File.dirname(File.dirname(File.dirname(__FILE__)))), 'notifier', 'bin', uname, 'notify')} #{self.base_dir}"
         | 
| 104 | 
            +
                    end
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
              end
         | 
| 108 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            module Entangler
         | 
| 2 | 
            +
              module Executor
         | 
| 3 | 
            +
                module Background
         | 
| 4 | 
            +
                  module Master
         | 
| 5 | 
            +
                    protected
         | 
| 6 | 
            +
                    def start_remote_slave
         | 
| 7 | 
            +
                      require 'open3'
         | 
| 8 | 
            +
                      @remote_writer, @remote_reader, remote_err, @remote_thread = Open3.popen3("ssh -q #{@opts[:remote_user]}@#{@opts[:remote_host]} -p #{@opts[:remote_port]} -C \"source ~/.rvm/environments/default && entangler slave #{@opts[:remote_base_dir]}\"")
         | 
| 9 | 
            +
                      remote_err.close
         | 
| 10 | 
            +
                    end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    def wait_for_threads
         | 
| 13 | 
            +
                      super
         | 
| 14 | 
            +
                      Process.wait @remote_thread[:pid] rescue nil
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    def kill_off_threads
         | 
| 18 | 
            +
                      Process.kill("INT", @remote_thread[:pid])
         | 
| 19 | 
            +
                      super
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -1,23 +1,32 @@ | |
| 1 1 | 
             
            require 'logger'
         | 
| 2 2 | 
             
            require 'fileutils'
         | 
| 3 3 | 
             
            require 'thread'
         | 
| 4 | 
            +
            require_relative 'background/base'
         | 
| 5 | 
            +
            require_relative 'processing/base'
         | 
| 4 6 |  | 
| 5 7 | 
             
            module Entangler
         | 
| 6 8 | 
             
              module Executor
         | 
| 7 9 | 
             
                class Base
         | 
| 10 | 
            +
                  include Entangler::Executor::Background::Base, Entangler::Executor::Processing::Base
         | 
| 11 | 
            +
             | 
| 8 12 | 
             
                  attr_reader :base_dir
         | 
| 9 13 |  | 
| 10 14 | 
             
                  def initialize(base_dir, opts = {})
         | 
| 11 15 | 
             
                    @base_dir = File.realpath(File.expand_path(base_dir))
         | 
| 12 16 | 
             
                    @notify_sleep = 0
         | 
| 17 | 
            +
                    @exported_at = 0
         | 
| 13 18 | 
             
                    @opts = opts
         | 
| 14 19 | 
             
                    @opts[:ignore] = [/^\/\.git.*/, /^\/\.entangler.*/, /^\/\.idea.*/, /^\/log.*/, /^\/tmp.*/] unless @opts.has_key?(:ignore)
         | 
| 15 20 | 
             
                    validate_opts
         | 
| 16 21 | 
             
                    logger.info("Starting executor")
         | 
| 17 22 | 
             
                  end
         | 
| 18 23 |  | 
| 19 | 
            -
                  def  | 
| 20 | 
            -
                     | 
| 24 | 
            +
                  def generate_abs_path(rel_path)
         | 
| 25 | 
            +
                    File.join(self.base_dir, rel_path)
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def strip_base_path(path, base_dir = self.base_dir)
         | 
| 29 | 
            +
                    File.expand_path(path).sub(base_dir, '')
         | 
| 21 30 | 
             
                  end
         | 
| 22 31 |  | 
| 23 32 | 
             
                  def run
         | 
| @@ -27,214 +36,18 @@ module Entangler | |
| 27 36 | 
             
                    start_local_consumer
         | 
| 28 37 | 
             
                    logger.debug("NOTIFY PID: #{@notify_daemon_pid}")
         | 
| 29 38 | 
             
                    Signal.trap("INT") { kill_off_threads }
         | 
| 30 | 
            -
                     | 
| 31 | 
            -
                    @remote_io_thread.join
         | 
| 32 | 
            -
                    @local_io_thread.join
         | 
| 33 | 
            -
                    Process.wait @notify_daemon_pid
         | 
| 34 | 
            -
                  end
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                  def kill_off_threads
         | 
| 37 | 
            -
                    Process.kill("TERM", @notify_daemon_pid) rescue nil
         | 
| 38 | 
            -
                    @consumer_thread.terminate
         | 
| 39 | 
            -
                    @remote_io_thread.terminate
         | 
| 40 | 
            -
                    @local_io_thread.terminate
         | 
| 41 | 
            -
                  end
         | 
| 42 | 
            -
             | 
| 43 | 
            -
                  def start_file_transfers(paths, pipe)
         | 
| 44 | 
            -
                    Marshal.dump(paths.map{|path| EntangledFile.new(path) }, pipe)
         | 
| 39 | 
            +
                    wait_for_threads
         | 
| 45 40 | 
             
                  end
         | 
| 46 41 |  | 
| 47 | 
            -
                   | 
| 48 | 
            -
             | 
| 49 | 
            -
                     | 
| 50 | 
            -
                    @notify_daemon_pid = spawn(start_notify_daemon_cmd, out: w)
         | 
| 51 | 
            -
                    w.close
         | 
| 52 | 
            -
                    @notify_reader = r
         | 
| 53 | 
            -
                  end
         | 
| 54 | 
            -
             | 
| 55 | 
            -
                  def start_local_io
         | 
| 56 | 
            -
                    logger.info('starting local IO')
         | 
| 57 | 
            -
                    @local_action_queue = Queue.new
         | 
| 58 | 
            -
                    @local_io_thread = Thread.new do
         | 
| 59 | 
            -
                      begin
         | 
| 60 | 
            -
                        msg = []
         | 
| 61 | 
            -
                        loop do
         | 
| 62 | 
            -
                          ready = IO.select([@notify_reader]).first
         | 
| 63 | 
            -
                          next unless ready && ready.any?
         | 
| 64 | 
            -
                          break if ready.first.eof?
         | 
| 65 | 
            -
                          line = ready.first.gets
         | 
| 66 | 
            -
                          next if line.nil? || line.empty?
         | 
| 67 | 
            -
                          line = line.strip
         | 
| 68 | 
            -
                          next if line == '-'
         | 
| 69 | 
            -
                          @local_action_queue.push line
         | 
| 70 | 
            -
                        end
         | 
| 71 | 
            -
                      rescue => e
         | 
| 72 | 
            -
                        $stderr.puts e.message
         | 
| 73 | 
            -
                        $stderr.puts e.backtrace.join("\n")
         | 
| 74 | 
            -
                        kill_off_threads
         | 
| 75 | 
            -
                      end
         | 
| 76 | 
            -
                    end
         | 
| 77 | 
            -
                  end
         | 
| 78 | 
            -
             | 
| 79 | 
            -
                  def start_local_consumer
         | 
| 80 | 
            -
                    @consumer_thread = Thread.new do
         | 
| 81 | 
            -
                      loop do
         | 
| 82 | 
            -
                        msg = [@local_action_queue.pop]
         | 
| 83 | 
            -
                        while !@local_action_queue.empty?
         | 
| 84 | 
            -
                          msg << @local_action_queue.pop
         | 
| 85 | 
            -
                        end
         | 
| 86 | 
            -
                        while Time.now.to_i <= @notify_sleep
         | 
| 87 | 
            -
                          sleep 1
         | 
| 88 | 
            -
                          while !@local_action_queue.empty?
         | 
| 89 | 
            -
                            msg << @local_action_queue.pop
         | 
| 90 | 
            -
                          end
         | 
| 91 | 
            -
                        end
         | 
| 92 | 
            -
                        process_lines(msg.uniq)
         | 
| 93 | 
            -
                        msg = []
         | 
| 94 | 
            -
                        sleep 1
         | 
| 95 | 
            -
                      end
         | 
| 96 | 
            -
                    end
         | 
| 42 | 
            +
                  protected
         | 
| 43 | 
            +
                  def validate_opts
         | 
| 44 | 
            +
                    raise "Base directory doesn't exist" unless Dir.exists?(self.base_dir)
         | 
| 97 45 | 
             
                  end
         | 
| 98 46 |  | 
| 99 47 | 
             
                  def send_to_remote(msg = {})
         | 
| 100 48 | 
             
                    Marshal.dump(msg, @remote_writer)
         | 
| 101 49 | 
             
                  end
         | 
| 102 50 |  | 
| 103 | 
            -
                  def start_remote_io
         | 
| 104 | 
            -
                    logger.info('starting remote IO')
         | 
| 105 | 
            -
                    @remote_io_thread = Thread.new do
         | 
| 106 | 
            -
                      begin
         | 
| 107 | 
            -
                        loop do
         | 
| 108 | 
            -
                          msg = Marshal.load(@remote_reader)
         | 
| 109 | 
            -
                          next if msg.nil?
         | 
| 110 | 
            -
             | 
| 111 | 
            -
                          case msg[:type]
         | 
| 112 | 
            -
                          when :new_changes
         | 
| 113 | 
            -
                            logger.debug("Got #{msg[:content].length} new folder changes from remote")
         | 
| 114 | 
            -
             | 
| 115 | 
            -
                            created_dirs = []
         | 
| 116 | 
            -
                            dirs_to_remove = []
         | 
| 117 | 
            -
                            files_to_remove = []
         | 
| 118 | 
            -
                            files_to_update = []
         | 
| 119 | 
            -
             | 
| 120 | 
            -
                            msg[:content].each do |base, changes|
         | 
| 121 | 
            -
                              possible_creation_dirs = changes[:dirs].clone
         | 
| 122 | 
            -
                              possible_creation_files = changes[:files].keys.clone
         | 
| 123 | 
            -
                              full_base_path = generate_abs_path(base)
         | 
| 124 | 
            -
             | 
| 125 | 
            -
                              unless File.directory?(full_base_path)
         | 
| 126 | 
            -
                                FileUtils::mkdir_p(full_base_path)
         | 
| 127 | 
            -
                                @notify_sleep = Time.now.to_i + 60
         | 
| 128 | 
            -
                              end
         | 
| 129 | 
            -
             | 
| 130 | 
            -
                              Dir.entries(full_base_path).each do |f|
         | 
| 131 | 
            -
                                next if ['.', '..'].include? f
         | 
| 132 | 
            -
                                full_path = File.join(generate_abs_path(base), f)
         | 
| 133 | 
            -
                                if File.directory?(full_path)
         | 
| 134 | 
            -
                                  possible_creation_dirs -= [f]
         | 
| 135 | 
            -
                                  dirs_to_remove << full_path unless changes[:dirs].include?(f)
         | 
| 136 | 
            -
                                elsif changes[:files].has_key?(f)
         | 
| 137 | 
            -
                                  possible_creation_files -= [f]
         | 
| 138 | 
            -
                                  files_to_update << File.join(base, f) unless changes[:files][f] == [File.size(full_path), File.mtime(full_path).to_i]
         | 
| 139 | 
            -
                                else
         | 
| 140 | 
            -
                                  files_to_remove << full_path
         | 
| 141 | 
            -
                                end
         | 
| 142 | 
            -
                              end
         | 
| 143 | 
            -
             | 
| 144 | 
            -
                              dirs_to_create = possible_creation_dirs.map{|d| File.join(generate_abs_path(base), d)}
         | 
| 145 | 
            -
                              if dirs_to_create.any?
         | 
| 146 | 
            -
                                logger.debug("Creating #{dirs_to_create.length} dirs")
         | 
| 147 | 
            -
                                @notify_sleep = Time.now.to_i + 60
         | 
| 148 | 
            -
                                FileUtils.mkdir_p dirs_to_create
         | 
| 149 | 
            -
                              end
         | 
| 150 | 
            -
                              created_dirs += dirs_to_create
         | 
| 151 | 
            -
                              files_to_update += possible_creation_files.map{|f| File.join(base, f)}
         | 
| 152 | 
            -
                            end
         | 
| 153 | 
            -
             | 
| 154 | 
            -
                            @notify_sleep = Time.now.to_i + 60 if (files_to_remove + created_dirs + dirs_to_remove + files_to_update).any?
         | 
| 155 | 
            -
             | 
| 156 | 
            -
                            if files_to_remove.any?
         | 
| 157 | 
            -
                              logger.debug("Deleting #{files_to_remove.length} files")
         | 
| 158 | 
            -
                              FileUtils.rm files_to_remove
         | 
| 159 | 
            -
                            end
         | 
| 160 | 
            -
                            if dirs_to_remove.any?
         | 
| 161 | 
            -
                              logger.debug("Deleting #{dirs_to_remove.length} dirs")
         | 
| 162 | 
            -
                              FileUtils.rm_r dirs_to_remove
         | 
| 163 | 
            -
                            end
         | 
| 164 | 
            -
                            if files_to_update.any?
         | 
| 165 | 
            -
                              logger.debug("Creating #{files_to_update.length} new entangled files to sync")
         | 
| 166 | 
            -
                              send_to_remote(type: :entangled_files, content: files_to_update.map{|f| Entangler::EntangledFile.new(f) })
         | 
| 167 | 
            -
                            end
         | 
| 168 | 
            -
                            @notify_sleep = Time.now.to_i + 1 if (files_to_remove + created_dirs + dirs_to_remove + files_to_update).any?
         | 
| 169 | 
            -
                            @notify_sleep += 60 if files_to_update.any?
         | 
| 170 | 
            -
                          when :entangled_files
         | 
| 171 | 
            -
                            logger.debug("Got #{msg[:content].length} entangled files from remote")
         | 
| 172 | 
            -
                            completed_files, updated_files = msg[:content].partition(&:done?)
         | 
| 173 | 
            -
             | 
| 174 | 
            -
                            completed_files.each(&:export)
         | 
| 175 | 
            -
             | 
| 176 | 
            -
                            updated_files = updated_files.find_all{|f| f.state != 1 || f.file_exists? }
         | 
| 177 | 
            -
                            if updated_files.any?
         | 
| 178 | 
            -
                              send_to_remote(type: :entangled_files, content: updated_files)
         | 
| 179 | 
            -
                            end
         | 
| 180 | 
            -
                            @notify_sleep = Time.now.to_i + 1 if completed_files.any?
         | 
| 181 | 
            -
                          end
         | 
| 182 | 
            -
                        end
         | 
| 183 | 
            -
                      rescue => e
         | 
| 184 | 
            -
                        $stderr.puts e.message
         | 
| 185 | 
            -
                        $stderr.puts e.backtrace.join("\n")
         | 
| 186 | 
            -
                        kill_off_threads
         | 
| 187 | 
            -
                      end
         | 
| 188 | 
            -
                    end
         | 
| 189 | 
            -
                  end
         | 
| 190 | 
            -
             | 
| 191 | 
            -
                  def process_lines(lines)
         | 
| 192 | 
            -
                    to_process = lines.map do |line|
         | 
| 193 | 
            -
                      path = line[2..-1]
         | 
| 194 | 
            -
                      stripped_path = strip_base_path(path)
         | 
| 195 | 
            -
                      next unless @opts[:ignore].nil? || @opts[:ignore].none?{|i| stripped_path.match(i) }
         | 
| 196 | 
            -
                      next unless File.directory?(path)
         | 
| 197 | 
            -
             | 
| 198 | 
            -
                      [stripped_path, generate_file_list(path)]
         | 
| 199 | 
            -
                    end.compact.sort_by(&:first)
         | 
| 200 | 
            -
             | 
| 201 | 
            -
                    return unless to_process.any?
         | 
| 202 | 
            -
                    logger.debug("PROCESSING #{to_process.count} folder/s")
         | 
| 203 | 
            -
                    send_to_remote(type: :new_changes, content: to_process)
         | 
| 204 | 
            -
                  end
         | 
| 205 | 
            -
             | 
| 206 | 
            -
                  def generate_file_list(path)
         | 
| 207 | 
            -
                    dirs = []
         | 
| 208 | 
            -
                    files = {}
         | 
| 209 | 
            -
             | 
| 210 | 
            -
                    Dir.entries(path).each do |f|
         | 
| 211 | 
            -
                      next if ['.', '..'].include? f
         | 
| 212 | 
            -
                      f_path = File.join(path, f)
         | 
| 213 | 
            -
                      if File.directory? f_path
         | 
| 214 | 
            -
                        dirs << f
         | 
| 215 | 
            -
                      else
         | 
| 216 | 
            -
                        files[f] = [File.size(f_path), File.mtime(f_path).to_i]
         | 
| 217 | 
            -
                      end
         | 
| 218 | 
            -
                    end
         | 
| 219 | 
            -
             | 
| 220 | 
            -
                    {dirs: dirs, files: files}
         | 
| 221 | 
            -
                  end
         | 
| 222 | 
            -
             | 
| 223 | 
            -
                  def generate_abs_path(rel_path)
         | 
| 224 | 
            -
                    File.join(self.base_dir, rel_path)
         | 
| 225 | 
            -
                  end
         | 
| 226 | 
            -
             | 
| 227 | 
            -
                  def strip_base_path(path, base_dir = self.base_dir)
         | 
| 228 | 
            -
                    File.expand_path(path).sub(base_dir, '')
         | 
| 229 | 
            -
                  end
         | 
| 230 | 
            -
             | 
| 231 | 
            -
                  def start_notify_daemon_cmd
         | 
| 232 | 
            -
                    uname = `uname`.strip.downcase
         | 
| 233 | 
            -
                    raise 'Unsupported OS' unless ['darwin', 'linux'].include?(uname)
         | 
| 234 | 
            -
             | 
| 235 | 
            -
                    "#{File.join(File.dirname(File.dirname(File.dirname(__FILE__))), 'notifier', 'bin', uname, 'notify')} #{self.base_dir}"
         | 
| 236 | 
            -
                  end
         | 
| 237 | 
            -
             | 
| 238 51 | 
             
                  def logger
         | 
| 239 52 | 
             
                    FileUtils::mkdir_p log_dir
         | 
| 240 53 | 
             
                    @logger ||= Logger.new(File.join(log_dir, 'entangler.log'))
         | 
| @@ -1,30 +1,29 @@ | |
| 1 | 
            +
            require_relative 'background/master'
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Entangler
         | 
| 2 4 | 
             
              module Executor
         | 
| 3 5 | 
             
                class Master < Base
         | 
| 4 | 
            -
                   | 
| 5 | 
            -
                    super
         | 
| 6 | 
            -
                    raise 'Missing remote base dir' unless @opts.keys.include?(:remote_base_dir)
         | 
| 7 | 
            -
                    raise 'Missing remote user' unless @opts.keys.include?(:remote_user)
         | 
| 8 | 
            -
                    raise 'Missing remote host' unless @opts.keys.include?(:remote_host)
         | 
| 9 | 
            -
                    @opts[:remote_port] ||= '22'
         | 
| 10 | 
            -
                    res = `ssh -q #{@opts[:remote_user]}@#{@opts[:remote_host]} -p #{@opts[:remote_port]} -C "[[ -d '#{@opts[:remote_base_dir]}' ]] && echo 'ok' || echo 'missing'"`
         | 
| 11 | 
            -
                    raise 'Cannot connect to remote' if res.empty?
         | 
| 12 | 
            -
                    raise 'Remote base dir invalid' unless res.strip == 'ok'
         | 
| 13 | 
            -
                  end
         | 
| 6 | 
            +
                  include Entangler::Executor::Background::Master
         | 
| 14 7 |  | 
| 15 8 | 
             
                  def run
         | 
| 16 9 | 
             
                    perform_initial_rsync
         | 
| 17 10 | 
             
                    sleep 1
         | 
| 18 11 | 
             
                    start_remote_slave
         | 
| 19 12 | 
             
                    super
         | 
| 20 | 
            -
                    Process.wait @remote_thread[:pid] rescue nil
         | 
| 21 13 | 
             
                    @remote_writer.close
         | 
| 22 14 | 
             
                    @remote_reader.close
         | 
| 23 15 | 
             
                  end
         | 
| 24 16 |  | 
| 25 | 
            -
                   | 
| 26 | 
            -
             | 
| 17 | 
            +
                  private
         | 
| 18 | 
            +
                  def validate_opts
         | 
| 27 19 | 
             
                    super
         | 
| 20 | 
            +
                    raise 'Missing remote base dir' unless @opts.keys.include?(:remote_base_dir)
         | 
| 21 | 
            +
                    raise 'Missing remote user' unless @opts.keys.include?(:remote_user)
         | 
| 22 | 
            +
                    raise 'Missing remote host' unless @opts.keys.include?(:remote_host)
         | 
| 23 | 
            +
                    @opts[:remote_port] ||= '22'
         | 
| 24 | 
            +
                    res = `ssh -q #{@opts[:remote_user]}@#{@opts[:remote_host]} -p #{@opts[:remote_port]} -C "[[ -d '#{@opts[:remote_base_dir]}' ]] && echo 'ok' || echo 'missing'"`
         | 
| 25 | 
            +
                    raise 'Cannot connect to remote' if res.empty?
         | 
| 26 | 
            +
                    raise 'Remote base dir invalid' unless res.strip == 'ok'
         | 
| 28 27 | 
             
                  end
         | 
| 29 28 |  | 
| 30 29 | 
             
                  def perform_initial_rsync
         | 
| @@ -34,12 +33,6 @@ module Entangler | |
| 34 33 | 
             
                    end
         | 
| 35 34 | 
             
                    logger.debug 'Initial sync complete'
         | 
| 36 35 | 
             
                  end
         | 
| 37 | 
            -
             | 
| 38 | 
            -
                  def start_remote_slave
         | 
| 39 | 
            -
                    require 'open3'
         | 
| 40 | 
            -
                    @remote_writer, @remote_reader, remote_err, @remote_thread = Open3.popen3("ssh -q #{@opts[:remote_user]}@#{@opts[:remote_host]} -p #{@opts[:remote_port]} -C \"source ~/.rvm/environments/default && entangler slave #{@opts[:remote_base_dir]}\"")
         | 
| 41 | 
            -
                    remote_err.close
         | 
| 42 | 
            -
                  end
         | 
| 43 36 | 
             
                end
         | 
| 44 37 | 
             
              end
         | 
| 45 38 | 
             
            end
         | 
| @@ -0,0 +1,122 @@ | |
| 1 | 
            +
            module Entangler
         | 
| 2 | 
            +
              module Executor
         | 
| 3 | 
            +
                module Processing
         | 
| 4 | 
            +
                  module Base
         | 
| 5 | 
            +
                    protected
         | 
| 6 | 
            +
                    def generate_file_list(path)
         | 
| 7 | 
            +
                      dirs = []
         | 
| 8 | 
            +
                      files = {}
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                      Dir.entries(path).each do |f|
         | 
| 11 | 
            +
                        next if ['.', '..'].include? f
         | 
| 12 | 
            +
                        f_path = File.join(path, f)
         | 
| 13 | 
            +
                        if File.directory? f_path
         | 
| 14 | 
            +
                          dirs << f
         | 
| 15 | 
            +
                        else
         | 
| 16 | 
            +
                          files[f] = [File.size(f_path), File.mtime(f_path).to_i]
         | 
| 17 | 
            +
                        end
         | 
| 18 | 
            +
                      end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                      {dirs: dirs, files: files}
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    def process_new_changes(content)
         | 
| 24 | 
            +
                      logger.debug("Got #{content.length} new folder changes from remote")
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                      created_dirs = []
         | 
| 27 | 
            +
                      dirs_to_remove = []
         | 
| 28 | 
            +
                      files_to_remove = []
         | 
| 29 | 
            +
                      files_to_update = []
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                      content.each do |base, changes|
         | 
| 32 | 
            +
                        possible_creation_dirs = changes[:dirs].clone
         | 
| 33 | 
            +
                        possible_creation_files = changes[:files].keys.clone
         | 
| 34 | 
            +
                        full_base_path = generate_abs_path(base)
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                        unless File.directory?(full_base_path)
         | 
| 37 | 
            +
                          FileUtils::mkdir_p(full_base_path)
         | 
| 38 | 
            +
                          @notify_sleep = Time.now.to_i + 60
         | 
| 39 | 
            +
                        end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                        Dir.entries(full_base_path).each do |f|
         | 
| 42 | 
            +
                          next if ['.', '..'].include? f
         | 
| 43 | 
            +
                          full_path = File.join(generate_abs_path(base), f)
         | 
| 44 | 
            +
                          if File.directory?(full_path)
         | 
| 45 | 
            +
                            possible_creation_dirs -= [f]
         | 
| 46 | 
            +
                            dirs_to_remove << full_path unless changes[:dirs].include?(f)
         | 
| 47 | 
            +
                          elsif changes[:files].has_key?(f)
         | 
| 48 | 
            +
                            possible_creation_files -= [f]
         | 
| 49 | 
            +
                            files_to_update << File.join(base, f) unless changes[:files][f] == [File.size(full_path), File.mtime(full_path).to_i]
         | 
| 50 | 
            +
                          else
         | 
| 51 | 
            +
                            files_to_remove << full_path
         | 
| 52 | 
            +
                          end
         | 
| 53 | 
            +
                        end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                        dirs_to_create = possible_creation_dirs.map{|d| File.join(generate_abs_path(base), d)}
         | 
| 56 | 
            +
                        if dirs_to_create.any?
         | 
| 57 | 
            +
                          logger.debug("Creating #{dirs_to_create.length} dirs")
         | 
| 58 | 
            +
                          @notify_sleep = Time.now.to_i + 60
         | 
| 59 | 
            +
                          FileUtils.mkdir_p dirs_to_create
         | 
| 60 | 
            +
                        end
         | 
| 61 | 
            +
                        created_dirs += dirs_to_create
         | 
| 62 | 
            +
                        files_to_update += possible_creation_files.map{|f| File.join(base, f)}
         | 
| 63 | 
            +
                      end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                      @notify_sleep = Time.now.to_i + 60 if (files_to_remove + created_dirs + dirs_to_remove + files_to_update).any?
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                      if files_to_remove.any?
         | 
| 68 | 
            +
                        logger.debug("Deleting #{files_to_remove.length} files")
         | 
| 69 | 
            +
                        FileUtils.rm files_to_remove
         | 
| 70 | 
            +
                      end
         | 
| 71 | 
            +
                      if dirs_to_remove.any?
         | 
| 72 | 
            +
                        logger.debug("Deleting #{dirs_to_remove.length} dirs")
         | 
| 73 | 
            +
                        FileUtils.rm_r dirs_to_remove
         | 
| 74 | 
            +
                      end
         | 
| 75 | 
            +
                      if files_to_update.any?
         | 
| 76 | 
            +
                        logger.debug("Creating #{files_to_update.length} new entangled files to sync")
         | 
| 77 | 
            +
                        send_to_remote(type: :entangled_files, content: files_to_update.map{|f| Entangler::EntangledFile.new(f) })
         | 
| 78 | 
            +
                      end
         | 
| 79 | 
            +
                      @notify_sleep = Time.now.to_f + 0.5 if (files_to_remove + created_dirs + dirs_to_remove + files_to_update).any?
         | 
| 80 | 
            +
                      @notify_sleep += 60 if files_to_update.any?
         | 
| 81 | 
            +
                    end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                    def process_entangled_files(content)
         | 
| 84 | 
            +
                      logger.debug("Got #{content.length} entangled files from remote")
         | 
| 85 | 
            +
                      completed_files, updated_files = content.partition(&:done?)
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                      if completed_files.any?
         | 
| 88 | 
            +
                        @exported_at = Time.now.to_f
         | 
| 89 | 
            +
                        @exported_folders = completed_files.map{|ef| "#{File.dirname(generate_abs_path(ef.path))}/" }.uniq
         | 
| 90 | 
            +
                        completed_files.each(&:export)
         | 
| 91 | 
            +
                      end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                      updated_files = updated_files.find_all{|f| f.state != 1 || f.file_exists? }
         | 
| 94 | 
            +
                      if updated_files.any?
         | 
| 95 | 
            +
                        send_to_remote(type: :entangled_files, content: updated_files)
         | 
| 96 | 
            +
                      end
         | 
| 97 | 
            +
                      @notify_sleep = Time.now.to_f + 0.5 if completed_files.any?
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                    def process_lines(lines)
         | 
| 101 | 
            +
                      paths = lines.map{|line| line[2..-1] }
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                      if @exported_at < Time.now.to_f && Time.now.to_f < @exported_at + 2
         | 
| 104 | 
            +
                        paths -= @exported_folders
         | 
| 105 | 
            +
                      end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                      to_process = paths.map do |path|
         | 
| 108 | 
            +
                        stripped_path = strip_base_path(path)
         | 
| 109 | 
            +
                        next unless @opts[:ignore].nil? || @opts[:ignore].none?{|i| stripped_path.match(i) }
         | 
| 110 | 
            +
                        next unless File.directory?(path)
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                        [stripped_path, generate_file_list(path)]
         | 
| 113 | 
            +
                      end.compact.sort_by(&:first)
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                      return unless to_process.any?
         | 
| 116 | 
            +
                      logger.debug("PROCESSING #{to_process.count} folder/s")
         | 
| 117 | 
            +
                      send_to_remote(type: :new_changes, content: to_process)
         | 
| 118 | 
            +
                    end
         | 
| 119 | 
            +
                  end
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
              end
         | 
| 122 | 
            +
            end
         | 
    
        data/lib/entangler/version.rb
    CHANGED
    
    
    
        data/lib/entangler.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: entangler
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.1. | 
| 4 | 
            +
              version: 0.1.2
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Dave Allie
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2016-10- | 
| 11 | 
            +
            date: 2016-10-25 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: bundler
         | 
| @@ -88,8 +88,11 @@ files: | |
| 88 88 | 
             
            - exe/entangler
         | 
| 89 89 | 
             
            - lib/entangler.rb
         | 
| 90 90 | 
             
            - lib/entangler/entangled_file.rb
         | 
| 91 | 
            +
            - lib/entangler/executor/background/base.rb
         | 
| 92 | 
            +
            - lib/entangler/executor/background/master.rb
         | 
| 91 93 | 
             
            - lib/entangler/executor/base.rb
         | 
| 92 94 | 
             
            - lib/entangler/executor/master.rb
         | 
| 95 | 
            +
            - lib/entangler/executor/processing/base.rb
         | 
| 93 96 | 
             
            - lib/entangler/executor/slave.rb
         | 
| 94 97 | 
             
            - lib/entangler/version.rb
         | 
| 95 98 | 
             
            - lib/notifier/bin/darwin/notify
         |