1
1
# frozen_string_literal: true
2
2
3
+ require "log4r"
3
4
require "pathname"
4
5
require "vagrant/util/busy"
5
6
require "vagrant/util/subprocess"
7
+ require "vagrant/util/which"
6
8
require_relative "../model/list_result"
7
9
8
10
module VagrantPlugins
@@ -15,10 +17,19 @@ class Base
15
17
include Vagrant ::Util ::Retryable
16
18
17
19
def initialize
20
+ @logger = Log4r ::Logger . new ( "vagrant::provider::utm::base" )
21
+
18
22
# This flag is used to keep track of interrupted state (SIGINT)
19
23
@interrupted = false
20
24
# The path to the scripts directory.
21
25
@script_path = Pathname . new ( File . expand_path ( "../scripts" , __dir__ ) )
26
+
27
+ # Set 'utmctl' path
28
+ @utmctl_path = Vagrant ::Util ::Which . which ( "utmctl" )
29
+
30
+ # if not found, fall back to /usr/local/bin/utmctl
31
+ @utmctl_path ||= "/Applications/UTM.app/Contents/MacOS/utmctl"
32
+ @logger . info ( "utmctl path: #{ @utmctl_path } " )
22
33
end
23
34
24
35
# Check if the VM with the given UUID (Name) exists.
@@ -94,6 +105,130 @@ def execute(*cmd, &block)
94
105
# Return the outputs of the command
95
106
"#{ result . stdout } #{ result . stderr } "
96
107
end
108
+
109
+ # Execute the given subcommand for utmctl and return the output.
110
+ # Heavily inspired from https://github.com/hashicorp/vagrant/blob/main/plugins/providers/virtualbox/driver/base.rb.
111
+ # @param [String] subcommand The subcommand to execute.
112
+ # @return [String] The output of the command.
113
+ def utmctl_execute ( *command , &block )
114
+ # Get the options hash if it exists
115
+ opts = { }
116
+ opts = command . pop if command . last . is_a? ( Hash )
117
+
118
+ tries = 0
119
+ tries = 3 if opts [ :retryable ]
120
+
121
+ # Variable to store our execution result
122
+ r = nil
123
+
124
+ retryable ( on : Errors ::UtmctlError , tries : tries , sleep : 1 ) do
125
+ # if there is an error with utmctl, this gets set to true
126
+ errored = false
127
+
128
+ # Execute the command
129
+ r = raw ( *command , &block )
130
+
131
+ # If the command was a failure, then raise an exception that is
132
+ # nicely handled by Vagrant.
133
+ if r . exit_code != 0
134
+ if @interrupted
135
+ @logger . info ( "Exit code != 0, but interrupted. Ignoring." )
136
+ elsif r . exit_code == 126
137
+ # To be consistent with VBoxManage
138
+ raise Errors ::UtmctlNotFoundError
139
+ else
140
+ errored = true
141
+ end
142
+ elsif r . stdout . include? ( "Error:" )
143
+ # if utmctl fails but doesn't return a non-zero exit code
144
+ # Handle that here
145
+ errored = true
146
+ end
147
+
148
+ # If there was an error running utmctl, show the error and the output
149
+ return unless errored
150
+
151
+ raise Errors ::UtmctlError ,
152
+ command : command . inspect ,
153
+ stderr : r . stderr ,
154
+ stdout : r . stdout
155
+ end
156
+
157
+ # Return the output, making sure to replace any Windows-style
158
+ # newlines with Unix-style.
159
+ r . stdout . gsub ( "\r \n " , "\n " )
160
+ end
161
+
162
+ # Executes a command and returns the raw result object.
163
+ def raw ( *command , &block )
164
+ int_callback = lambda do
165
+ @interrupted = true
166
+
167
+ # We have to execute this in a thread due to trap contexts
168
+ # and locks.
169
+ Thread . new { @logger . info ( "Interrupted." ) } . join
170
+ end
171
+
172
+ # Append in the options for subprocess
173
+ # NOTE: We include the LANG env var set to C to prevent command output
174
+ # from being localized
175
+ command << { notify : %i[ stdout stderr ] , env : env_lang }
176
+
177
+ Vagrant ::Util ::Busy . busy ( int_callback ) do
178
+ Vagrant ::Util ::Subprocess . execute ( @utmctl_path , *command , &block )
179
+ end
180
+ rescue Vagrant ::Util ::Subprocess ::LaunchError => e
181
+ raise Vagrant ::Errors ::UtmctlLaunchError ,
182
+ message : e . to_s
183
+ end
184
+
185
+ private
186
+
187
+ # List of LANG values to attempt to use
188
+ LANG_VARIATIONS = %w[ C.UTF-8 C.utf8 en_US.UTF-8 en_US.utf8 C POSIX ] . map ( &:freeze ) . freeze
189
+
190
+ # By default set the LANG to C. If the host has the locale command
191
+ # available, check installed locales and verify C is included (or
192
+ # use C variant if available).
193
+ def env_lang
194
+ # If already set, just return immediately
195
+ return @env_lang if @env_lang
196
+
197
+ # Default the LANG to C
198
+ @env_lang = { LANG : "C" }
199
+
200
+ # If the locale command is not available, return default
201
+ return @env_lang unless Vagrant ::Util ::Which . which ( "locale" )
202
+
203
+ return @env_lang = @@env_lang if defined? ( @@env_lang )
204
+
205
+ @logger . debug ( "validating LANG value for virtualbox cli commands" )
206
+ # Get list of available locales on the system
207
+ result = Vagrant ::Util ::Subprocess . execute ( "locale" , "-a" )
208
+
209
+ # If the command results in an error, just log the error
210
+ # and return the default value
211
+ if result . exit_code != 0
212
+ @logger . warn ( "locale command failed (exit code: #{ result . exit_code } ): #{ result . stderr } " )
213
+ return @env_lang
214
+ end
215
+ available = result . stdout . lines . map ( &:chomp ) . find_all do |l |
216
+ l == "C" || l == "POSIX" || l . start_with? ( "C." ) || l . start_with? ( "en_US." )
217
+ end
218
+ @logger . debug ( "list of available C locales: #{ available . inspect } " )
219
+
220
+ # Attempt to find a valid LANG from locale list
221
+ lang = LANG_VARIATIONS . detect { |l | available . include? ( l ) }
222
+
223
+ if lang
224
+ @logger . debug ( "valid variation found for LANG value: #{ lang } " )
225
+ @env_lang [ :LANG ] = lang
226
+ @@env_lang = @env_lang
227
+ end
228
+
229
+ @logger . debug ( "LANG value set: #{ @env_lang [ :LANG ] . inspect } " )
230
+ @env_lang
231
+ end
97
232
end
98
233
end
99
234
end
0 commit comments