vagrant-parallels 1.7.7 → 2.2.0
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 +5 -5
- data/CHANGELOG.md +73 -35
- data/README.md +29 -31
- data/lib/vagrant-parallels/action.rb +2 -0
- data/lib/vagrant-parallels/action/box_unregister.rb +2 -2
- data/lib/vagrant-parallels/action/import.rb +2 -12
- data/lib/vagrant-parallels/action/network.rb +6 -1
- data/lib/vagrant-parallels/action/package_vagrantfile.rb +33 -0
- data/lib/vagrant-parallels/action/prepare_clone_snapshot.rb +1 -2
- data/lib/vagrant-parallels/action/sane_defaults.rb +3 -18
- data/lib/vagrant-parallels/cap/mount_options.rb +39 -0
- data/lib/vagrant-parallels/config.rb +1 -1
- data/lib/vagrant-parallels/driver/base.rb +14 -36
- data/lib/vagrant-parallels/driver/meta.rb +6 -8
- data/lib/vagrant-parallels/driver/pd_11.rb +2 -2
- data/lib/vagrant-parallels/driver/pd_12.rb +1 -1
- data/lib/vagrant-parallels/errors.rb +9 -5
- data/lib/vagrant-parallels/guest_cap/linux/mount_parallels_shared_folder.rb +31 -64
- data/lib/vagrant-parallels/plugin.rb +15 -1
- data/lib/vagrant-parallels/synced_folder.rb +10 -20
- data/lib/vagrant-parallels/util/unix_mount_helpers.rb +121 -0
- data/lib/vagrant-parallels/version.rb +1 -1
- data/locales/en.yml +27 -14
- metadata +14 -13
- data/lib/vagrant-parallels/driver/pd_10.rb +0 -20
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative "../util/unix_mount_helpers"
|
2
|
+
|
3
|
+
module VagrantPlugins
|
4
|
+
module Parallels
|
5
|
+
module SyncedFolderCap
|
6
|
+
module MountOptions
|
7
|
+
extend VagrantPlugins::Parallels::Util::UnixMountHelpers
|
8
|
+
|
9
|
+
PRL_MOUNT_TYPE = "prl_fs".freeze
|
10
|
+
|
11
|
+
# Returns mount options for a parallels synced folder
|
12
|
+
#
|
13
|
+
# @param [Machine] machine
|
14
|
+
# @param [String] name of mount
|
15
|
+
# @param [String] path of mount on guest
|
16
|
+
# @param [Hash] hash of mount options
|
17
|
+
def self.mount_options(machine, name, guest_path, options)
|
18
|
+
mount_options = options.fetch(:mount_options, [])
|
19
|
+
detected_ids = detect_owner_group_ids(machine, guest_path, mount_options, options)
|
20
|
+
mount_uid = detected_ids[:uid]
|
21
|
+
mount_gid = detected_ids[:gid]
|
22
|
+
|
23
|
+
mount_options << "uid=#{mount_uid}"
|
24
|
+
mount_options << "gid=#{mount_gid}"
|
25
|
+
mount_options = mount_options.join(',')
|
26
|
+
return mount_options, mount_uid, mount_gid
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.mount_type(machine)
|
30
|
+
return PRL_MOUNT_TYPE
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.mount_name(machine, data)
|
34
|
+
data[:guestpath].gsub(/[*":<>?|\/\\]/,'_').sub(/^_/, '')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -71,7 +71,7 @@ module VagrantPlugins
|
|
71
71
|
@functional_psf = true
|
72
72
|
end
|
73
73
|
|
74
|
-
@linked_clone =
|
74
|
+
@linked_clone = true if @linked_clone == UNSET_VALUE
|
75
75
|
@linked_clone_snapshot = nil if @linked_clone_snapshot == UNSET_VALUE
|
76
76
|
|
77
77
|
@name = nil if @name == UNSET_VALUE
|
@@ -193,14 +193,13 @@ module VagrantPlugins
|
|
193
193
|
end
|
194
194
|
end
|
195
195
|
|
196
|
-
# Deletes
|
196
|
+
# Deletes host-only networks that aren't being used by any virtual machine.
|
197
197
|
def delete_unused_host_only_networks
|
198
198
|
networks = read_virtual_networks
|
199
|
-
|
200
|
-
#
|
199
|
+
|
200
|
+
# Exclude all host-only network interfaces which were not created by vagrant provider.
|
201
201
|
networks.keep_if do |net|
|
202
|
-
net['Type'] == 'host-only' && net['
|
203
|
-
net['Bound To'].match(/^(?>vnic|Parallels Host-Only #)(\d+)$/)[1].to_i >= 2
|
202
|
+
net['Type'] == 'host-only' && net['Network ID'] =~ /^vagrant-vnet(\d+)$/
|
204
203
|
end
|
205
204
|
|
206
205
|
read_vms_info.each do |vm|
|
@@ -210,8 +209,8 @@ module VagrantPlugins
|
|
210
209
|
end
|
211
210
|
end
|
212
211
|
|
212
|
+
# Delete all unused network interfaces.
|
213
213
|
networks.each do |net|
|
214
|
-
# Delete the actual host only network interface.
|
215
214
|
execute_prlsrvctl('net', 'del', net['Network ID'])
|
216
215
|
end
|
217
216
|
end
|
@@ -496,7 +495,6 @@ module VagrantPlugins
|
|
496
495
|
#
|
497
496
|
# {
|
498
497
|
# name: 'Host-Only', # Parallels Network ID
|
499
|
-
# bound_to: 'vnic1', # interface name
|
500
498
|
# ip: '10.37.129.2', # IP address of the interface
|
501
499
|
# netmask: '255.255.255.0', # netmask associated with the interface
|
502
500
|
# status: 'Up' # status of the interface
|
@@ -519,11 +517,9 @@ module VagrantPlugins
|
|
519
517
|
}
|
520
518
|
|
521
519
|
adapter = net_info['Parallels adapter']
|
522
|
-
if adapter
|
523
|
-
|
524
|
-
iface[:
|
525
|
-
iface[:netmask] = adapter['Subnet mask'] || adapter['IPv4 subnet mask']
|
526
|
-
iface[:bound_to] = net_info['Bound To']
|
520
|
+
if adapter
|
521
|
+
iface[:ip] = adapter['IPv4 address']
|
522
|
+
iface[:netmask] = adapter['IPv4 subnet mask']
|
527
523
|
iface[:status] = 'Up'
|
528
524
|
|
529
525
|
if adapter['IPv6 address'] && adapter['IPv6 subnet mask']
|
@@ -618,11 +614,9 @@ module VagrantPlugins
|
|
618
614
|
}
|
619
615
|
adapter = net_info['Parallels adapter']
|
620
616
|
|
621
|
-
if adapter
|
622
|
-
|
623
|
-
iface[:
|
624
|
-
iface[:netmask] = adapter['Subnet mask'] || adapter['IPv4 subnet mask']
|
625
|
-
iface[:bound_to] = net_info['Bound To']
|
617
|
+
if adapter
|
618
|
+
iface[:ip] = adapter['IPv4 address']
|
619
|
+
iface[:netmask] = adapter['IPv4 subnet mask']
|
626
620
|
iface[:status] = 'Up'
|
627
621
|
end
|
628
622
|
|
@@ -724,22 +718,6 @@ module VagrantPlugins
|
|
724
718
|
vms_arr | templates_arr
|
725
719
|
end
|
726
720
|
|
727
|
-
# Regenerates 'SourceVmUuid' to avoid SMBIOS UUID collision [GH-113]
|
728
|
-
#
|
729
|
-
def regenerate_src_uuid
|
730
|
-
settings = read_settings
|
731
|
-
vm_config = File.join(settings.fetch('Home'), 'config.pvs')
|
732
|
-
|
733
|
-
# Generate and put new SourceVmUuid
|
734
|
-
xml = Nokogiri::XML(File.open(vm_config))
|
735
|
-
p = '//ParallelsVirtualMachine/Identification/SourceVmUuid'
|
736
|
-
xml.xpath(p).first.content = "{#{SecureRandom.uuid}}"
|
737
|
-
|
738
|
-
File.open(vm_config, 'w') do |f|
|
739
|
-
f.write xml.to_xml
|
740
|
-
end
|
741
|
-
end
|
742
|
-
|
743
721
|
# Registers the virtual machine
|
744
722
|
#
|
745
723
|
# @param [String] pvm_file Path to the machine image (*.pvm)
|
@@ -858,14 +836,14 @@ module VagrantPlugins
|
|
858
836
|
true
|
859
837
|
end
|
860
838
|
|
861
|
-
private
|
862
|
-
|
863
839
|
# Wraps 'execute' and returns the output of given 'prlctl' subcommand.
|
864
840
|
def execute_prlctl(*command, &block)
|
865
841
|
execute(@prlctl_path, *command, &block)
|
866
842
|
end
|
867
843
|
|
868
|
-
|
844
|
+
private
|
845
|
+
|
846
|
+
# Wraps 'execute' and returns the output of given 'prlsrvctl' subcommand.
|
869
847
|
def execute_prlsrvctl(*command, &block)
|
870
848
|
execute(@prlsrvctl_path, *command, &block)
|
871
849
|
end
|
@@ -37,26 +37,23 @@ module VagrantPlugins
|
|
37
37
|
@@version_lock.synchronize do
|
38
38
|
@@version = read_version
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
41
|
# Instantiate the proper version driver for Parallels Desktop
|
42
42
|
@logger.debug("Finding driver for Parallels Desktop version: #{@@version}")
|
43
43
|
|
44
44
|
major_ver = @@version.split('.').first.to_i
|
45
45
|
driver_klass =
|
46
46
|
case major_ver
|
47
|
-
when 1..
|
48
|
-
when 10 then PD_10
|
47
|
+
when 1..10 then raise Errors::ParallelsUnsupportedVersion
|
49
48
|
when 11 then PD_11
|
50
49
|
else PD_12
|
51
50
|
end
|
52
51
|
|
53
52
|
# Starting since PD 11 only Pro and Business editions have CLI
|
54
53
|
# functionality and can be used with Vagrant.
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
raise Errors::ParallelsUnsupportedEdition
|
59
|
-
end
|
54
|
+
edition = read_edition
|
55
|
+
if !edition || !%w(any pro business).include?(edition)
|
56
|
+
raise Errors::ParallelsUnsupportedEdition
|
60
57
|
end
|
61
58
|
|
62
59
|
@logger.info("Using Parallels driver: #{driver_klass}")
|
@@ -143,6 +140,7 @@ module VagrantPlugins
|
|
143
140
|
# * prlctl version 8.0.12345.123456
|
144
141
|
# * prlctl version 9.0.12345.123456
|
145
142
|
# * prlctl version 10.0.0 (12345) rev 123456
|
143
|
+
# * prlctl version 14.0.1 (45154)
|
146
144
|
#
|
147
145
|
# But we need exactly the first 3 numbers: "x.x.x"
|
148
146
|
output = execute(@prlctl_path, '--version')
|
@@ -2,13 +2,13 @@ require 'log4r'
|
|
2
2
|
|
3
3
|
require 'vagrant/util/platform'
|
4
4
|
|
5
|
-
require_relative '
|
5
|
+
require_relative 'base'
|
6
6
|
|
7
7
|
module VagrantPlugins
|
8
8
|
module Parallels
|
9
9
|
module Driver
|
10
10
|
# Driver for Parallels Desktop 11.
|
11
|
-
class PD_11 <
|
11
|
+
class PD_11 < Base
|
12
12
|
def initialize(uuid)
|
13
13
|
super(uuid)
|
14
14
|
|
@@ -27,10 +27,6 @@ module VagrantPlugins
|
|
27
27
|
error_key(:json_parse_error)
|
28
28
|
end
|
29
29
|
|
30
|
-
class LinuxMountFailed < VagrantParallelsError
|
31
|
-
error_key(:linux_mount_failed)
|
32
|
-
end
|
33
|
-
|
34
30
|
class LinuxPrlFsInvalidOptions < VagrantParallelsError
|
35
31
|
error_key(:linux_prl_fs_invalid_options)
|
36
32
|
end
|
@@ -39,6 +35,10 @@ module VagrantPlugins
|
|
39
35
|
error_key(:mac_os_x_required)
|
40
36
|
end
|
41
37
|
|
38
|
+
class NetworkCollision < VagrantParallelsError
|
39
|
+
error_key(:network_collision)
|
40
|
+
end
|
41
|
+
|
42
42
|
class NetworkInvalidAddress < VagrantParallelsError
|
43
43
|
error_key(:network_invalid_address)
|
44
44
|
end
|
@@ -55,6 +55,10 @@ module VagrantPlugins
|
|
55
55
|
error_key(:parallels_invalid_version)
|
56
56
|
end
|
57
57
|
|
58
|
+
class ParallelsMountFailed < VagrantParallelsError
|
59
|
+
error_key(:parallels_mount_failed)
|
60
|
+
end
|
61
|
+
|
58
62
|
class ParallelsNotDetected < VagrantParallelsError
|
59
63
|
error_key(:parallels_not_detected)
|
60
64
|
end
|
@@ -104,4 +108,4 @@ module VagrantPlugins
|
|
104
108
|
end
|
105
109
|
end
|
106
110
|
end
|
107
|
-
end
|
111
|
+
end
|
@@ -1,7 +1,19 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
require_relative "../../util/unix_mount_helpers"
|
4
|
+
|
1
5
|
module VagrantPlugins
|
2
6
|
module Parallels
|
3
7
|
module GuestLinuxCap
|
4
8
|
class MountParallelsSharedFolder
|
9
|
+
extend VagrantPlugins::Parallels::Util::UnixMountHelpers
|
10
|
+
|
11
|
+
# Mounts Parallels Desktop shared folder on linux guest
|
12
|
+
#
|
13
|
+
# @param [Machine] machine
|
14
|
+
# @param [String] name of mount
|
15
|
+
# @param [String] path of mount on guest
|
16
|
+
# @param [Hash] hash of mount options
|
5
17
|
def self.mount_parallels_shared_folder(machine, name, guestpath, options)
|
6
18
|
# Sanity check for mount options: we are not supporting
|
7
19
|
# VirtualBox-specific 'fmode' and 'dmode' options
|
@@ -15,83 +27,38 @@ module VagrantPlugins
|
|
15
27
|
end
|
16
28
|
end
|
17
29
|
|
18
|
-
|
19
|
-
|
30
|
+
guest_path = Shellwords.escape(guestpath)
|
31
|
+
mount_type = options[:plugin].capability(:mount_type)
|
20
32
|
|
21
|
-
|
33
|
+
@@logger.debug("Mounting #{name} (#{options[:hostpath]} to #{guestpath})")
|
22
34
|
|
23
|
-
|
24
|
-
|
25
|
-
else
|
26
|
-
mount_uid = "`id -u #{options[:owner]}`"
|
27
|
-
end
|
28
|
-
|
29
|
-
if options[:group].is_a? Integer
|
30
|
-
mount_gid = options[:group]
|
31
|
-
mount_gid_old = options[:group]
|
32
|
-
else
|
33
|
-
mount_gid = "`getent group #{options[:group]} | cut -d: -f3`"
|
34
|
-
mount_gid_old = "`id -g #{options[:group]}`"
|
35
|
-
end
|
36
|
-
|
37
|
-
# First mount command uses getent to get the group
|
38
|
-
mount_options = "-o uid=#{mount_uid},gid=#{mount_gid}"
|
39
|
-
mount_options += ",#{options[:mount_options].join(',')}" if options[:mount_options]
|
40
|
-
mount_commands << "mount -t prl_fs #{mount_options} #{name} #{expanded_guest_path}"
|
41
|
-
|
42
|
-
# Second mount command uses the old style `id -g`
|
43
|
-
mount_options = "-o uid=#{mount_uid},gid=#{mount_gid_old}"
|
44
|
-
mount_options += ",#{options[:mount_options].join(',')}" if options[:mount_options]
|
45
|
-
mount_commands << "mount -t prl_fs #{mount_options} #{name} #{expanded_guest_path}"
|
46
|
-
|
47
|
-
# Clear prior symlink if exists
|
48
|
-
if machine.communicate.test("test -L #{expanded_guest_path}")
|
49
|
-
machine.communicate.sudo("rm #{expanded_guest_path}")
|
50
|
-
end
|
35
|
+
mount_options, mount_uid, mount_gid = options[:plugin].capability(:mount_options, name, guest_path, options)
|
36
|
+
mount_command = "mount -t #{mount_type} -o #{mount_options} #{name} #{guest_path}"
|
51
37
|
|
52
38
|
# Create the guest path if it doesn't exist
|
53
|
-
machine.communicate.sudo("mkdir -p #{
|
39
|
+
machine.communicate.sudo("mkdir -p #{guest_path}")
|
54
40
|
|
55
41
|
# Attempt to mount the folder. We retry here a few times because
|
56
42
|
# it can fail early on.
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
end
|
66
|
-
|
67
|
-
success = status == 0 && !no_such_device
|
68
|
-
break if success
|
69
|
-
end
|
70
|
-
|
71
|
-
break if success
|
72
|
-
|
73
|
-
attempts += 1
|
74
|
-
if attempts > 10
|
75
|
-
raise VagrantPlugins::Parallels::Errors::LinuxMountFailed,
|
76
|
-
command: mount_commands.join("\n")
|
77
|
-
end
|
78
|
-
|
79
|
-
sleep 2
|
43
|
+
stderr = ""
|
44
|
+
retryable(on: Errors::ParallelsMountFailed, tries: 3, sleep: 5) do
|
45
|
+
machine.communicate.sudo(mount_command,
|
46
|
+
error_class: Errors::ParallelsMountFailed,
|
47
|
+
error_key: :parallels_mount_failed,
|
48
|
+
command: mount_command,
|
49
|
+
output: stderr,
|
50
|
+
) { |type, data| stderr = data if type == :stderr }
|
80
51
|
end
|
81
52
|
|
82
|
-
|
83
|
-
machine.communicate.sudo <<-EOH.gsub(/^ {10}/, "")
|
84
|
-
if command -v /sbin/init && /sbin/init 2>/dev/null --version | grep upstart; then
|
85
|
-
/sbin/initctl emit --no-wait vagrant-mounted MOUNTPOINT=#{expanded_guest_path}
|
86
|
-
fi
|
87
|
-
EOH
|
53
|
+
emit_upstart_notification(machine, guest_path)
|
88
54
|
end
|
89
55
|
|
90
56
|
def self.unmount_parallels_shared_folder(machine, guestpath, options)
|
91
|
-
|
92
|
-
|
57
|
+
guest_path = Shellwords.escape(guestpath)
|
58
|
+
|
59
|
+
result = machine.communicate.sudo("umount #{guest_path}", error_check: false)
|
93
60
|
if result == 0
|
94
|
-
machine.communicate.sudo("rmdir #{
|
61
|
+
machine.communicate.sudo("rmdir #{guest_path}", error_check: false)
|
95
62
|
end
|
96
63
|
end
|
97
64
|
|
@@ -103,6 +103,21 @@ module VagrantPlugins
|
|
103
103
|
SyncedFolder
|
104
104
|
end
|
105
105
|
|
106
|
+
synced_folder_capability(:parallels, "mount_name") do
|
107
|
+
require_relative "cap/mount_options"
|
108
|
+
SyncedFolderCap::MountOptions
|
109
|
+
end
|
110
|
+
|
111
|
+
synced_folder_capability(:parallels, "mount_options") do
|
112
|
+
require_relative "cap/mount_options"
|
113
|
+
SyncedFolderCap::MountOptions
|
114
|
+
end
|
115
|
+
|
116
|
+
synced_folder_capability(:parallels, "mount_type") do
|
117
|
+
require_relative "cap/mount_options"
|
118
|
+
SyncedFolderCap::MountOptions
|
119
|
+
end
|
120
|
+
|
106
121
|
# This initializes the internationalization strings.
|
107
122
|
def self.setup_i18n
|
108
123
|
I18n.load_path << File.expand_path('locales/en.yml', Parallels.source_root)
|
@@ -143,7 +158,6 @@ module VagrantPlugins
|
|
143
158
|
# our drivers only when they are needed.
|
144
159
|
module Driver
|
145
160
|
autoload :Meta, File.expand_path('../driver/meta', __FILE__)
|
146
|
-
autoload :PD_10, File.expand_path('../driver/pd_10', __FILE__)
|
147
161
|
autoload :PD_11, File.expand_path('../driver/pd_11', __FILE__)
|
148
162
|
autoload :PD_12, File.expand_path('../driver/pd_12', __FILE__)
|
149
163
|
end
|
@@ -19,14 +19,11 @@ module VagrantPlugins
|
|
19
19
|
end
|
20
20
|
|
21
21
|
defs << {
|
22
|
-
|
23
|
-
|
22
|
+
name: data[:plugin].capability(:mount_name, data),
|
23
|
+
hostpath: hostpath.to_s,
|
24
24
|
}
|
25
25
|
end
|
26
26
|
|
27
|
-
# We should prepare only folders with unique hostpath values.
|
28
|
-
# Anyway, duplicates will be mounted later.
|
29
|
-
defs.uniq! { |d| d[:hostpath] }
|
30
27
|
driver(machine).share_folders(defs)
|
31
28
|
|
32
29
|
# short guestpaths first, so we don't step on ourselves
|
@@ -39,8 +36,6 @@ module VagrantPlugins
|
|
39
36
|
end
|
40
37
|
end
|
41
38
|
|
42
|
-
shf_config = driver(machine).read_shared_folders
|
43
|
-
|
44
39
|
# Parallels Shared Folder services can override Vagrant synced folder
|
45
40
|
# configuration. These services should be pre-configured.
|
46
41
|
if machine.guest.capability?(:prepare_psf_services)
|
@@ -49,12 +44,8 @@ module VagrantPlugins
|
|
49
44
|
|
50
45
|
# Go through each folder and mount
|
51
46
|
machine.ui.output(I18n.t('vagrant.actions.vm.share_folders.mounting'))
|
52
|
-
folders.each do |
|
53
|
-
|
54
|
-
# It allows to mount one host folder more then one time [GH-105]
|
55
|
-
id = shf_config.key(data[:hostpath])
|
56
|
-
|
57
|
-
if data[:guestpath] and id
|
47
|
+
folders.each do |id , data|
|
48
|
+
if data[:guestpath]
|
58
49
|
# Guest path specified, so mount the folder to specified point
|
59
50
|
machine.ui.detail(I18n.t('vagrant.actions.vm.share_folders.mounting_entry',
|
60
51
|
guestpath: data[:guestpath],
|
@@ -70,7 +61,11 @@ module VagrantPlugins
|
|
70
61
|
|
71
62
|
# Mount the actual folder
|
72
63
|
machine.guest.capability(
|
73
|
-
|
64
|
+
:mount_parallels_shared_folder,
|
65
|
+
data[:plugin].capability(:mount_name, data),
|
66
|
+
data[:guestpath],
|
67
|
+
data
|
68
|
+
)
|
74
69
|
else
|
75
70
|
# If no guest path is specified, then automounting is disabled
|
76
71
|
machine.ui.detail(I18n.t('vagrant.actions.vm.share_folders.nomount_entry',
|
@@ -89,7 +84,7 @@ module VagrantPlugins
|
|
89
84
|
end
|
90
85
|
|
91
86
|
# Remove the shared folders from the VM metadata
|
92
|
-
names = folders.map { |
|
87
|
+
names = folders.map { |_id, data| data[:plugin].capability(:mount_name, data) }
|
93
88
|
driver(machine).unshare_folders(names)
|
94
89
|
end
|
95
90
|
|
@@ -103,11 +98,6 @@ module VagrantPlugins
|
|
103
98
|
def driver(machine)
|
104
99
|
machine.provider.driver
|
105
100
|
end
|
106
|
-
|
107
|
-
def os_friendly_id(id)
|
108
|
-
# Replace chars *, ", :, <, >, ?, |, /, \
|
109
|
-
id.gsub(/[*":<>?|\/\\]/,'_').sub(/^_/, '')
|
110
|
-
end
|
111
101
|
end
|
112
102
|
end
|
113
103
|
end
|