| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  | # typed: true | 
					
						
							| 
									
										
										
										
											2019-04-19 15:38:03 +09:00
										 |  |  | # frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  | require "tempfile" | 
					
						
							| 
									
										
										
										
											2024-01-26 17:33:55 -08:00
										 |  |  | require "system_command" | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | module UnpackStrategy | 
					
						
							| 
									
										
										
										
											2020-08-09 06:09:05 +02:00
										 |  |  |   # Strategy for unpacking disk images. | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  |   class Dmg | 
					
						
							| 
									
										
										
										
											2024-01-26 17:33:55 -08:00
										 |  |  |     extend SystemCommand::Mixin | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  |     include UnpackStrategy | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-09 06:09:05 +02:00
										 |  |  |     # Helper module for listing the contents of a volume mounted from a disk image. | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |     module Bom | 
					
						
							| 
									
										
										
										
											2024-01-26 17:33:55 -08:00
										 |  |  |       extend SystemCommand::Mixin | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-19 21:46:20 +09:00
										 |  |  |       DMG_METADATA = Set.new(%w[
 | 
					
						
							| 
									
										
										
										
											2021-02-11 13:24:19 +00:00
										 |  |  |         .background | 
					
						
							|  |  |  |         .com.apple.timemachine.donotpresent | 
					
						
							|  |  |  |         .com.apple.timemachine.supported | 
					
						
							|  |  |  |         .DocumentRevisions-V100 | 
					
						
							|  |  |  |         .DS_Store | 
					
						
							|  |  |  |         .fseventsd | 
					
						
							|  |  |  |         .MobileBackups | 
					
						
							|  |  |  |         .Spotlight-V100 | 
					
						
							|  |  |  |         .TemporaryItems | 
					
						
							|  |  |  |         .Trashes | 
					
						
							|  |  |  |         .VolumeIcon.icns | 
					
						
							|  |  |  |       ]).freeze | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |       private_constant :DMG_METADATA | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-13 18:08:53 +01:00
										 |  |  |       class Error < RuntimeError; end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       class EmptyError < Error | 
					
						
							|  |  |  |         def initialize(path) | 
					
						
							|  |  |  |           super "BOM for path '#{path}' is empty." | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |       # Check if path is considered disk image metadata. | 
					
						
							|  |  |  |       sig { params(pathname: Pathname).returns(T::Boolean) } | 
					
						
							|  |  |  |       def self.dmg_metadata?(pathname) | 
					
						
							|  |  |  |         DMG_METADATA.include?(pathname.cleanpath.ascend.to_a.last.to_s) | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |       # Check if path is a symlink to a system directory (commonly to /Applications). | 
					
						
							|  |  |  |       sig { params(pathname: Pathname).returns(T::Boolean) } | 
					
						
							|  |  |  |       def self.system_dir_symlink?(pathname) | 
					
						
							|  |  |  |         pathname.symlink? && MacOS.system_dir?(pathname.dirname.join(pathname.readlink)) | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |       sig { params(pathname: Pathname).returns(String) } | 
					
						
							|  |  |  |       def self.bom(pathname) | 
					
						
							|  |  |  |         tries = 0
 | 
					
						
							|  |  |  |         result = loop do | 
					
						
							|  |  |  |           # We need to use `find` here instead of Ruby in order to properly handle | 
					
						
							|  |  |  |           # file names containing special characters, such as “e” + “´” vs. “é”. | 
					
						
							|  |  |  |           r = system_command("find", args: [".", "-print0"], chdir: pathname, print_stderr: false) | 
					
						
							|  |  |  |           tries += 1
 | 
					
						
							| 
									
										
										
										
											2020-11-25 22:19:52 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |           # Spurious bug on CI, which in most cases can be worked around by retrying. | 
					
						
							|  |  |  |           break r unless r.stderr.match?(/Interrupted system call/i) | 
					
						
							| 
									
										
										
										
											2020-11-25 22:19:52 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |           raise "Command `#{r.command.shelljoin}` was interrupted." if tries >= 3
 | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2020-11-25 22:19:52 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |         odebug "Command `#{result.command.shelljoin}` in '#{pathname}' took #{tries} tries." if tries > 1
 | 
					
						
							| 
									
										
										
										
											2020-11-26 11:24:39 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |         bom_paths = result.stdout.split("\0") | 
					
						
							| 
									
										
										
										
											2020-11-26 11:24:39 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |         raise EmptyError, pathname if bom_paths.empty? | 
					
						
							| 
									
										
										
										
											2020-11-25 19:37:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |         bom_paths | 
					
						
							|  |  |  |           .reject { |path| dmg_metadata?(Pathname(path)) } | 
					
						
							|  |  |  |           .reject { |path| system_dir_symlink?(pathname/path) } | 
					
						
							|  |  |  |           .join("\n") | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-09 06:09:05 +02:00
										 |  |  |     # Strategy for unpacking a volume mounted from a disk image. | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |     class Mount | 
					
						
							|  |  |  |       include UnpackStrategy | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-24 18:43:20 +02:00
										 |  |  |       def eject(verbose: false) | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |         tries = 3
 | 
					
						
							|  |  |  |         begin | 
					
						
							|  |  |  |           return unless path.exist? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           if tries > 1
 | 
					
						
							|  |  |  |             disk_info = system_command!( | 
					
						
							|  |  |  |               "diskutil", | 
					
						
							|  |  |  |               args:         ["info", "-plist", path], | 
					
						
							|  |  |  |               print_stderr: false, | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |               verbose:, | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # For HFS, just use <mount-path> | 
					
						
							|  |  |  |             # For APFS, find the <physical-store> corresponding to <mount-path> | 
					
						
							|  |  |  |             eject_paths = disk_info.plist | 
					
						
							| 
									
										
										
										
											2023-03-20 13:16:31 -07:00
										 |  |  |                                    .fetch("APFSPhysicalStores", []) | 
					
						
							| 
									
										
										
										
											2024-02-22 23:29:55 +00:00
										 |  |  |                                    .filter_map { |store| store["APFSPhysicalStore"] } | 
					
						
							| 
									
										
										
										
											2023-03-20 13:16:31 -07:00
										 |  |  |                                    .presence || [path] | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |             eject_paths.each do |eject_path| | 
					
						
							|  |  |  |               system_command! "diskutil", | 
					
						
							|  |  |  |                               args:         ["eject", eject_path], | 
					
						
							|  |  |  |                               print_stderr: false, | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |                               verbose: | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |             end | 
					
						
							|  |  |  |           else | 
					
						
							| 
									
										
										
										
											2021-03-28 04:04:07 -07:00
										 |  |  |             system_command! "diskutil", | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |                             args:         ["unmount", "force", path], | 
					
						
							| 
									
										
										
										
											2021-03-28 04:04:07 -07:00
										 |  |  |                             print_stderr: false, | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |                             verbose: | 
					
						
							| 
									
										
										
										
											2021-03-28 04:04:07 -07:00
										 |  |  |           end | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |         rescue ErrorDuringExecution => e | 
					
						
							|  |  |  |           raise e if (tries -= 1).zero? | 
					
						
							| 
									
										
										
										
											2018-09-17 02:45:00 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |           sleep 1
 | 
					
						
							|  |  |  |           retry | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       private | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-20 12:03:48 +02:00
										 |  |  |       sig { override.params(unpack_dir: Pathname, basename: Pathname, verbose: T::Boolean).returns(T.untyped) } | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |       def extract_to_dir(unpack_dir, basename:, verbose:) | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |         tries = 3
 | 
					
						
							| 
									
										
										
										
											2023-03-13 18:08:53 +01:00
										 |  |  |         bom = begin | 
					
						
							| 
									
										
										
										
											2023-03-20 13:15:43 -07:00
										 |  |  |           Bom.bom(path) | 
					
						
							| 
									
										
										
										
											2023-03-13 18:08:53 +01:00
										 |  |  |         rescue Bom::EmptyError => e | 
					
						
							| 
									
										
										
										
											2023-03-21 00:39:22 +01:00
										 |  |  |           raise e if (tries -= 1).zero? | 
					
						
							| 
									
										
										
										
											2023-03-13 18:08:53 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |           sleep 1
 | 
					
						
							|  |  |  |           retry | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |         Tempfile.open(["", ".bom"]) do |bomfile| | 
					
						
							|  |  |  |           bomfile.close | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           Tempfile.open(["", ".list"]) do |filelist| | 
					
						
							| 
									
										
										
										
											2023-03-13 18:08:53 +01:00
										 |  |  |             filelist.puts(bom) | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |             filelist.close | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-24 18:43:20 +02:00
										 |  |  |             system_command! "mkbom", | 
					
						
							| 
									
										
										
										
											2018-11-02 17:18:07 +00:00
										 |  |  |                             args:    ["-s", "-i", filelist.path, "--", bomfile.path], | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |                             verbose: | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |           end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-01 09:49:10 +01:00
										 |  |  |           bomfile_path = T.must(bomfile.path) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           # Ditto will try to write as the UID, not the EUID and the Tempfile has 0700 permissions. | 
					
						
							|  |  |  |           if Process.euid != Process.uid | 
					
						
							|  |  |  |             FileUtils.chown(nil, Process.gid, bomfile_path) | 
					
						
							|  |  |  |             FileUtils.chmod "g+rw", bomfile_path | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |           system_command!("ditto", | 
					
						
							| 
									
										
										
										
											2024-04-01 09:49:10 +01:00
										 |  |  |                           args:    ["--bom", bomfile_path, "--", path, unpack_dir], | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |                           verbose:) | 
					
						
							| 
									
										
										
										
											2018-07-29 10:04:51 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-31 13:16:11 +00:00
										 |  |  |           FileUtils.chmod "u+w", Pathname.glob(unpack_dir/"**/*", File::FNM_DOTMATCH).reject(&:symlink?) | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     private_constant :Mount | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-20 12:03:48 +02:00
										 |  |  |     sig { returns(T::Array[String]) } | 
					
						
							| 
									
										
										
										
											2018-07-30 09:49:59 +02:00
										 |  |  |     def self.extensions | 
					
						
							|  |  |  |       [".dmg"] | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2018-07-29 10:04:51 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-30 09:49:59 +02:00
										 |  |  |     def self.can_extract?(path) | 
					
						
							| 
									
										
										
										
											2019-10-14 10:44:52 +02:00
										 |  |  |       stdout, _, status = system_command("hdiutil", args: ["imageinfo", "-format", path], print_stderr: false) | 
					
						
							| 
									
										
										
										
											2019-10-13 17:19:02 +02:00
										 |  |  |       status.success? && !stdout.empty? | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |     private | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-20 12:03:48 +02:00
										 |  |  |     sig { override.params(unpack_dir: Pathname, basename: Pathname, verbose: T::Boolean).returns(T.untyped) } | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  |     def extract_to_dir(unpack_dir, basename:, verbose:) | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |       mount(verbose:) do |mounts| | 
					
						
							| 
									
										
										
										
											2019-04-08 12:47:15 -04:00
										 |  |  |         raise "No mounts found in '#{path}'; perhaps this is a bad disk image?" if mounts.empty? | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         mounts.each do |mount| | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |           mount.extract(to: unpack_dir, verbose:) | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def mount(verbose: false) | 
					
						
							| 
									
										
										
										
											2024-02-26 16:58:39 +00:00
										 |  |  |       Dir.mktmpdir("homebrew-dmg", HOMEBREW_TEMP) do |mount_dir| | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |         mount_dir = Pathname(mount_dir) | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-02 16:15:09 +01:00
										 |  |  |         without_eula = system_command( | 
					
						
							|  |  |  |           "hdiutil", | 
					
						
							| 
									
										
										
										
											2018-11-02 17:18:07 +00:00
										 |  |  |           args:         [ | 
					
						
							| 
									
										
										
										
											2020-09-19 21:20:25 +08:00
										 |  |  |             "attach", "-plist", "-nobrowse", "-readonly", | 
					
						
							| 
									
										
										
										
											2018-09-02 16:15:09 +01:00
										 |  |  |             "-mountrandom", mount_dir, path | 
					
						
							|  |  |  |           ], | 
					
						
							| 
									
										
										
										
											2018-11-02 17:18:07 +00:00
										 |  |  |           input:        "qn\n", | 
					
						
							| 
									
										
										
										
											2018-09-02 16:15:09 +01:00
										 |  |  |           print_stderr: false, | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |           verbose:, | 
					
						
							| 
									
										
										
										
											2018-09-02 16:15:09 +01:00
										 |  |  |         ) | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         # If mounting without agreeing to EULA succeeded, there is none. | 
					
						
							|  |  |  |         plist = if without_eula.success? | 
					
						
							|  |  |  |           without_eula.plist | 
					
						
							|  |  |  |         else | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |           cdr_path = mount_dir/path.basename.sub_ext(".cdr") | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-02 11:44:00 +01:00
										 |  |  |           quiet_flag = "-quiet" unless verbose | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-02 16:15:09 +01:00
										 |  |  |           system_command!( | 
					
						
							|  |  |  |             "hdiutil", | 
					
						
							| 
									
										
										
										
											2018-11-02 17:18:07 +00:00
										 |  |  |             args:    [ | 
					
						
							| 
									
										
										
										
											2018-11-02 11:44:00 +01:00
										 |  |  |               "convert", *quiet_flag, "-format", "UDTO", "-o", cdr_path, path | 
					
						
							| 
									
										
										
										
											2018-09-02 16:15:09 +01:00
										 |  |  |             ], | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |             verbose:, | 
					
						
							| 
									
										
										
										
											2018-09-02 16:15:09 +01:00
										 |  |  |           ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           with_eula = system_command!( | 
					
						
							|  |  |  |             "hdiutil", | 
					
						
							| 
									
										
										
										
											2018-11-02 17:18:07 +00:00
										 |  |  |             args:    [ | 
					
						
							| 
									
										
										
										
											2020-09-19 21:20:25 +08:00
										 |  |  |               "attach", "-plist", "-nobrowse", "-readonly", | 
					
						
							| 
									
										
										
										
											2018-09-02 16:15:09 +01:00
										 |  |  |               "-mountrandom", mount_dir, cdr_path | 
					
						
							|  |  |  |             ], | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |             verbose:, | 
					
						
							| 
									
										
										
										
											2018-09-02 16:15:09 +01:00
										 |  |  |           ) | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |           if verbose && !(eula_text = without_eula.stdout).empty? | 
					
						
							| 
									
										
										
										
											2020-07-06 15:30:57 -04:00
										 |  |  |             ohai "Software License Agreement for '#{path}':", eula_text | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  |           end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           with_eula.plist | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |         mounts = if plist.respond_to?(:fetch) | 
					
						
							|  |  |  |           plist.fetch("system-entities", []) | 
					
						
							| 
									
										
										
										
											2024-02-22 23:29:55 +00:00
										 |  |  |                .filter_map { |entity| entity["mount-point"] } | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |                .map { |path| Mount.new(path) } | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  |         else | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |           [] | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-24 07:03:24 +02:00
										 |  |  |         begin | 
					
						
							|  |  |  |           yield mounts | 
					
						
							|  |  |  |         ensure | 
					
						
							| 
									
										
										
										
											2018-07-24 18:43:20 +02:00
										 |  |  |           mounts.each do |mount| | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |             mount.eject(verbose:) | 
					
						
							| 
									
										
										
										
											2018-07-24 18:43:20 +02:00
										 |  |  |           end | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |