entangler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 545e59239dbbe8601083e4d7a827721a0aafbdd4
4
+ data.tar.gz: f97f1d7eec1f101ad4211acbd872c31ca9876179
5
+ SHA512:
6
+ metadata.gz: d5f23e59f7bb1319594d4805a82d959e443ea13038c874ccade02b6784de9b2f2065e877d03e0a2051cd00a348e4db18439481a73905d3428bbc3b7facaaf259
7
+ data.tar.gz: 83fdd384fa0b618539fccc33d1bd8491b4c2edc341bbe8f7a37471405c11820341968d90f434dca165ca4d2c5e2f05062db0742144528cc8c59b08955a07cd86
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /log/
11
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.0
5
+ before_install: gem install bundler -v 1.12.5
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at dave@daveallie.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in entangler.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Dave Allie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Entangler
2
+
3
+
4
+
5
+ ## Installation
6
+
7
+ $ gem install entangler
8
+
9
+ ## Usage
10
+
11
+ ```shell
12
+ entangler master /some/base/path user@remote:/some/remote/path
13
+ ```
14
+
15
+ ## Development
16
+
17
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
18
+
19
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
20
+
21
+ ## Contributing
22
+
23
+ Bug reports and pull requests are welcome on GitHub at https://github.com/daveallie/entangler. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
24
+
25
+
26
+ ## License
27
+
28
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "entangler"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/entangler.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'entangler/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "entangler"
8
+ spec.version = Entangler::VERSION
9
+ spec.authors = ["Dave Allie"]
10
+ spec.email = ["dave@daveallie.com"]
11
+
12
+ spec.summary = %q{Two way file syncer using platform native notify and rdiff syncing.}
13
+ spec.description = %q{Two way file syncer using platform native notify and rdiff syncing.}
14
+ spec.homepage = "https://github.com/daveallie/entangler"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.12"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec", "~> 3.0"
25
+ spec.add_dependency "lib_ruby_diff", "~> 0.1"
26
+ end
data/exe/entangler ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ require "entangler"
3
+
4
+ mode = ARGV[0]
5
+ opts = {}
6
+
7
+ raise 'Must supply at least 2 args' unless ARGV.length >= 2
8
+
9
+ if mode == 'master'
10
+ raise 'Missing remote argument' unless ARGV.length >= 3
11
+ user, rest = ARGV[2].split('@', 2)
12
+ host, path = rest.split(':', 2)
13
+ opts = {remote_base_dir: path, remote_user: user, remote_host: host}
14
+ opts[:remote_port] = ARGV[3] if ARGV[3]
15
+ elsif mode == 'slave'
16
+ opts[:mode] = 'slave'
17
+ else
18
+ raise "Mode: #{mode} unknown, please read documentation"
19
+ end
20
+
21
+ Entangler.run(ARGV[1], opts)
@@ -0,0 +1,139 @@
1
+ require 'lib_ruby_diff'
2
+ require 'tempfile'
3
+
4
+ module Entangler
5
+ class EntangledFile
6
+ # 0: file initialized
7
+ # 1: sig loaded
8
+ # 2: delta loaded
9
+ attr_accessor :state
10
+ attr_accessor :desired_modtime
11
+ attr_reader :path
12
+
13
+ def initialize(rel_path)
14
+ @path = rel_path
15
+ @state = 0
16
+ @desired_modtime = Time.now.to_i
17
+ end
18
+
19
+ def done?
20
+ @state == 2
21
+ end
22
+
23
+ def full_path
24
+ Entangler.executor.generate_abs_path(@path)
25
+ end
26
+
27
+ def file_exists?
28
+ File.exists?(full_path)
29
+ end
30
+
31
+ def signature_exists?
32
+ defined?(@signature_tempfile)
33
+ end
34
+
35
+ def signature_file
36
+ return @signature_tempfile if signature_exists?
37
+ @signature_tempfile = Tempfile.new('sig_file')
38
+ if File.exists?(full_path)
39
+ LibRubyDiff.signature(full_path, @signature_tempfile.path)
40
+ else
41
+ temp_empty_file = Tempfile.new('empty_file')
42
+ LibRubyDiff.signature(temp_empty_file.path, @signature_tempfile.path)
43
+ end
44
+ @signature_tempfile.rewind
45
+ @signature_tempfile
46
+ end
47
+
48
+ def write_signature(contents)
49
+ @signature_tempfile = Tempfile.new('sig_file')
50
+ @signature_tempfile.write(contents)
51
+ @signature_tempfile.rewind
52
+ end
53
+
54
+ def signature
55
+ signature_file.read
56
+ end
57
+
58
+ def delta_exists?
59
+ defined?(@delta_tempfile)
60
+ end
61
+
62
+ def delta_file
63
+ return @delta_tempfile if delta_exists?
64
+ raise "Signature file doesn't exist when creaing delta" unless signature_exists?
65
+
66
+ @delta_tempfile = Tempfile.new('delta_file')
67
+ LibRubyDiff.delta(full_path, signature_file.path, @delta_tempfile.path)
68
+ @delta_tempfile.rewind
69
+ @delta_tempfile
70
+ end
71
+
72
+ def write_delta(contents)
73
+ @delta_tempfile = Tempfile.new('delta_file')
74
+ @delta_tempfile.write(contents)
75
+ @delta_tempfile.rewind
76
+ end
77
+
78
+ def delta
79
+ delta_file.read
80
+ end
81
+
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
+ def close_and_unlink_files
99
+ if signature_exists?
100
+ @signature_tempfile.close
101
+ @signature_tempfile.unlink
102
+ @signature_tempfile = nil
103
+ end
104
+
105
+ if delta_exists?
106
+ @delta_tempfile.close
107
+ @delta_tempfile.unlink
108
+ @delta_tempfile = nil
109
+ end
110
+ end
111
+
112
+ def marshal_dump
113
+ last_arg = nil
114
+
115
+ if @state == 0
116
+ last_arg = signature_file.read
117
+ @state = 1
118
+ elsif @state == 1
119
+ @desired_modtime = File.mtime(full_path).to_i
120
+ last_arg = delta_file.read
121
+ @state = 2
122
+ end
123
+
124
+ close_and_unlink_files
125
+
126
+ [@path, @state, @desired_modtime, last_arg]
127
+ end
128
+
129
+ def marshal_load(array)
130
+ @path, @state, @desired_modtime, last_arg = *array
131
+
132
+ if @state == 1
133
+ write_signature(last_arg)
134
+ elsif @state == 2
135
+ write_delta(last_arg)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,242 @@
1
+ require 'logger'
2
+ require 'fileutils'
3
+ require 'thread'
4
+
5
+ module Entangler
6
+ module Executor
7
+ class Base
8
+ attr_reader :base_dir
9
+
10
+ def initialize(base_dir, opts = {})
11
+ @base_dir = File.realpath(File.expand_path(base_dir))
12
+ @notify_sleep = 0
13
+ @opts = opts
14
+ @opts[:ignore] = [/^\/\.git.*/, /^\/\.entangler.*/, /^\/\.idea.*/, /^\/log.*/, /^\/tmp.*/] unless @opts.has_key?(:ignore)
15
+ validate_opts
16
+ logger.info("Starting executor")
17
+ end
18
+
19
+ def validate_opts
20
+ raise "Base directory doesn't exist" unless Dir.exists?(self.base_dir)
21
+ end
22
+
23
+ def run
24
+ start_notify_daemon
25
+ start_local_io
26
+ start_remote_io
27
+ start_local_consumer
28
+ logger.debug("NOTIFY PID: #{@notify_daemon_pid}")
29
+ Signal.trap("INT") { kill_off_threads }
30
+ @consumer_thread.join
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)
45
+ end
46
+
47
+ def start_notify_daemon
48
+ logger.info('starting notify daemon')
49
+ r,w = IO.pipe
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
97
+ end
98
+
99
+ def send_to_remote(msg = {})
100
+ Marshal.dump(msg, @remote_writer)
101
+ end
102
+
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
+
124
+ Dir.entries(generate_abs_path(base)).each do |f|
125
+ next if ['.', '..'].include? f
126
+ full_path = File.join(generate_abs_path(base), f)
127
+ if File.directory?(full_path)
128
+ possible_creation_dirs -= [f]
129
+ dirs_to_remove << full_path unless changes[:dirs].include?(f)
130
+ elsif changes[:files].has_key?(f)
131
+ possible_creation_files -= [f]
132
+ files_to_update << File.join(base, f) unless changes[:files][f] == [File.size(full_path), File.mtime(full_path).to_i]
133
+ else
134
+ files_to_remove << full_path
135
+ end
136
+ end
137
+
138
+ dirs_to_create = possible_creation_dirs.map{|d| File.join(generate_abs_path(base), d)}
139
+ if dirs_to_create.any?
140
+ logger.debug("Creating #{dirs_to_create.length} dirs")
141
+ @notify_sleep = Time.now.to_i + 60
142
+ FileUtils.mkdir_p dirs_to_create
143
+ end
144
+ created_dirs += dirs_to_create
145
+ files_to_update += possible_creation_files.map{|f| File.join(base, f)}
146
+ end
147
+
148
+ @notify_sleep = Time.now.to_i + 60 if (files_to_remove + created_dirs + dirs_to_remove + files_to_update).any?
149
+
150
+ if files_to_remove.any?
151
+ logger.debug("Deleting #{files_to_remove.length} files")
152
+ FileUtils.rm files_to_remove
153
+ end
154
+ if dirs_to_remove.any?
155
+ logger.debug("Deleting #{dirs_to_remove.length} dirs")
156
+ FileUtils.rm_r dirs_to_remove
157
+ end
158
+ if files_to_update.any?
159
+ logger.debug("Creating #{files_to_update.length} new entangled files to sync")
160
+ send_to_remote(type: :entangled_files, content: files_to_update.map{|f| Entangler::EntangledFile.new(f) })
161
+ end
162
+ @notify_sleep = Time.now.to_i + 1 if (files_to_remove + created_dirs + dirs_to_remove + files_to_update).any?
163
+ @notify_sleep += 60 if files_to_update.any?
164
+ when :entangled_files
165
+ logger.debug("Got #{msg[:content].length} entangled files from remote")
166
+ completed_files, updated_files = msg[:content].partition(&:done?)
167
+
168
+ completed_files.each(&:export)
169
+
170
+ updated_files = updated_files.find_all{|f| f.state != 1 || f.file_exists? }
171
+ if updated_files.any?
172
+ send_to_remote(type: :entangled_files, content: updated_files)
173
+ end
174
+ @notify_sleep = Time.now.to_i + 1 if completed_files.any?
175
+ end
176
+ end
177
+ rescue => e
178
+ $stderr.puts e.message
179
+ $stderr.puts e.backtrace.join("\n")
180
+ kill_off_threads
181
+ end
182
+ end
183
+ end
184
+
185
+ def process_lines(lines)
186
+ to_process = lines.map do |line|
187
+ path = line[2..-1]
188
+ stripped_path = strip_base_path(path)
189
+ next unless @opts[:ignore].nil? || @opts[:ignore].none?{|i| stripped_path.match(i) }
190
+ next unless File.directory?(path)
191
+
192
+ [stripped_path, generate_file_list(path)]
193
+ end.compact.sort_by(&:first)
194
+
195
+ return unless to_process.any?
196
+ logger.debug("PROCESSING #{to_process.count} folder/s")
197
+ send_to_remote(type: :new_changes, content: to_process)
198
+ end
199
+
200
+ def generate_file_list(path)
201
+ dirs = []
202
+ files = {}
203
+
204
+ Dir.entries(path).each do |f|
205
+ next if ['.', '..'].include? f
206
+ f_path = File.join(path, f)
207
+ if File.directory? f_path
208
+ dirs << f
209
+ else
210
+ files[f] = [File.size(f_path), File.mtime(f_path).to_i]
211
+ end
212
+ end
213
+
214
+ {dirs: dirs, files: files}
215
+ end
216
+
217
+ def generate_abs_path(rel_path)
218
+ File.join(self.base_dir, rel_path)
219
+ end
220
+
221
+ def strip_base_path(path, base_dir = self.base_dir)
222
+ File.expand_path(path).sub(base_dir, '')
223
+ end
224
+
225
+ def start_notify_daemon_cmd
226
+ uname = `uname`.strip.downcase
227
+ raise 'Unsupported OS' unless ['darwin', 'linux'].include?(uname)
228
+
229
+ "#{File.join(File.dirname(File.dirname(File.dirname(__FILE__))), 'notifier', 'bin', uname, 'notify')} #{self.base_dir}"
230
+ end
231
+
232
+ def logger
233
+ FileUtils::mkdir_p log_dir
234
+ @logger ||= Logger.new(File.join(log_dir, 'entangler.log'))
235
+ end
236
+
237
+ def log_dir
238
+ File.join(base_dir, '.entangler', 'log')
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,45 @@
1
+ module Entangler
2
+ module Executor
3
+ class Master < Base
4
+ def validate_opts
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
14
+
15
+ def run
16
+ perform_initial_rsync
17
+ sleep 1
18
+ start_remote_slave
19
+ super
20
+ Process.wait @remote_thread[:pid] rescue nil
21
+ @remote_writer.close
22
+ @remote_reader.close
23
+ end
24
+
25
+ def kill_off_threads
26
+ Process.kill("INT", @remote_thread[:pid])
27
+ super
28
+ end
29
+
30
+ def perform_initial_rsync
31
+ logger.info 'Running initial sync'
32
+ IO.popen("rsync -azv --exclude .git --exclude log --exclude .entangler --exclude tmp -e \"ssh -p #{@opts[:remote_port]}\" --delete #{base_dir}/ #{@opts[:remote_user]}@#{@opts[:remote_host]}:#{@opts[:remote_base_dir]}/").each do |line|
33
+ logger.debug line.chomp
34
+ end
35
+ logger.debug 'Initial sync complete'
36
+ 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
+ end
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ module Entangler
2
+ module Executor
3
+ class Slave < Base
4
+ def initialize(base_dir, opts = {})
5
+ super(base_dir, opts)
6
+ STDIN.binmode
7
+ STDOUT.binmode
8
+ STDIN.sync = true
9
+ STDOUT.sync = true
10
+
11
+ @remote_reader = STDIN
12
+ @remote_writer = STDOUT
13
+ $stderr.reopen(File.join(log_dir, 'entangler.err'), "w")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Entangler
2
+ VERSION = "0.1.0"
3
+ end