devbox_launcher 0.2.0 → 0.3.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb15ad0ea9b993eb803001449bff9970a345d12f872ae973a551e5899b3f3401
4
- data.tar.gz: c4586ec0d2658d7eba82d88550afc849342a5675d7dde501ff1cdf9aae838119
3
+ metadata.gz: 5d79623bafc3e14c6b9f9f58a8031282a334d8ea833e188546d1bffa4ffc1c10
4
+ data.tar.gz: 350a59f18358ba1c473aed63dd033b3699b658e69a02d63bd7d29306a57bd695
5
5
  SHA512:
6
- metadata.gz: 0f244644671795da723b0eca23a9546fceabe1c98897362767070b2582c95b58fa94ad27ea57161cd0c2dbfd2a76518c8c0c5f693a724e732289c7a6dfbb8ee8
7
- data.tar.gz: 3f49c7aff85f9a3500e9da5139cfddd178734a9413cd5f0e03cb3d95b6b684864b3983a8242de5041ac744b34b490cb605902a1c7256bed3c9629f8d28ed6a6e
6
+ metadata.gz: d25e43064171246a712be3a2ab71d2b30d5a3a58891bdeb84538293004147ed6a2b5f7bea5054e66cf433b5a47c0bce154a9aa2649e863e282c5a4a9d95a89dd
7
+ data.tar.gz: 59102e2931d6327865ddc9bb87706dd31bffd703b618a7c9b05735062ac4c0cf69852b9b70be89223f99ee8f82858ceeb6091f7d784b3f43339402f9b070731f
data/.gitignore CHANGED
File without changes
data/.rspec CHANGED
File without changes
File without changes
@@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased]
7
+ ## [0.3.4]
8
8
  ### Added
9
- - Initial release
9
+ - Rescue from `Errno::ECONNREFUSED` and retry
10
+
11
+ ## [0.3.3]
12
+ ### Fixed
13
+ - Support launching multiple boxes at the same time
14
+
15
+ ## [0.3.2]
16
+ ### Fixed
17
+ - Fix: add missing file
18
+
19
+ ## [0.3.1]
20
+ ### Fixed
21
+ - Recover when the devbox is in a shutdown cycle
22
+
23
+ ## [0.3.0]
24
+ ### Changed
25
+ - Label sessions with devbox
26
+ - On linux, mutagen relies on watchman to detect changes
27
+
28
+ ### Fixed
29
+ - In Linux, mutagen no longer burns CPU every 10 seconds
30
+
31
+ ## [0.2.0]
32
+ ### Added
33
+ - Initial release
File without changes
data/Gemfile CHANGED
File without changes
@@ -1,35 +1,37 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- devbox_launcher (0.2.0)
4
+ devbox_launcher (0.3.4)
5
5
  activesupport (~> 6.0)
6
6
  bcrypt_pbkdf (~> 1.0)
7
7
  ed25519 (~> 1.2)
8
8
  net-ssh (~> 5.2)
9
+ os (~> 1.0)
10
+ ruby-watchman (= 0.0.2)
9
11
  ssh-config (= 0.1.3)
10
12
  thor (~> 1.0)
11
13
 
12
14
  GEM
13
15
  remote: https://rubygems.org/
14
16
  specs:
15
- activesupport (6.0.2.1)
17
+ activesupport (6.0.3.1)
16
18
  concurrent-ruby (~> 1.0, >= 1.0.2)
17
19
  i18n (>= 0.7, < 2)
18
20
  minitest (~> 5.1)
19
21
  tzinfo (~> 1.1)
20
- zeitwerk (~> 2.2)
22
+ zeitwerk (~> 2.2, >= 2.2.2)
21
23
  bcrypt_pbkdf (1.0.1)
22
24
  byebug (11.0.1)
23
25
  coderay (1.1.2)
24
- concurrent-ruby (1.1.5)
25
- deep_fetch (0.0.5)
26
+ concurrent-ruby (1.1.6)
26
27
  diff-lcs (1.3)
27
28
  ed25519 (1.2.4)
