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) ⇒ nil, ...
Execute a process securely without 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 ⇒ Object
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.
194 195 196 |
# File 'lib/aspera/environment.rb', line 194 def initialize initialize_fields end |
Instance Attribute Details
#cpu ⇒ Object (readonly)
Returns the value of attribute cpu.
192 193 194 |
# File 'lib/aspera/environment.rb', line 192 def cpu @cpu end |
#default_gui_mode ⇒ Object (readonly)
Returns the value of attribute default_gui_mode.
192 193 194 |
# File 'lib/aspera/environment.rb', line 192 def default_gui_mode @default_gui_mode end |
#file_illegal_characters ⇒ Object
Returns the value of attribute file_illegal_characters.
191 192 193 |
# File 'lib/aspera/environment.rb', line 191 def file_illegal_characters @file_illegal_characters end |
#os ⇒ Object (readonly)
Returns the value of attribute os.
192 193 194 |
# File 'lib/aspera/environment.rb', line 192 def os @os end |
#url_method ⇒ Object
Returns the value of attribute url_method.
191 192 193 |
# File 'lib/aspera/environment.rb', line 191 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
180 181 182 |
# File 'lib/aspera/environment.rb', line 180 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
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/aspera/environment.rb', line 158 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) ⇒ nil, ...
Execute a process securely without shell.
Execution mode can be:
-
:execute -> Kernel.system, return nil
-
:background -> Process.spawn, return pid
-
:capture -> Open3.capture3, return stdout
103 104 105 106 107 108 109 110 111 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 |
# File 'lib/aspera/environment.rb', line 103 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} stdout, stderr, status = Open3.capture3(*argv, **kwargs) Log.log.debug{"status=#{status}, stderr=#{stderr}"} Log.log.trace1{"stdout=#{stdout}"} raise "Process failed: #{status.exitstatus} (#{stderr})" if exception && !status.success? stdout 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.
175 176 177 |
# File 'lib/aspera/environment.rb', line 175 def terminal? $stdout.tty? end |
.terminal_supports_unicode? ⇒ Boolean
187 188 189 |
# File 'lib/aspera/environment.rb', line 187 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
145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/aspera/environment.rb', line 145 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_*
241 242 243 |
# File 'lib/aspera/environment.rb', line 241 def architecture "#{@os}-#{@cpu}" end |
#exe_file(name = nil) ⇒ String
Add executable file extension (e.g. “.exe”) for current OS
248 249 250 251 |
# File 'lib/aspera/environment.rb', line 248 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
255 256 257 258 259 |
# File 'lib/aspera/environment.rb', line 255 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
261 262 263 |
# File 'lib/aspera/environment.rb', line 261 def graphical? @default_gui_mode == :graphical end |
#initialize_fields ⇒ Object
initialize fields from environment
199 200 201 202 203 204 205 206 207 208 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 |
# File 'lib/aspera/environment.rb', line 199 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
278 279 280 281 282 283 284 285 286 |
# File 'lib/aspera/environment.rb', line 278 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
292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'lib/aspera/environment.rb', line 292 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
268 269 270 271 272 273 274 275 |
# File 'lib/aspera/environment.rb', line 268 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 ⇒ Object
Replacement character for illegal filename characters Can also be used as safe “join” character
309 310 311 312 |
# File 'lib/aspera/environment.rb', line 309 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
317 318 319 320 321 322 323 324 325 326 327 328 329 330 |
# File 'lib/aspera/environment.rb', line 317 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 |