entangler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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