28
29
  i18n (1.8.2)
29
30
  concurrent-ruby (~> 1.0)
30
31
  method_source (0.9.2)
31
- minitest (5.14.0)
32
+ minitest (5.14.1)
32
33
  net-ssh (5.2.0)
34
+ os (1.1.0)
33
35
  pry (0.12.2)
34
36
  coderay (~> 1.1.0)
35
37
  method_source (~> 0.9.0)
@@ -50,12 +52,13 @@ GEM
50
52
  diff-lcs (>= 1.2.0, < 2.0)
51
53
  rspec-support (~> 3.9.0)
52
54
  rspec-support (3.9.0)
55
+ ruby-watchman (0.0.2)
53
56
  ssh-config (0.1.3)
54
57
  thor (1.0.1)
55
58
  thread_safe (0.3.6)
56
- tzinfo (1.2.6)
59
+ tzinfo (1.2.7)
57
60
  thread_safe (~> 0.1)
58
- zeitwerk (2.2.2)
61
+ zeitwerk (2.3.0)
59
62
 
60
63
  PLATFORMS
61
64
  ruby
@@ -68,4 +71,4 @@ DEPENDENCIES
68
71
  rspec (~> 3.0)
69
72
 
70
73
  BUNDLED WITH
71
- 2.0.2
74
+ 2.1.4
File without changes
data/README.md CHANGED
@@ -38,6 +38,8 @@ devbox start your-username
38
38
 
39
39
  If you want to mosh in immediately, add the `--mosh` switch.
40
40
 
