remote_sh 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
+ SHA256:
3
+ metadata.gz: 2cb32323000e3b9ae8bad777982e2b0f305cfb7f58a5963165b623b90957c3c0
4
+ data.tar.gz: c016fb49746c43098ded07c64a88f5cace021facca28fd8d7fa48d8bce33a43b
5
+ SHA512:
6
+ metadata.gz: 76c505f34d51c8ae4ce831358dd40841ca846697247831586ee59b0ccb36ca1e565efba0dd210716cc85e2039b819186fbecada4df56a720ed3dcc7b7bc9bcd1
7
+ data.tar.gz: 65dfa6e49f6112303b5ce406703e60560dc378728e2fbd0a660147311c514e9912431fb1ea54fc92e5512174cfc33f5719032053c42ab7b6ce508518fe9348dd
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Remote
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/remote`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ ```bash
14
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
15
+ ```
16
+
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
18
+
19
+ ```bash
20
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ 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.
30
+
31
+ 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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/remote.
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "fileutils"
5
+
6
+ module RemoteSh
7
+ class Cli < Thor
8
+ desc "attach", "attaches to remote host and starts file sync"
9
+ def attach
10
+ hostname = WorkspaceConfiguration.host_config["name"]
11
+
12
+ FileUtils.mkdir_p("#{PID_FOLDER}/#{hostname}")
13
+
14
+ local_path = WorkspaceConfiguration::WOKRING_DIR
15
+ pid_filename = "#{PID_FOLDER}/#{hostname}/#{local_path.gsub("/", "__")}"
16
+
17
+ unless File.exist?(pid_filename)
18
+ File.open(pid_filename, "w") do |f|
19
+ @owns_sync = true
20
+ f.write("busy")
21
+ end
22
+ end
23
+
24
+ host = WorkspaceConfiguration.host_config["host"]
25
+ host_path = WorkspaceConfiguration.config["host_path"]
26
+
27
+ if @owns_sync
28
+ FileSyncer.start(host, host_path, WorkspaceConfiguration.config["ignore"] || [])
29
+ PortForwarder.start_or_skip(WorkspaceConfiguration.host_config)
30
+ end
31
+
32
+ SshHelper.attach(host, host_path)
33
+ ensure
34
+ File.delete(pid_filename) if @owns_sync
35
+ end
36
+
37
+ desc "sync_down", "synces down remote changes"
38
+ def sync_down
39
+ RsyncHelper.down(
40
+ WorkspaceConfiguration::WOKRING_DIR,
41
+ WorkspaceConfiguration.config["host_path"],
42
+ WorkspaceConfiguration.host_config["host"],
43
+ WorkspaceConfiguration.config["ignore"] || []
44
+ )
45
+ end
46
+
47
+ desc "init", "inits"
48
+ def init
49
+ WorkspaceConfiguration.init!
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'webrick'
5
+ require 'socket'
6
+ require 'filewatcher'
7
+
8
+ module RemoteSh
9
+ module FileSyncer
10
+ module_function
11
+
12
+ def start(host, host_path, ignored_files)
13
+ local_path = WorkspaceConfiguration::WOKRING_DIR
14
+
15
+ RsyncHelper.up(local_path, host_path, host, ignored_files)
16
+
17
+ mutex = Mutex.new
18
+
19
+ Thread.new do
20
+ Filewatcher.new("#{local_path}/**/{.[^\.]*,*}", interval: 1).watch do |_changes|
21
+ if @sync_side == :client || !mutex.locked?
22
+ mutex.synchronize do
23
+ @sync_side = :client
24
+ RsyncHelper.up(local_path, host_path, host, ignored_files)
25
+ @sync_side = nil
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ Thread.new do
32
+ start_server(mutex, local_path, host_path, host, ignored_files)
33
+ end
34
+ end
35
+
36
+ def start_server(mutex, local_path, host_path, host, ignored_files)
37
+ pid_filename = "#{PID_FOLDER}/#{local_path.gsub("/", "__")}_syncing"
38
+ socket_file = "#{pid_filename}_socket"
39
+
40
+ path = socket_file.split("/")[...-1].join("/")
41
+
42
+ `ssh #{host} "rm #{socket_file}"`
43
+ `ssh #{host} "mkdir -p #{path}"`
44
+
45
+ Thread.new do
46
+ ruby_code = <<~RUBY.gsub('"', '\"')
47
+ require "net_http_unix"
48
+ require "filewatcher"
49
+
50
+ Thread.new do
51
+ Filewatcher.new("#{host_path}/**/{.[^\.]*,*}", interval: 1).watch do |_changes|
52
+ request = Net::HTTP::Get.new("/request_sync")
53
+ NetX::HTTPUnix.new("unix://" + "#{socket_file}").request(request)
54
+ end
55
+ end
56
+
57
+ loop do
58
+ request = Net::HTTP::Get.new("/ping")
59
+ NetX::HTTPUnix.new("unix://" + "#{socket_file}").request(request)
60
+ sleep(3)
61
+ end
62
+ RUBY
63
+
64
+ Open3.capture3("ssh -q #{host} 'ruby -e \"#{ruby_code}\"'")
65
+ end
66
+
67
+ UNIXServer.open(socket_file) do |ssocket|
68
+ SshHelper.forward_local_socket(host, socket_file)
69
+
70
+ server = WEBrick::HTTPServer.new(
71
+ DoNotListen: true,
72
+ Logger: WEBrick::Log.new("/dev/null"),
73
+ AccessLog: [],
74
+ )
75
+ server.listeners << ssocket
76
+
77
+ server.mount_proc "/request_sync" do |_req, res|
78
+ if @sync_side == :server || !mutex.locked?
79
+ mutex.synchronize do
80
+ @sync_side = :server
81
+ RsyncHelper.down(local_path, host_path, host, ignored_files)
82
+ @sync_side = nil
83
+ end
84
+ end
85
+
86
+ res.body = "request_sync"
87
+ end
88
+
89
+ server.mount_proc "/ping" do |_req, res|
90
+ res.body = "ping"
91
+ end
92
+
93
+ trap("INT") { server.shutdown }
94
+
95
+ server.start
96
+ end
97
+ ensure
98
+ SshHelper.close_local_socket(host, socket_file)
99
+ File.unlink(socket_file)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module RemoteSh
6
+ class HostsConfiguration
7
+ include Singleton
8
+
9
+ CONFIGURATION_PATH = "#{Dir.home}/.config/remote_sh"
10
+ CONFIGURATION_FILE = "#{CONFIGURATION_PATH}/servers.yaml"
11
+
12
+ # servers:
13
+ # - name: main
14
+ # host: root@127.0.0.1
15
+ # blacklist_ports: ['22', '25', '631']
16
+
17
+ def self.config
18
+ instance.config
19
+ end
20
+
21
+ def parse
22
+ FileUtils.mkdir_p(CONFIGURATION_PATH)
23
+ @config = YAML.load_file(CONFIGURATION_FILE)
24
+ end
25
+
26
+ def config
27
+ @config || parse
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RemoteSh
4
+ module PortForwarder
5
+ module_function
6
+
7
+ def start_or_skip(host_config)
8
+ host_name = host_config["name"]
9
+ host_host = host_config["host"]
10
+
11
+ remote_blacklist_ports = host_config["blacklist_ports"]
12
+ local_blacklisted_ports = HostsConfiguration.config["blacklisted_ports"]
13
+
14
+ pid_filename = "#{PID_FOLDER}/#{host_name}_portforwarding"
15
+
16
+ return if File.exist?(pid_filename)
17
+
18
+ # puts "started"
19
+
20
+ pid_folder = "#{PID_FOLDER}/#{host_name}"
21
+
22
+ local_opened_ports = current_ports(local_blacklisted_ports)
23
+ local_opened_ports.each { |port| SshHelper.forward_local_port(host_host, port) }
24
+
25
+ remote_opened_ports = SshHelper.current_ports(host_host, remote_blacklist_ports + local_opened_ports)
26
+ remote_opened_ports.each { |port| SshHelper.forward_port(host_host, port) }
27
+
28
+ Process.fork do
29
+ File.open(pid_filename, "w") { |f| f.write("busy") }
30
+
31
+ work_loop(
32
+ host_host,
33
+ pid_folder,
34
+ remote_blacklist_ports,
35
+ remote_opened_ports,
36
+ local_blacklisted_ports,
37
+ local_opened_ports
38
+ )
39
+ SshHelper.current_ports(host_host, remote_blacklist_ports + local_opened_ports).each { |port| SshHelper.close_port(host_host, port) }
40
+ current_ports(local_blacklisted_ports + remote_opened_ports).each { |port| SshHelper.close_local_port(host_host, port) }
41
+ ensure
42
+ File.delete(pid_filename)
43
+ end
44
+ end
45
+
46
+ def current_ports(local_blacklisted_ports)
47
+ `lsof -PiTCP -sTCP:LISTEN`
48
+ .split("\n")
49
+ .map(&:split)[1..]
50
+ .map { _1[8].split(":").last }
51
+ .uniq - local_blacklisted_ports
52
+ end
53
+
54
+ def work_loop(
55
+ host,
56
+ pid_folder,
57
+ remote_blacklisted_ports,
58
+ remote_opened_ports,
59
+ local_blacklisted_ports,
60
+ local_opened_ports
61
+ )
62
+ # remote ports
63
+ remote_opened_ports_was = remote_opened_ports
64
+ remote_opened_ports = SshHelper.current_ports(host, remote_blacklisted_ports + local_opened_ports)
65
+
66
+ remote_ports_to_close = remote_opened_ports_was - remote_opened_ports
67
+ remote_ports_to_open = remote_opened_ports - remote_opened_ports_was
68
+
69
+ remote_ports_to_open.each { |port| SshHelper.forward_port(host, port) }
70
+ remote_ports_to_close.each { |port| SshHelper.close_port(host, port) }
71
+
72
+ #local ports
73
+
74
+ local_opened_ports_was = local_opened_ports
75
+ local_opened_ports = current_ports(local_blacklisted_ports + remote_opened_ports)
76
+
77
+ local_ports_to_close = local_opened_ports_was - local_opened_ports
78
+ local_ports_to_open = local_opened_ports - local_opened_ports_was
79
+
80
+ # puts 'local_ports_to_open' + local_ports_to_open.inspect
81
+ local_ports_to_open.each { |port| SshHelper.forward_local_port(host, port) }
82
+ # puts 'local_ports_to_close' + local_ports_to_close.inspect
83
+ local_ports_to_close.each { |port| SshHelper.close_local_port(host, port) }
84
+
85
+ sleep(1)
86
+
87
+ work_loop(
88
+ host,
89
+ pid_folder,
90
+ remote_blacklisted_ports,
91
+ remote_opened_ports,
92
+ local_blacklisted_ports,
93
+ local_opened_ports
94
+ ) unless Dir["#{pid_folder}/*"].empty?
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RemoteSh
4
+ module RsyncHelper
5
+ module_function
6
+
7
+ # def create_filters(ignore_paths)
8
+ # included_files_arg = ""
9
+ # excluded_files_arg = ""
10
+ #
11
+ # ignore_paths.each do |path|
12
+ # expanded_path = "#{WorkspaceConfiguration::WOKRING_DIR}/#{path}"
13
+ #
14
+ # next unless File.exist?(expanded_path)
15
+ #
16
+ # File.foreach(expanded_path) do |line|
17
+ # line = line.strip
18
+ # next if line.start_with?("#")
19
+ #
20
+ # if line.start_with?("!")
21
+ # included_files_arg += "--include='#{line[1..-1]}' "
22
+ # else
23
+ # excluded_files_arg += "--exclude='#{line}' "
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ # "#{excluded_files_arg} #{included_files_arg} -f'- .remote_sh'"
29
+ # end
30
+
31
+ def up(from, to, host, _ignore_paths)
32
+ # result = `rsync -varz --delete --rsync-path="sudo rsync" #{create_filters(ignore_paths)} #{from}/ #{host}:#{to}/`
33
+ result = `rsync -azl --safe-links --delete --rsync-path="sudo rsync" -f'- .remote_sh' #{from}/ #{host}:#{to}/`
34
+ raise result if $?.exitstatus != 0
35
+ result
36
+ end
37
+
38
+ def down(from, to, host, _ignore_paths)
39
+ # result = `rsync -varz --rsync-path="sudo rsync" #{create_filters(ignore_paths)} #{host}:#{to}/ #{from}/`
40
+ result = `rsync -azl --safe-links --rsync-path="sudo rsync" -f'- .remote_sh' #{host}:#{to}/ #{from}/`
41
+ raise result if $?.exitstatus != 0
42
+ result
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ require 'digest/sha1'
3
+
4
+ module RemoteSh
5
+ module SshHelper
6
+ module_function
7
+
8
+ def attach(host, dir)
9
+ system("ssh -t #{host} \"cd #{dir}; exec \$SHELL -l\"")
10
+ end
11
+
12
+ def current_ports(host, blacklist_ports)
13
+ `ssh #{host} "netstat -tuln | grep LISTEN"`
14
+ .split("\n")
15
+ .map(&:split)
16
+ .map { _1[3].split(':').last }
17
+ .uniq - blacklist_ports
18
+ end
19
+
20
+ def normalize_port(port)
21
+ port = port.to_i
22
+
23
+ if port == 80 || port == 443
24
+ port += 8000
25
+ elsif port <= 1024
26
+ port += 10000
27
+ end
28
+
29
+ port.to_s
30
+ end
31
+
32
+ def forward_local_port(host, port)
33
+ normalized_port = normalize_port(port)
34
+ system("ssh -q -f -N -M -S /tmp/remote_local_pf_#{port} -R #{port}:localhost:#{normalized_port} #{host}")
35
+ end
36
+
37
+ def close_local_port(host, port)
38
+ system("ssh -q -S /tmp/remote_local_pf_#{port} -O exit #{host}")
39
+ end
40
+
41
+ def forward_local_socket(host, socket)
42
+ system("ssh -f -N -M -S /tmp/remote_local_pf_#{Digest::SHA1.hexdigest(socket)} -R #{socket}:#{socket} #{host}")
43
+ end
44
+
45
+ def close_local_socket(host, socket)
46
+ system("ssh -q -S /tmp/remote_local_pf_#{Digest::SHA1.hexdigest(socket)} -O exit #{host}")
47
+ end
48
+
49
+ def forward_port(host, port)
50
+ normalized_port = normalize_port(port)
51
+ # puts "forwarding #{port} to #{normalized_port}"
52
+ system("ssh -f -N -M -S /tmp/remote_pf_#{port} -L #{normalized_port}:localhost:#{port} #{host}")
53
+ end
54
+
55
+ def close_port(host, port)
56
+ # normalized_port = normalize_port(port)
57
+ # puts "closing #{port} forwarded to #{normalized_port}"
58
+ system("ssh -q -S /tmp/remote_pf_#{port} -O exit #{host}")
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RemoteSh
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module RemoteSh
6
+ class WorkspaceConfiguration
7
+ include Singleton
8
+
9
+ WOKRING_DIR = Dir.pwd
10
+ CONFIGURATION_FILE = "#{WOKRING_DIR}/.remote_sh/connection.yaml"
11
+
12
+ # host: name_from_config
13
+ # host_path: path_on_server
14
+ # ignore: ...
15
+
16
+ def self.config
17
+ instance.config
18
+ end
19
+
20
+ def self.host_config
21
+ instance.host_config
22
+ end
23
+
24
+ def self.init!
25
+ current_path = Dir.getwd
26
+
27
+ default_server = HostsConfiguration.config['default_server']
28
+ default_remote_root_path = HostsConfiguration.config['default_remote_root_path']
29
+ default_local_root_path = HostsConfiguration.config['default_local_root_path']
30
+
31
+ Dir.mkdir("#{current_path}/.remote_sh")
32
+
33
+ current_relative_path = current_path.delete_prefix(default_local_root_path)
34
+
35
+ remote_path = "#{default_remote_root_path}#{current_relative_path}"
36
+ host = HostsConfiguration.config['servers'].find { |s| s['name'] == default_server }['host']
37
+
38
+ `ssh #{host} "mkdir -p #{remote_path}"`
39
+
40
+ File.open("#{Dir.getwd}/.remote_sh/connection.yaml", 'w') do |file|
41
+ file.write(<<~YAML)
42
+ host: main
43
+ host_path: #{remote_path}
44
+ ignore:
45
+ - .gitignore
46
+ YAML
47
+ end
48
+ end
49
+
50
+ def parse
51
+ @config = YAML.load_file(CONFIGURATION_FILE)
52
+ end
53
+
54
+ def config
55
+ @config || parse
56
+ end
57
+
58
+ def host_config
59
+ @host_config ||=
60
+ HostsConfiguration.config["servers"].find { _1["name"] == config["host"] }
61
+ end
62
+ end
63
+ end
data/lib/remote_sh.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ require "singleton"
5
+
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.setup
8
+
9
+ module RemoteSh
10
+ PID_FOLDER = "/tmp/remote_sh"
11
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remote_sh
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pavel Egorov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-12-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: filewatcher
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: zeitwerk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webrick
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: CLI for remote development
70
+ email:
71
+ - moonmeander47@ya.ru
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - README.md
77
+ - lib/remote_sh.rb
78
+ - lib/remote_sh/cli.rb
79
+ - lib/remote_sh/file_syncer.rb
80
+ - lib/remote_sh/hosts_configuration.rb
81
+ - lib/remote_sh/port_forwarder.rb
82
+ - lib/remote_sh/rsync_helper.rb
83
+ - lib/remote_sh/ssh_helper.rb
84
+ - lib/remote_sh/version.rb
85
+ - lib/remote_sh/workspace_configuration.rb
86
+ homepage: https://github.com/emfy0/remote_sh
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ homepage_uri: https://github.com/emfy0/remote_sh
91
+ source_code_uri: https://github.com/emfy0/remote_sh
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 3.0.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.5.22
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: CLI for remote development
111
+ test_files: []