Class: Aspera::Environment
- Inherits:
-
Object
- Object
- Aspera::Environment
- Includes:
- Singleton
- Defined in:
- lib/aspera/environment.rb
Overview
detect OS, architecture, and specific stuff
Constant Summary collapse
- OS_WINDOWS =
:windows- OS_MACOS =
:osx- OS_LINUX =
:linux- OS_AIX =
:aix- OS_LIST =
[OS_WINDOWS, OS_MACOS, OS_LINUX, OS_AIX].freeze
- CPU_X86_64 =
:x86_64- CPU_ARM64 =
:arm64- CPU_PPC64 =
:ppc64- CPU_PPC64LE =
:ppc64le- CPU_S390 =
:s390- CPU_LIST =
[CPU_X86_64, CPU_ARM64, CPU_PPC64, CPU_PPC64LE, CPU_S390].freeze
- BITS_PER_BYTE =
8- MEBI =
1024 * 1024
- BYTES_PER_MEBIBIT =
MEBI / BITS_PER_BYTE
- I18N_VARS =
%w(LC_ALL LC_CTYPE LANG).freeze
- WINDOWS_FILENAME_INVALID_CHARACTERS =
“/” is invalid on both Unix and Windows, other are Windows special characters See: learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
'<>:"/\\|?*'- REPLACE_CHARACTER =
'_'- RB_EXT =
'.rb'- PROCESS_MODES =
%i[execute background capture].freeze
Instance Attribute Summary collapse
-
#cpu ⇒ Object
readonly
Returns the value of attribute cpu.
-
#default_gui_mode ⇒ Object
readonly
Returns the value of attribute default_gui_mode.
-
#file_illegal_characters ⇒ Object
Returns the value of attribute file_illegal_characters.
-
#os ⇒ Object
readonly
Returns the value of attribute os.
-
#url_method ⇒ Object
Returns the value of attribute url_method.
Class Method Summary collapse
-
.build_spawn_argv(cmd, kwargs) ⇒ Object
Build argv for Process.spawn / Kernel.system (no shell).
-
.empty_binding ⇒ Object
Empty variable binding for secure eval.
-
.force_terminal_c ⇒ Object
force locale to C so that unicode characters are not used.
-
.restrict_file_access(path, mode: nil) ⇒ Object
restrict access to a file or folder to user only.
- .ruby_version ⇒ Object
-
.secure_eval(code, file, line, user_binding = nil) ⇒ Object
Secure execution of Ruby code.
-
.secure_execute(*cmd, mode: :execute, **kwargs) ⇒ Boolean, ...
Executes a command without invoking a shell.
-
.shell_escape_pretty(str) ⇒ Object
like ‘Shellwords.shellescape`, but does not escape `=`.
-
.terminal? ⇒ Boolean
True if we are in a terminal.
- .terminal_supports_unicode? ⇒ Boolean
-
.write_file_restricted(path, force: false, mode: nil) ⇒ Object
Write content to a file, with restricted access.
Instance Method Summary collapse
-
#architecture ⇒ Object
Normalized architecture name See constants: OS_* and CPU_*.
-
#exe_file(name = nil) ⇒ String
Add executable file extension (e.g. “.exe”) for current OS.
-
#fix_home ⇒ Object
on Windows, the env var %USERPROFILE% provides the path to user’s home more reliably than %HOMEDRIVE%%HOMEPATH% so, tell Ruby the right way.
- #graphical? ⇒ Boolean
-
#initialize ⇒ Environment
constructor
A new instance of Environment.
-
#initialize_fields ⇒ Object
initialize fields from environment.
-
#open_editor(file_path) ⇒ Object
open a file in an editor.
-
#open_uri(the_url) ⇒ Object
Allows a user to open a URL if method is :text, then URL is displayed on terminal if method is :graphical, then the URL will be opened with the default browser.
-
#open_uri_graphical(uri) ⇒ Object
Open a URI in a graphical browser Command must be non blocking.
-
#safe_filename_character ⇒ String
Replacement character for illegal filename characters Can also be used as safe “join” character.
-
#sanitized_filename(filename) ⇒ String
Sanitize a filename by replacing illegal characters.
Constructor Details
#initialize ⇒ Environment
Returns a new instance of Environment.
204 205 206 |
# File 'lib/aspera/environment.rb', line 204 def initialize initialize_fields end |
Instance Attribute Details
#cpu ⇒ Object (readonly)
Returns the value of attribute cpu.
202 203 204 |
# File 'lib/aspera/environment.rb', line 202 def cpu @cpu end |
#default_gui_mode ⇒ Object (readonly)
Returns the value of attribute default_gui_mode.
202 203 204 |
# File 'lib/aspera/environment.rb', line 202 def default_gui_mode @default_gui_mode end |
#file_illegal_characters ⇒ Object
Returns the value of attribute file_illegal_characters.
201 202 203 |
# File 'lib/aspera/environment.rb', line 201 def file_illegal_characters @file_illegal_characters end |
#os ⇒ Object (readonly)
Returns the value of attribute os.
202 203 204 |
# File 'lib/aspera/environment.rb', line 202 def os @os end |
#url_method ⇒ Object
Returns the value of attribute url_method.
201 202 203 |
# File 'lib/aspera/environment.rb', line 201 def url_method @url_method end |
Class Method Details
.build_spawn_argv(cmd, kwargs) ⇒ Object
Build argv for Process.spawn / Kernel.system (no shell)
68 69 70 71 72 73 74 75 |
# File 'lib/aspera/environment.rb', line 68 def build_spawn_argv(cmd, kwargs) env = kwargs.delete(:env) argv = [] argv << env if env argv << [cmd.first, cmd.first] # no shell, preserve argv[0] argv.concat(cmd.drop(1)) argv end |
.empty_binding ⇒ Object
Empty variable binding for secure eval
53 54 55 |
# File 'lib/aspera/environment.rb', line 53 def empty_binding return Kernel.binding end |
.force_terminal_c ⇒ Object
force locale to C so that unicode characters are not used
190 191 192 |
# File 'lib/aspera/environment.rb', line 190 def force_terminal_c I18N_VARS.each{ |var| ENV[var] = 'C'} end |
.restrict_file_access(path, mode: nil) ⇒ Object
restrict access to a file or folder to user only
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/aspera/environment.rb', line 168 def restrict_file_access(path, mode: nil) if mode.nil? # or FileUtils ? if File.file?(path) mode = 0o600 elsif File.directory?(path) mode = 0o700 else Log.log.debug{"No restriction can be set for #{path}"} end end File.chmod(mode, path) unless mode.nil? rescue => e Log.log.warn(e.) end |
.ruby_version ⇒ Object
48 49 50 |
# File 'lib/aspera/environment.rb', line 48 def ruby_version return RbConfig::CONFIG['RUBY_PROGRAM_VERSION'] end |
.secure_eval(code, file, line, user_binding = nil) ⇒ Object
Secure execution of Ruby code
61 62 63 |
# File 'lib/aspera/environment.rb', line 61 def secure_eval(code, file, line, user_binding = nil) Kernel.send('lave'.reverse, code, user_binding || empty_binding, file, line) end |
.secure_execute(*cmd, mode: :execute, **kwargs) ⇒ Boolean, ...
Executes a command without invoking a shell.
The command is provided as an array to avoid shell interpolation and ensure safer execution.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/aspera/environment.rb', line 112 def secure_execute(*cmd, mode: :execute, **kwargs) cmd = cmd.map(&:to_s) Aspera.assert(cmd.size.positive?, type: ArgumentError){'executable must be present'} Aspera.assert_values(mode, PROCESS_MODES, type: ArgumentError){'mode'} Log.log.debug do parts = [mode.to_s, 'command:'] kwargs[:env]&.each{ |k, v| parts << "#{k}=#{shell_escape_pretty(v.to_s)}"} cmd.each{ |a| parts << shell_escape_pretty(a)} parts.join(' ') end case mode when :execute # https://docs.ruby-lang.org/en/master/Kernel.html#method-i-system # https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options kwargs[:exception] = true unless kwargs.key?(:exception) Kernel.system(*build_spawn_argv(cmd, kwargs), **kwargs) when :background # https://docs.ruby-lang.org/en/master/Process.html#method-c-spawn # https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options kwargs[:close_others] = true unless kwargs.key?(:close_others) pid = Process.spawn(*build_spawn_argv(cmd, kwargs), **kwargs) Log.dump(:pid, pid) pid when :capture # https://docs.ruby-lang.org/en/master/Open3.html#method-c-capture3 # https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options argv = [kwargs.delete(:env)].compact + cmd exception = kwargs.delete(:exception){true} result = Open3.capture3(*argv, **kwargs) Log.dump(:stdout, result[0], level: :trace1) Log.dump(:stderr, result[1], level: :trace1) Log.dump(:status, result[2]) raise "Process failed: #{result[2].exitstatus} (#{result[1]})" if exception && !result[2].success? result else Aspera.error_unreachable_line end end |
.shell_escape_pretty(str) ⇒ Object
like ‘Shellwords.shellescape`, but does not escape `=`
78 79 80 81 82 83 84 85 |
# File 'lib/aspera/environment.rb', line 78 def shell_escape_pretty(str) # Safe unquoted characters + '=' explicitly allowed return str if str.match?(%r{\A[A-Za-z0-9_.,:/@+=-]+\z}) # return str if Shellwords.shellescape(str) == str # Otherwise use single quotes "'#{str.gsub("'", %q('\'\''))}'" end |
.terminal? ⇒ Boolean
Returns true if we are in a terminal.
185 186 187 |
# File 'lib/aspera/environment.rb', line 185 def terminal? $stdout.tty? end |
.terminal_supports_unicode? ⇒ Boolean
197 198 199 |
# File 'lib/aspera/environment.rb', line 197 def terminal_supports_unicode? terminal? && I18N_VARS.any?{ |var| ENV[var]&.include?('UTF-8')} end |
.write_file_restricted(path, force: false, mode: nil) ⇒ Object
Write content to a file, with restricted access
155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/aspera/environment.rb', line 155 def write_file_restricted(path, force: false, mode: nil) Aspera.assert(block_given?, type: Aspera::InternalError) if force || !File.exist?(path) # Windows may give error File.unlink(path) rescue nil # content provided by block File.write(path, yield) restrict_file_access(path, mode: mode) end return path end |
Instance Method Details
#architecture ⇒ Object
Normalized architecture name See constants: OS_* and CPU_*
251 252 253 |
# File 'lib/aspera/environment.rb', line 251 def architecture "#{@os}-#{@cpu}" end |
#exe_file(name = nil) ⇒ String
Add executable file extension (e.g. “.exe”) for current OS
258 259 260 261 |
# File 'lib/aspera/environment.rb', line 258 def exe_file(name = nil) return name unless @executable_extension return "#{name}#{@executable_extension}" end |
#fix_home ⇒ Object
on Windows, the env var %USERPROFILE% provides the path to user’s home more reliably than %HOMEDRIVE%%HOMEPATH% so, tell Ruby the right way
265 266 267 268 269 |
# File 'lib/aspera/environment.rb', line 265 def fix_home return unless @os.eql?(OS_WINDOWS) && ENV.key?('USERPROFILE') && Dir.exist?(ENV.fetch('USERPROFILE', nil)) ENV['HOME'] = ENV.fetch('USERPROFILE', nil) Log.log.debug{"Windows: set HOME to USERPROFILE: #{Dir.home}"} end |
#graphical? ⇒ Boolean
271 272 273 |
# File 'lib/aspera/environment.rb', line 271 def graphical? @default_gui_mode == :graphical end |
#initialize_fields ⇒ Object
initialize fields from environment
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/aspera/environment.rb', line 209 def initialize_fields @os = case RbConfig::CONFIG['host_os'] when /mswin/, /msys/, /mingw/, /cygwin/, /bccwin/, /wince/, /emc/ OS_WINDOWS when /darwin/, /mac os/ OS_MACOS when /linux/ OS_LINUX when /aix/ OS_AIX else Aspera.error_unexpected_value(RbConfig::CONFIG['host_os']){'host_os'} end @cpu = case RbConfig::CONFIG['host_cpu'] when /x86_64/, /x64/ CPU_X86_64 when /powerpc/, /ppc64/ @os.eql?(OS_LINUX) ? CPU_PPC64LE : CPU_PPC64 when /s390/ CPU_S390 when /arm/, /aarch64/ CPU_ARM64 else Aspera.error_unexpected_value(RbConfig::CONFIG['host_cpu']){'host_cpu'} end @executable_extension = @os.eql?(OS_WINDOWS) ? '.exe' : nil # :text or :graphical depending on the environment @default_gui_mode = if [Environment::OS_WINDOWS, Environment::OS_MACOS].include?(os) || (ENV.key?('DISPLAY') && !ENV['DISPLAY'].empty?) # assume not remotely connected on macos and windows or unix family :graphical else :text end @url_method = @default_gui_mode @file_illegal_characters = REPLACE_CHARACTER + WINDOWS_FILENAME_INVALID_CHARACTERS nil end |
#open_editor(file_path) ⇒ Object
open a file in an editor
288 289 290 291 292 293 294 295 296 |
# File 'lib/aspera/environment.rb', line 288 def open_editor(file_path) if ENV.key?('EDITOR') self.class.secure_execute(ENV['EDITOR'], file_path.to_s) elsif @os.eql?(Environment::OS_WINDOWS) self.class.secure_execute('notepad.exe', %Q{"#{file_path}"}) else open_uri_graphical(file_path.to_s) end end |
#open_uri(the_url) ⇒ Object
Allows a user to open a URL if method is :text, then URL is displayed on terminal if method is :graphical, then the URL will be opened with the default browser. this is non blocking
302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/aspera/environment.rb', line 302 def open_uri(the_url) case @url_method when :graphical open_uri_graphical(the_url) when :text case the_url.to_s when /^http/ puts "USER ACTION: please enter this URL in a browser:\n#{the_url.to_s.red}\n" else puts "USER ACTION: open this:\n#{the_url.to_s.red}\n" end else Aspera.error_unexpected_value(@url_method){'URL open method'} end end |
#open_uri_graphical(uri) ⇒ Object
Open a URI in a graphical browser Command must be non blocking
278 279 280 281 282 283 284 285 |
# File 'lib/aspera/environment.rb', line 278 def open_uri_graphical(uri) case @os when Environment::OS_MACOS then return self.class.secure_execute('open', uri.to_s) when Environment::OS_WINDOWS then return self.class.secure_execute('start', 'explorer', %Q{"#{uri}"}) when Environment::OS_LINUX then return self.class.secure_execute('xdg-open', uri.to_s) else Assert.error_unexpected_value(os){'no graphical open method'} end end |
#safe_filename_character ⇒ String
Replacement character for illegal filename characters Can also be used as safe “join” character
320 321 322 323 |
# File 'lib/aspera/environment.rb', line 320 def safe_filename_character return REPLACE_CHARACTER if @file_illegal_characters.nil? || @file_illegal_characters.empty? @file_illegal_characters[0] end |
#sanitized_filename(filename) ⇒ String
Sanitize a filename by replacing illegal characters
328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
# File 'lib/aspera/environment.rb', line 328 def sanitized_filename(filename) safe_char = safe_filename_character # Windows does not allow file name: # - with control characters anywhere # - ending with space or dot filename = filename.gsub(/[\x00-\x1F\x7F]/, safe_char) filename = filename.chop while filename.end_with?(' ', '.') if @file_illegal_characters&.size.to_i >= 2 # replace all illegal characters with safe_char filename = filename.tr(@file_illegal_characters[1..-1], safe_char) end # ensure only one safe_char is used at a time return filename.gsub(/#{Regexp.escape(safe_char)}+/, safe_char).chomp(safe_char) end |