41
+ Note: Linux users that sync mutagen sessions need to install [Watchman](https://facebook.github.io/watchman/).
42
+
41
43
  ## Development
42
44
 
43
45
  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.
data/Rakefile CHANGED
File without changes
@@ -39,4 +39,6 @@ Gem::Specification.new do |spec|
39
39
  spec.add_runtime_dependency "bcrypt_pbkdf", "~> 1.0"
40
40
  spec.add_runtime_dependency "ssh-config", "0.1.3"
41
41
  spec.add_runtime_dependency "activesupport", "~> 6.0"
42
+ spec.add_runtime_dependency "os", "~> 1.0"
43
+ spec.add_runtime_dependency "ruby-watchman", "0.0.2"
42
44
  end
@@ -3,6 +3,10 @@ require "ssh-config"
3
3
  require "open3"
4
4
  require "thor"
5
5
  require "net/ssh"
6
+ require "os"
7
+ require "ruby-watchman"
8
+ require 'socket'
9
+ require 'pathname'
6
10
  require "yaml"
7
11
  require "devbox_launcher/version"
8
12
 
@@ -11,3 +15,7 @@ module DevboxLauncher
11
15
  end
12
16
 
13
17
  require "devbox_launcher/cli"
18
+ require "devbox_launcher/watchman"
19
+ require "devbox_launcher/models/description"
20
+ require "devbox_launcher/models/mutagen"
21
+ require "devbox_launcher/models/box"
@@ -0,0 +1,206 @@
1
+ module DevboxLauncher
2
+ class Box
3
+
4
+ WAIT_BOOT_RESCUED_EXCEPTIONS = [
5
+ Net::SSH::ConnectionTimeout,
6
+ Net::SSH::Disconnect,
7
+ Errno::ECONNRESET,
8
+ Errno::ETIMEDOUT,
9
+ Errno::ECONNREFUSED,
10
+ ]
11
+ WAIT_BOOT_IN_SECONDS = 10.freeze
12
+ DEFAULT_IDENTIFY_FILE_PATH = "~/.ssh/google_compute_engine".freeze
13
+ SSH_CONFIG_PATH = File.expand_path("~/.ssh/config").freeze
14
+ CONFIG_PATH = File.expand_path("~/.devbox_launcher.yml").freeze
15
+ CONFIG = YAML.load_file(CONFIG_PATH).freeze
16
+
17
+ attr_reader :account
18
+
19
+ def initialize(account)
20
+ @account = account
21
+ end
22
+
23
+ def start
24
+ start_stdout, start_stderr, start_status =
25
+ Open3.capture3(start_cmd)
26
+
27
+ set_ssh_config!(hostname, {
28
+ username: username,
29
+ ip: description.ip,
30
+ })
31
+
32
+ wait_boot
33
+
34
+ reset_mutagen_session(
35
+ mutagen_config: config[:mutagen],
36
+ hostname: hostname,
37
+ )
38
+ end
39
+
40
+ def start_cmd
41
+ args = {
42
+ project: config[:project],
43
+ account: account,
44
+ }.map do |(key, val)|
45
+ ["--#{key}", val].join("=")
46
+ end.join(" ")
47
+
48
+ [
49
+ "gcloud",
50
+ "compute",
51
+ "instances",
52
+ "start",
53
+ name,
54
+ args
55
+ ].join(" ")
56
+ end
57
+
58
+ def wait_boot(tries: 1)
59
+ Net::SSH.start(hostname, username, timeout: WAIT_BOOT_IN_SECONDS) do |ssh|
60
+ puts "[#{ssh.exec!('date').chomp}] Machine booted"
61
+ end
62
+ rescue *WAIT_BOOT_RESCUED_EXCEPTIONS
63
+ puts "Not booted. Waiting #{WAIT_BOOT_IN_SECONDS} seconds before trying again..."
64
+
65
+ sleep WAIT_BOOT_IN_SECONDS
66
+
67
+ description = describe(name)
68
+ if !description.running?
69
+ puts "Detected that the machine is not running " \
70
+ "(status is #{description.status}). Booting it..."
71
+ start_box name, username
72
+ end
73
+
74
+ wait_boot name, username, tries: tries+1
75
+ end
76
+
77
+ def description(reload: false)
78
+ return @description if !reload && @description
79
+
80
+ puts "Fetching box's description..."
81
+
82
+ describe_command = %Q(gcloud compute instances describe #{name})
83
+ describe_stdout, describe_stderr, describe_status =
84
+ Open3.capture3(describe_command)
85
+
86
+ if !describe_status.success?
87
+ msg = "Problem fetching the description of #{name}. "
88
+ msg += "Please ensure you can call `#{describe_command}`.\n"
89
+ msg += "Error:\n"
90
+ msg += describe_stderr
91
+ fail msg
92
+ end
93
+
94
+ @description = Description.new(describe_stdout)
95
+ end
96
+
97
+ def set_ssh_config!(hostname, username:, ip:)
98
+ FileUtils.touch(SSH_CONFIG_PATH)
99
+ config = ConfigFile.new
100
+ args = {
101
+ "HostName" => ip,
102
+ "User" => username,
103
+ "IdentityFile" => DEFAULT_IDENTIFY_FILE_PATH,
104
+ }
105
+ args.each do |key, value|
106
+ config.set(hostname, key, value)
107
+ end
108
+ config.save
109
+ end
110
+
111
+ def reset_mutagen_session
112
+ mutagen_config = config[:mutagen]
113
+ return if mutagen_config.nil?
114
+
115
+ alpha_dir = mutagen_config[:alpha]
116
+ beta_dir = mutagen_config[:beta]
117
+
118
+ return if alpha_dir.nil? || beta_dir.nil?
119
+
120
+ terminate_mutagen_session
121
+
122
+ create_mutagen_session(
123
+ alpha_dir: alpha_dir,
124
+ beta_dir: beta_dir,
125
+ hostname: hostname,
126
+ username: username,
127
+ )
128
+
129
+ if OS.linux?
130
+ watch_alpha(alpha_dir: alpha_dir, hostname: hostname)
131
+ end
132
+ end
133
+
134
+ def terminate_mutagen_session(username)
135
+ puts "Terminating mutagen session..."
136
+ terminate_mutagen_command =
137
+ %Q(mutagen terminate --label-selector=#{username})
138
+ terminate_mutagen_stdout,
139
+ terminate_mutagen_stderr,
140
+ terminate_mutagen_status =
141
+ Open3.capture3(terminate_mutagen_command)
142
+
143
+ if not terminate_mutagen_status.success?
144
+ # mutagen prints to stdout and stderr
145
+ msg = "Failed to terminate mutagen sessions: " \
146
+ "#{terminate_mutagen_stdout} -" \
147
+ "#{terminate_mutagen_stderr}"
148
+ fail msg
149
+ end
150
+ end
151
+
152
+ def create_mutagen_session(alpha_dir:, beta_dir:, hostname:, username:)
153
+ puts "Create mutagen session syncing local #{alpha_dir} " \
154
+ "with #{hostname} #{beta_dir}"
155
+
156
+ create_mutagen_command = [
157
+ "mutagen sync create",
158
+ alpha_dir,
159
+ "#{hostname}:#{beta_dir}",
160
+ "--label=#{username}",
161
+ ]
162
+ create_mutagen_command << "--watch-mode-alpha=no-watch" if OS.linux?
163
+
164
+ create_mutagen_stdout,
165
+ create_mutagen_stderr,
166
+ create_mutagen_status =
167
+ Open3.capture3(create_mutagen_command.join(" "))
168
+
169
+ if not create_mutagen_status.success?
170
+ # mutagen prints to stdout and stderr
171
+ msg = "Failed to create mutagen sessions: " \
172
+ "#{create_mutagen_stdout} -" \
173
+ "#{create_mutagen_stderr}"
174
+ fail msg
175
+ end
176
+ end
177
+
178
+ def watch_alpha(alpha_dir:, hostname:)
179
+ watchman = Watchman.new(dir: alpha_dir)
180
+ watchman.trigger("mutagen sync flush --label-selector=#{hostname}")
181
+ end
182
+
183
+ def name
184
+ @name ||= config[:box]
185
+ end
186
+
187
+ def hostname
188
+ [name, username, "devbox"].join("-")
189
+ end
190
+
191
+ def username
192
+ @username ||= account.gsub(/\W/, "_")
193
+ end
194
+
195
+ def config
196
+ return @config if @config
197
+
198
+ if not CONFIG.has_key?(account)
199
+ fail "No config in #{CONFIG_PATH} found for #{account}"
200
+ end
201
+
202
+ @config = CONFIG[account].with_indifferent_access
203
+ end
204
+
205
+ end
206
+ end
@@ -11,133 +11,7 @@ module DevboxLauncher
11
11
  option :mosh, type: :boolean, desc: "Mosh in"
12
12
 
13
13
  def start(account)
14
- if not CONFIG.has_key?(account)
15
- fail "No config in #{CONFIG_PATH} found for #{account}"
16
- end
17
-
18
- config = CONFIG[account].with_indifferent_access
19
-
20
- username = account.gsub(/\W/, "_")
21
-
22
- set_account_command = %Q(gcloud config set account #{account})
23
- set_account_stdout, set_account_stderr, set_account_status =
24
- Open3.capture3(set_account_command)
25
-
26
- set_project_command = %Q(gcloud config set project #{config[:project]})
27
- set_project_stdout, set_project_stderr, set_project_status =
28
- Open3.capture3(set_project_command)
29
-
30
- name = config[:box]
31
-
32
- start_command = %Q(gcloud compute instances start #{name})
33
- start_stdout, start_stderr, start_status = Open3.capture3(start_command)
34
-
35
- puts "Fetching IP..."
36
- describe_command = %Q(gcloud compute instances describe #{name})
37
- describe_stdout, describe_stderr, describe_status =
38
- Open3.capture3(describe_command)
39
-
40
- if !describe_status.success?
41
- msg = "Problem fetching the IP address. "
42
- msg += "Please ensure you can call `#{describe_command}`.\n"
43
- msg += "Error:\n"
44
- msg += describe_stderr
45
- fail msg
46
- end
47
-
48
- description = YAML.load(describe_stdout)
49
-
50
- ip = description["networkInterfaces"].first["accessConfigs"].
51
- find { |config| config["kind"] == "compute#accessConfig" }["natIP"]
52
-
53
- puts "IP: #{ip}"
54
-
55
- hostname = "#{name}-devbox"
56
-
57
- set_ssh_config!(hostname, {
58
- username: username,
59
- ip: ip,
60
- })
61
-
62
- wait_boot(hostname, username)
63
-
64
- reset_mutagen_session(
65
- mutagen_config: config[:mutagen],
66
- hostname: hostname,
67
- )
68
-
69
- if options[:mosh]
70
- mosh_command = %Q(mosh #{hostname})
71
- system(mosh_command)
72
- end
73
- end
74
-
75
- no_commands do
76
- def wait_boot(hostname, username)
77
- Net::SSH.start(hostname, username, timeout: WAIT_BOOT_IN_SECONDS) do |ssh|
78
- puts "[#{ssh.exec!('date').chomp}] Machine booted"
79
- end
80
- rescue Net::SSH::ConnectionTimeout, Net::SSH::Disconnect, Errno::ECONNRESET
81
- puts "Not booted. Waiting #{WAIT_BOOT_IN_SECONDS} seconds before trying again..."
82
- wait_boot hostname, username
83
- end
84
-
85
- def set_ssh_config!(hostname, username:, ip:)
86
- FileUtils.touch(SSH_CONFIG_PATH)
87
- config = ConfigFile.new
88
- args = {
89
- "HostName" => ip,
90
- "User" => username,
91
- "IdentityFile" => DEFAULT_IDENTIFY_FILE_PATH,
92
- }
93
- args.each do |key, value|
94
- config.set(hostname, key, value)
95
- end
96
- config.save
97
- end
98
-
99
- def reset_mutagen_session(mutagen_config:, hostname:)
100
- return if mutagen_config.nil?
101
- alpha_dir = mutagen_config[:alpha]
102
- beta_dir = mutagen_config[:beta]
103
-
104
- return if alpha_dir.nil? || beta_dir.nil?
105
-
106
- puts "Terminating all mutagen sessions..."
107
- terminate_mutagen_command = %Q(mutagen terminate --all)
108
- terminate_mutagen_stdout,
109
- terminate_mutagen_stderr,
110
- terminate_mutagen_status =
111
- Open3.capture3(terminate_mutagen_command)
112
-
113
- if not terminate_mutagen_status.success?
114
- # mutagen prints to stdout and stderr
115
- msg = "Failed to terminate mutagen sessions: " \
116
- "#{terminate_mutagen_stdout} -" \
117
- "#{terminate_mutagen_stderr}"
118
- fail msg
119
- end
120
-
121
- puts "Create mutagen session syncing local #{alpha_dir} " \
122
- "with #{hostname} #{beta_dir}"
123
- create_mutagen_command = [
124
- "mutagen sync create",
125
- alpha_dir,
126
- "#{hostname}:#{beta_dir}",
127
- ].join(" ")
128
- create_mutagen_stdout,
129
- create_mutagen_stderr,
130
- create_mutagen_status =
131
- Open3.capture3(create_mutagen_command)
132
-
133
- if not create_mutagen_status.success?
134
- # mutagen prints to stdout and stderr
135
- msg = "Failed to create mutagen sessions: " \
136
- "#{create_mutagen_stdout} -" \
137
- "#{create_mutagen_stderr}"
138
- fail msg
139
- end
140
- end
14
+ Box.new(account, options).start
141
15
  end
142
16
 
143
17
  end
@@ -0,0 +1,222 @@
1
+ module DevboxLauncher
2
+ class Box
3
+
4
+ WAIT_BOOT_RESCUED_EXCEPTIONS = [
5
+ Net::SSH::ConnectionTimeout,
6
+ Net::SSH::Disconnect,
7
+ Errno::ECONNRESET,
8
+ Errno::ETIMEDOUT,
9
+ ]
10
+ WAIT_BOOT_IN_SECONDS = 10.freeze
11
+ MAX_BOOT_RETRIES = 10
12
+ DEFAULT_IDENTIFY_FILE_PATH = "~/.ssh/google_compute_engine".freeze
13
+ SSH_CONFIG_PATH = File.expand_path("~/.ssh/config").freeze
14
+ CONFIG_PATH = File.expand_path("~/.devbox_launcher.yml").freeze
15
+ CONFIG = YAML.load_file(CONFIG_PATH).freeze
16
+
17
+ attr_reader :account, :options
18
+
19
+ def initialize(account, options)
20
+ @account = account
21
+ @options = options
22
+ end
23
+
24
+ def start
25
+ start_stdout, start_stderr, start_status =
26
+ Open3.capture3(start_cmd)
27
+
28
+ set_ssh_config!
29
+
30
+ wait_boot
31
+
32
+ reset_mutagen_session
33
+
34
+ connect_mosh
35
+ end
36
+
37
+ def start_cmd
38
+ args = {
39
+ project: config[:project],
40
+ account: account,
41
+ }.map do |(key, val)|
42
+ ["--#{key}", val].join("=")
43
+ end.join(" ")
44
+
45
+ [
46
+ "gcloud",
47
+ "compute",
48
+ "instances",
49
+ "start",
50
+ name,
51
+ args
52
+ ].join(" ")
53
+ end
54
+
55
+ def connect_mosh
56
+ return if options[:mosh].nil?
57
+
58
+ mosh_cmd = %Q(mosh #{hostname})
59
+ system(mosh_cmd)
60
+ end
61
+
62
+ def wait_boot(tries: 1)
63
+ Net::SSH.start(hostname, username, timeout: WAIT_BOOT_IN_SECONDS) do |ssh|
64
+ puts "[#{ssh.exec!('date').chomp}] Machine booted"
65
+ end
66
+ rescue *WAIT_BOOT_RESCUED_EXCEPTIONS
67
+ puts "Not booted. Waiting #{WAIT_BOOT_IN_SECONDS} seconds before trying again..."
68
+
69
+ sleep WAIT_BOOT_IN_SECONDS
70
+
71
+ if !description(reload: true).running?
72
+ puts "Detected that the machine is not running " \
73
+ "(status is #{description.status}). Booting it..."
74
+ start
75
+ end
76
+
77
+ fail if tries >= MAX_BOOT_RETRIES
78
+
79
+ wait_boot tries: tries+1
80
+ end
81
+
82
+ def description(reload: false)
83
+ return @description if !reload && @description
84
+
85
+ puts "Fetching box's description..."
86
+
87
+ describe_stdout, describe_stderr, describe_status =
88
+ Open3.capture3(describe_cmd)
89
+
90
+ if !describe_status.success?
91
+ msg = "Problem fetching the description of #{name}. "
92
+ msg += "Please ensure you can call `#{describe_cmd}`.\n"
93
+ msg += "Error:\n"
94
+ msg += describe_stderr
95
+ fail msg
96
+ end
97
+
98
+ @description = Description.new(describe_stdout)
99
+ end
100
+
101
+ def describe_cmd
102
+ args = {
103
+ project: config[:project],
104
+ account: account,
105
+ }.map do |(key, val)|
106
+ ["--#{key}", val].join("=")
107
+ end.join(" ")
108
+
109
+ [
110
+ "gcloud",
111
+ "compute",
112
+ "instances",
113
+ "describe",
114
+ name,
115
+ args
116
+ ].join(" ")
117
+ end
118
+
119
+ def set_ssh_config!
120
+ FileUtils.touch(SSH_CONFIG_PATH)
121
+ config = ConfigFile.new
122
+ args = {
123
+ "HostName" => description.ip,
124
+ "User" => username,
125
+ "IdentityFile" => DEFAULT_IDENTIFY_FILE_PATH,
126
+ }
127
+ args.each do |key, value|
128
+ config.set(hostname, key, value)
129
+ end
130
+ config.save
131
+ end
132
+
133
+ def reset_mutagen_session
134
+ return if !mutagen_config.configured?
135
+
136
+ terminate_mutagen_session
137
+ create_mutagen_session
138
+ watch_alpha if OS.linux?
139
+ end
140
+
141
+ def terminate_mutagen_session
142
+ puts "Terminating mutagen session..."
143
+ terminate_mutagen_cmd =
144
+ %Q(mutagen terminate --label-selector=#{label})
145
+ terminate_mutagen_stdout,
146
+ terminate_mutagen_stderr,
147
+ terminate_mutagen_status =
148
+ Open3.capture3(terminate_mutagen_cmd)
149
+
150
+ if not terminate_mutagen_status.success?
151
+ # mutagen prints to stdout and stderr
152
+ msg = "Failed to terminate mutagen sessions: " \
153
+ "#{terminate_mutagen_stdout} -" \
154
+ "#{terminate_mutagen_stderr}"
155
+ fail msg
156
+ end
157
+ end
158
+
159
+ def label
160
+ "#{username}=#{name}"
161
+ end
162
+
163
+ def create_mutagen_session
164
+ puts "Create mutagen session syncing local " \
165
+ "#{mutagen_config.alpha_dir} with " \
166
+ "#{hostname} #{mutagen_config.beta_dir}"
167
+
168
+ create_mutagen_cmd = [
169
+ "mutagen sync create",
170
+ mutagen_config.alpha_dir,
171
+ "#{hostname}:#{mutagen_config.beta_dir}",
172
+ "--label=#{label}",
173
+ ]
174
+ create_mutagen_cmd << "--watch-mode-alpha=no-watch" if OS.linux?
175
+
176
+ create_mutagen_stdout,
177
+ create_mutagen_stderr,
178
+ create_mutagen_status =
179
+ Open3.capture3(create_mutagen_cmd.join(" "))
180
+
181
+ if not create_mutagen_status.success?
182
+ # mutagen prints to stdout and stderr
183
+ msg = "Failed to create mutagen sessions: " \
184
+ "#{create_mutagen_stdout} -" \
185
+ "#{create_mutagen_stderr}"
186
+ fail msg
187
+ end
188
+ end
189
+
190
+ def watch_alpha
191
+ watchman = Watchman.new(dir: mutagen_config.alpha_dir)
192
+ watchman.trigger("mutagen sync flush --label-selector=#{label}")
193
+ end
194
+
195
+ def name
196
+ @name ||= config[:box]
197
+ end
198
+
199
+ def hostname
200
+ [name, username, "devbox"].join("-")
201
+ end
202
+
203
+ def username
204
+ @username ||= account.gsub(/\W/, "_")
205
+ end
206
+
207
+ def config
208
+ return @config if @config
209
+
210
+ if not CONFIG.has_key?(account)
211
+ fail "No config in #{CONFIG_PATH} found for #{account}"
212
+ end
213
+
214
+ @config = CONFIG[account].with_indifferent_access
215
+ end
216
+
217
+ def mutagen_config
218
+ @mutagen_config ||= Mutagen.new(config[:mutagen])
219
+ end
220
+
221
+ end
222
+ end
@@ -0,0 +1,32 @@
1
+ module DevboxLauncher
2
+ class Description
3
+
4
+ def initialize(yaml)
5
+ @desc = YAML.load(yaml)
6
+ end
7
+
8
+ def ip
9
+ return @ip if @ip
10
+ network_interface = network_interfaces.first
11
+ access_configs = network_interface["accessConfigs"]
12
+
13
+ access_config = access_configs.find do |c|
14
+ c["kind"] == "compute#accessConfig"
15
+ end
16
+ @ip = access_config["natIP"]
17
+ end
18
+
19
+ def status
20
+ @status ||= @desc["status"]
21
+ end
22
+
23
+ def network_interfaces
24
+ @network_interfaces ||= @desc["networkInterfaces"]
25
+ end
26
+
27
+ def running?
28
+ status == "RUNNING"
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ module DevboxLauncher
2
+ class Mutagen
3
+
4
+ attr_reader :config
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def configured?
11
+ [config, alpha_dir, beta_dir].none?(&:nil?)
12
+ end
13
+
14
+ def alpha_dir
15
+ config[:alpha]
16
+ end
17
+
18
+ def beta_dir
19
+ config[:beta]
20
+ end
21
+
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module DevboxLauncher
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.4"
3
3
  end
@@ -0,0 +1,50 @@
1
+ module DevboxLauncher
2
+ class Watchman
3
+
4
+ attr_reader :dir
5
+
6
+ def initialize(dir:)
7
+ @dir = dir
8
+ end
9
+
10
+ def trigger(command)
11
+ UNIXSocket.open(sockname) do |socket|
12
+ root = Pathname.new(dir).expand_path.to_s
13
+ result = RubyWatchman.query(['watch-list'], socket)
14
+ roots = result['roots']
15
+ if !roots.include?(root)
16
+ # this path isn't being watched yet; try to set up watch
17
+ result = RubyWatchman.query(['watch-project', root], socket)
18
+
19
+ # root_restrict_files setting may prevent Watchman from working
20
+ raise "Unable to watch #{dir}" if result.has_key?('error')
21
+ end
22
+
23
+ query = ['trigger', root, {
24
+ 'name' => 'mutagen-sync',
25
+ 'expression' => ['match', '**/*', 'wholename'],
26
+ 'command' => command.split(" "),
27
+ }]
28
+ paths = RubyWatchman.query(query, socket)
29
+
30
+ # could return error if watch is removed
31
+ if paths.has_key?('error')
32
+ raise "Unable to set trigger. Error: #{paths['error']}"
33
+ end
34
+ end
35
+ end
36
+
37
+ def sockname
38
+ sockname = RubyWatchman.load(
39
+ %x{watchman --output-encoding=bser get-sockname}
40
+ )['sockname']
41
+
42
+ if !$?.exitstatus.zero?
43
+ raise "Failed to connect to watchman. Is it running?"
44
+ end
45
+
46
+ sockname
47
+ end
48
+
49
+ end
50
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devbox_launcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ramon Tayag
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-19 00:00:00.000000000 Z
11
+ date: 2020-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -136,7 +136,35 @@ dependencies:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
138
  version: '6.0'
139
- description:
139
+ - !ruby/object:Gem::Dependency
140
+ name: os
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '1.0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '1.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: ruby-watchman
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - '='
158
+ - !ruby/object:Gem::Version
159
+ version: 0.0.2
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - '='
165
+ - !ruby/object:Gem::Version
166
+ version: 0.0.2
167
+ description:
140
168
  email:
141
169
  - ramon.tayag@gmail.com
142
170
  executables:
@@ -161,8 +189,13 @@ files:
161
189
  - bin/setup
162
190
  - devbox_launcher.gemspec
163
191
  - lib/devbox_launcher.rb
192
+ - lib/devbox_launcher/box.rb
164
193
  - lib/devbox_launcher/cli.rb
194
+ - lib/devbox_launcher/models/box.rb
195
+ - lib/devbox_launcher/models/description.rb
196
+ - lib/devbox_launcher/models/mutagen.rb
165
197
  - lib/devbox_launcher/version.rb
198
+ - lib/devbox_launcher/watchman.rb
166
199
  homepage: https://github.com/bloom-solutions/devbox_launcher
167
200
  licenses:
168
201
  - MIT
@@ -171,7 +204,7 @@ metadata:
171
204
  homepage_uri: https://github.com/bloom-solutions/devbox_launcher
172
205
  source_code_uri: https://github.com/bloom-solutions/devbox_launcher
173
206
  changelog_uri: https://github.com/bloom-solutions/devbox_launcher/blob/master/CHANGELOG.md
174
- post_install_message:
207
+ post_install_message:
175
208
  rdoc_options: []
176
209
  require_paths:
177
210
  - lib
@@ -186,8 +219,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
219
  - !ruby/object:Gem::Version
187
220
  version: '0'
188
221
  requirements: []
189
- rubygems_version: 3.0.6
190
- signing_key:
222
+ rubygems_version: 3.0.8
223
+ signing_key:
191
224
  specification_version: 4
192
225
  summary: Conveniently launch your devbox
193
226
  test_files: []