ood_core 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8002c2e879b001e56001b19968b7832083ed9bef
4
- data.tar.gz: 4138c0a93735760e1ebaba489231229879f7b72b
3
+ metadata.gz: bab7efc85cb79ee4fbec84bd148968b5e3e7efa4
4
+ data.tar.gz: 856dbe1ae2f138f409875b6b19e007129d4ece73
5
5
  SHA512:
6
- metadata.gz: e308714bcb389500342e493a914542b9c59b63a0a8776ec4dc6883a3ab7dc1696ef44fdee40ea2c74ae9b94c89d8d9e58336cd6f5cf252b403ba055f755f7841
7
- data.tar.gz: f4b67e63a34642884b5bd6de6ecab47484d377b639fc174eb45c5bc11f4cc993e89105bb3092dd7cf6695d27951618ee4fc8f7b41308e6b3a8be8386243e39d2
6
+ metadata.gz: 6d9b87b7134e3e72fff1191ac24658a5243d71cee4d1942a1f5a946345640b4f6a999a7b662db9924c137234183cbef5eafbd355bbb1fe1ee0249a91b32c3866
7
+ data.tar.gz: a958afc1560a75e358f9655df0342a9831e49a2a4d3658bf74878fc61ae7a135a34248cd7b974dd5bea4190129481bb7f2810bf24a38d01fb12c6a64a74a9641
data/CHANGELOG.md CHANGED
@@ -1,33 +1,76 @@
1
- ## Unreleased
1
+ # Changelog
2
2
 
3
- ## 0.0.4 (2017-05-17)
3
+ All notable changes to this project will be documented in this file.
4
4
 
5
- Features:
5
+ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
+ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
7
 
7
- - removed `OodCore::Job::Script#min_phys_memory` due to lack of commonality
8
- across resource managers
9
- - removed `OodCore::Job::Script#join_files` due to lack of support in
10
- resource managers
11
- - by default all PBS jobs output stdout & stderr to output path unless an
12
- error path is specified (mimics behavior of Slurm and LSF)
8
+ ## [Unreleased]
13
9
 
14
- ## 0.0.3 (2017-04-28)
10
+ ## [0.0.5] - 2017-07-05
15
11
 
16
- Features:
12
+ ### Added
17
13
 
18
- - provide support for slurm conf file
14
+ - Add wallclock time limit to `OodCore::Job::Info` object.
15
+ - Add further support for the LSF adapter.
16
+ - Add a new Batch Connect template feature that builds batch scripts to launch
17
+ web servers.
18
+ - Add support for the PBS Professional resource manager.
19
+ - Add method to filter list of batch jobs for a given owner or owners.
19
20
 
20
- Bugfixes:
21
+ ### Changed
21
22
 
22
- - correct code documentation for `Script#min_phys_memory`
23
- - fix for login feature being allowed on all clusters even if not defined
23
+ - Torque adapter provides nodes/procs info if available for non-running jobs.
24
+ - Slurm adapter provides node info if available for non-running jobs.
25
+ - Changed the `CHANGELOG.md` formatting.
24
26
 
25
- ## 0.0.2 (2017-04-27)
27
+ ### Removed
26
28
 
27
- Features:
29
+ - Remove deprecated tests for the Slurm adapter.
28
30
 
29
- - removed the `OodCore::Job::NodeRequest` object
31
+ ### Fixed
30
32
 
31
- ## 0.0.1 (2017-04-17)
33
+ - Fix parsing bjobs output for LSF 9.1, which has extra SLOTS column.
32
34
 
33
- Initial release!
35
+ ## [0.0.4] - 2017-05-17
36
+
37
+ ### Changed
38
+
39
+ - By default all PBS jobs output stdout & stderr to output path unless an error
40
+ path is specified (mimics behavior of Slurm and LSF)
41
+
42
+ ### Removed
43
+
44
+ - Remove `OodCore::Job::Script#min_phys_memory` due to lack of commonality
45
+ across resource managers.
46
+ - Remove `OodCore::Job::Script#join_files` due to lack of support in resource
47
+ managers.
48
+
49
+ ## [0.0.3] - 2017-04-28
50
+
51
+ ### Added
52
+
53
+ - Provide support for Slurm conf file.
54
+
55
+ ### Fixed
56
+
57
+ - Correct code documentation for `Script#min_phys_memory`.
58
+ - Add fix for login feature being allowed on all clusters even if not defined.
59
+
60
+ ## [0.0.2] - 2017-04-27
61
+
62
+ ### Removed
63
+
64
+ - Remove the `OodCore::Job::NodeRequest` object.
65
+
66
+ ## 0.0.1 - 2017-04-17
67
+
68
+ ### Added
69
+
70
+ - Initial release!
71
+
72
+ [Unreleased]: https://github.com/OSC/ood_core/compare/v0.0.5...HEAD
73
+ [0.0.5]: https://github.com/OSC/ood_core/compare/v0.0.4...v0.0.5
74
+ [0.0.4]: https://github.com/OSC/ood_core/compare/v0.0.3...v0.0.4
75
+ [0.0.3]: https://github.com/OSC/ood_core/compare/v0.0.2...v0.0.3
76
+ [0.0.2]: https://github.com/OSC/ood_core/compare/v0.0.1...v0.0.2
data/lib/ood_core.rb CHANGED
@@ -30,4 +30,10 @@ module OodCore
30
30
  module Adapters
31
31
  end
32
32
  end
33
+
34
+ # A namespace for batch connect code
35
+ module BatchConnect
36
+ require "ood_core/batch_connect/template"
37
+ require "ood_core/batch_connect/factory"
38
+ end
33
39
  end
@@ -0,0 +1,42 @@
1
+ require "ood_core/refinements/hash_extensions"
2
+
3
+ module OodCore
4
+ module BatchConnect
5
+ # A factory that builds a batch connect template object from a
6
+ # configuration.
7
+ class Factory
8
+ using Refinements::HashExtensions
9
+
10
+ class << self
11
+ # Build a batch connect template from a configuration
12
+ # @param config [#to_h] configuration describing batch connect template
13
+ # @option config [#to_s] :template The batch connect template to use
14
+ # @raise [TemplateNotSpecified] if no template is specified
15
+ # @raise [TemplateNotFound] if the specified template does not exist
16
+ # @return [Template] the batch connect template object
17
+ def build(config)
18
+ c = config.to_h.symbolize_keys
19
+
20
+ template = c.fetch(:template) { raise TemplateNotSpecified, "batch connect configuration does not specify template" }.to_s
21
+
22
+ path_to_template = "ood_core/batch_connect/templates/#{template}"
23
+ begin
24
+ require path_to_template
25
+ rescue Gem::LoadError => e
26
+ raise Gem::LoadError, "Specified '#{template}' for batch connect template, but the gem is not loaded."
27
+ rescue LoadError => e
28
+ raise LoadError, "Could not load '#{template}'. Make sure that that batch connect template in the configuration file is valid."
29
+ end
30
+
31
+ template_method = "build_#{template}"
32
+
33
+ unless respond_to?(template_method)
34
+ raise TemplateNotFound, "batch connect configuration specifies nonexistent #{template} template"
35
+ end
36
+
37
+ send(template_method, c)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,207 @@
1
+ require "ood_core/refinements/hash_extensions"
2
+
3
+ module OodCore
4
+ module BatchConnect
5
+ # A template class that renders a batch script designed to facilitate
6
+ # external connections to the running job
7
+ class Template
8
+ using Refinements::HashExtensions
9
+ using Refinements::ArrayExtensions
10
+
11
+ # The context used to render this template
12
+ # @return [Hash] context hash
13
+ attr_reader :context
14
+
15
+ # @param context [#to_h] the context used to render the template
16
+ # @option context [#to_s] :work_dir Working directory for batch script
17
+ # @option context [#to_s] :conn_file ("connection.yml") The file that
18
+ # holds connection information
19
+ # @option context [#to_sym, Array<#to_sym>] :conn_params ([]) A list of
20
+ # connection parameters added to the connection file (`:host`, `:port`,
21
+ # and `:password` will always exist)
22
+ # @option context [#to_s] :bash_helpers ("...") Bash helper methods
23
+ # @option context [#to_i] :min_port (2000) Minimum port used when looking
24
+ # for available port
25
+ # @option context [#to_i] :max_port (65535) Maximum port used when
26
+ # looking for available port
27
+ # @option context [#to_i] :passwd_size (32) Length of randomly generated
28
+ # password
29
+ # @option context [#to_s] :script_wrapper ("%s") Bash code that wraps
30
+ # around the body of the template script (use `%s` to interpolate the
31
+ # body)
32
+ # @option context [#to_s] :before_script ("...") Bash code run before the
33
+ # main script is forked off
34
+ # @option context [#to_s] :before_file ("before.sh") Path to script that
35
+ # is sourced before main script is forked (assumes you don't modify
36
+ # `:before_script`)
37
+ # @option context [#to_s] :run_script ("...") Bash code that is forked
38
+ # off and treated as the main script
39
+ # @option context [#to_s] :script_file ("./script.sh") Path to script
40
+ # that is forked as the main scripta (assumes you don't modify
41
+ # `:run_script`)
42
+ # @option context [#to_s] :timeout ("") Timeout the main script in
43
+ # seconds, if empty then let script run for full walltime (assumes you
44
+ # don't modify `:run_script`)
45
+ # @option context [#to_s] :clean_script ("...") Bash code run during
46
+ # clean up after job finishes
47
+ # @option context [#to_s] :clean_file ("clean.sh") Path to script that is
48
+ # sourced during clean up (assumes you don't modify `:clean_script`)
49
+ def initialize(context = {})
50
+ @context = context.to_h.compact.symbolize_keys
51
+ raise ArgumentError, "No work_dir specified. Missing argument: work_dir" unless context.include?(:work_dir)
52
+ end
53
+
54
+ # Render this template as string
55
+ # @return [String] rendered template
56
+ def to_s
57
+ <<-EOT.gsub(/^ {10}/, '')
58
+ #!/bin/bash
59
+
60
+ #{script_wrapper}
61
+ EOT
62
+ end
63
+
64
+ private
65
+ # Working directory that batch script runs in
66
+ def work_dir
67
+ context.fetch(:work_dir).to_s
68
+ end
69
+
70
+ # The file that holds the connection information in yaml format
71
+ def conn_file
72
+ context.fetch(:conn_file, "connection.yml").to_s
73
+ end
74
+
75
+ # The parameters to include in the connection file
76
+ def conn_params
77
+ conn_params = Array.wrap(context.fetch(:conn_params, [])).map(&:to_sym)
78
+ (conn_params + [:host, :port, :password]).uniq
79
+ end
80
+
81
+ # Helper methods used in the bash scripts
82
+ def bash_helpers
83
+ context.fetch(:bash_helpers) do
84
+ min_port = context.fetch(:min_port, 2000).to_i
85
+ max_port = context.fetch(:max_port, 65535).to_i
86
+ passwd_size = context.fetch(:passwd_size, 32).to_i
87
+
88
+ <<-EOT.gsub(/^ {14}/, '')
89
+ # Generate random integer in range [$1..$2]
90
+ function random () {
91
+ shuf -i ${1}-${2} -n 1
92
+ }
93
+
94
+ # Check if port $1 is in use
95
+ function used_port () {
96
+ local PORT=${1}
97
+ nc -z localhost ${PORT} &>/dev/null
98
+ }
99
+
100
+ # Find available port in range [$1..$2]
101
+ # Default: [#{min_port}..#{max_port}]
102
+ function find_port () {
103
+ local PORT=$(random ${1:-#{min_port}} ${2:-#{max_port}})
104
+ while $(used_port ${PORT}); do
105
+ PORT=$(random ${1:-#{min_port}} ${2:-#{max_port}})
106
+ done
107
+ echo ${PORT}
108
+ }
109
+
110
+ # Generate random alphanumeric password with $1 (default: #{passwd_size}) characters
111
+ function create_passwd () {
112
+ tr -cd '[:alnum:]' < /dev/urandom 2>/dev/null | head -c${1:-#{passwd_size}}
113
+ }
114
+ EOT
115
+ end.to_s
116
+ end
117
+
118
+ # Bash code that wraps around the body of the template script (use `%s`
119
+ # to interpolate the body)
120
+ def script_wrapper
121
+ context.fetch(:script_wrapper, "%s").to_s % base_script
122
+ end
123
+
124
+ # Source in a developer defined script before running the main script
125
+ def before_script
126
+ context.fetch(:before_script) do
127
+ before_file = context.fetch(:before_file, "before.sh").to_s
128
+
129
+ "host=$(hostname)\n[[ -e \"#{before_file}\" ]] && source \"#{before_file}\""
130
+ end.to_s
131
+ end
132
+
133
+ # Fork off a developer defined main script and possibly time it out after
134
+ # a period of time
135
+ def run_script
136
+ context.fetch(:run_script) do
137
+ script_file = context.fetch(:script_file, "./script.sh").to_s
138
+ timeout = context.fetch(:timeout, "").to_s
139
+
140
+ timeout.empty? ? "\"#{script_file}\"" : "timeout #{timeout} \"#{script_file}\""
141
+ end.to_s
142
+ end
143
+
144
+ # Source in a developer defined script after running the main script
145
+ def after_script
146
+ context.fetch(:after_script) do
147
+ after_file = context.fetch(:after_file, "after.sh").to_s
148
+
149
+ "[[ -e \"#{after_file}\" ]] && source \"#{after_file}\""
150
+ end.to_s
151
+ end
152
+
153
+ # Source in a developer defined clean up script that is run during the
154
+ # clean up stage
155
+ def clean_script
156
+ context.fetch(:clean_script) do
157
+ clean_file = context.fetch(:clean_file, "clean.sh").to_s
158
+
159
+ "[[ -e \"#{clean_file}\" ]] && source \"#{clean_file}\""
160
+ end.to_s
161
+ end
162
+
163
+ # The base script template
164
+ def base_script
165
+ <<-EOT.gsub(/^ {12}/, '')
166
+ cd #{work_dir}
167
+
168
+ # Generate a connection yaml file with given parameters
169
+ function create_yml () {
170
+ echo "Generating connection YAML file..."
171
+ (
172
+ umask 077
173
+ echo -e "#{conn_params.map { |p| "#{p}: $#{p}" }.join('\n')}" > "#{conn_file}"
174
+ )
175
+ }
176
+
177
+ # Cleanliness is next to Godliness
178
+ function clean_up () {
179
+ echo "Cleaning up..."
180
+ #{clean_script.gsub(/\n(?=[^\s])/, "\n ")}
181
+ pkill -P $$
182
+ exit ${1:-0}
183
+ }
184
+
185
+ #{bash_helpers}
186
+
187
+ #{before_script}
188
+
189
+ echo "Script starting..."
190
+ #{run_script} &
191
+ SCRIPT_PID=$!
192
+
193
+ #{after_script}
194
+
195
+ # Create the connection yaml file
196
+ create_yml
197
+
198
+ # Wait for script process to finish
199
+ wait ${SCRIPT_PID} || clean_up 1
200
+
201
+ # Exit cleanly
202
+ clean_up
203
+ EOT
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,23 @@
1
+ require "ood_core/refinements/hash_extensions"
2
+
3
+ module OodCore
4
+ module BatchConnect
5
+ class Factory
6
+ using Refinements::HashExtensions
7
+
8
+ # Build the basic template from a configuration
9
+ # @param config [#to_h] the configuration for the batch connect template
10
+ def self.build_basic(config)
11
+ context = config.to_h.symbolize_keys.reject { |k, _| k == :template }
12
+ Templates::Basic.new(context)
13
+ end
14
+ end
15
+
16
+ module Templates
17
+ # A batch connect template that expects to start up a basic web server
18
+ # within a batch job
19
+ class Basic < Template
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,201 @@
1
+ require "ood_core/refinements/hash_extensions"
2
+
3
+ module OodCore
4
+ module BatchConnect
5
+ class Factory
6
+ using Refinements::HashExtensions
7
+
8
+ # Build the VNC template from a configuration
9
+ # @param config [#to_h] the configuration for the batch connect template
10
+ def self.build_vnc(config)
11
+ context = config.to_h.symbolize_keys.reject { |k, _| k == :template }
12
+ Templates::VNC.new(context)
13
+ end
14
+ end
15
+
16
+ module Templates
17
+ # A batch connect template that starts up a VNC server within a batch job
18
+ class VNC < Template
19
+ # @param context [#to_h] the context used to render the template
20
+ # @option context [#to_sym, Array<#to_sym>] :conn_params ([]) A list of
21
+ # connection parameters added to the connection file (`:host`,
22
+ # `:port`, `:password`, `:spassword`, `:display` and `:websocket`
23
+ # will always exist)
24
+ # @option context [#to_s] :websockify_cmd
25
+ # ("${WEBSOCKIFY_CMD:-/opt/websockify/run}") the path to the
26
+ # websockify script (assumes you don't modify `:after_script`)
27
+ # @option context [#to_s] :vnc_log ("vnc.log") path to vnc server log
28
+ # file (assumes you don't modify `:before_script` or `:after_script`)
29
+ # @option context [#to_s] :vnc_passwd ("vnc.passwd") path to the file
30
+ # generated that contains the encrypted vnc password (assumes you
31
+ # don't modify `:before_script`)
32
+ # @option context [#to_s] :vnc_args arguments used when starting up the
33
+ # vnc server (overrides any specific vnc argument) (assumes you don't
34
+ # modify `:before_script`)
35
+ # @option context [#to_s] :name ("") name of the vnc server session
36
+ # (not set if blank or `:vnc_args` is set) (assumes you don't modify
37
+ # `:before_script`)
38
+ # @option context [#to_s] :geometry ("") resolution of vnc display (not
39
+ # set if blank or `:vnc_args` is set) (assumes you don't modify
40
+ # `:before_script`)
41
+ # @option context [#to_s] :dpi ("") dpi of vnc display (not set if
42
+ # blank or `:vnc_args` is set) (assumes you don't modify
43
+ # `:before_script`)
44
+ # @option context [#to_s] :fonts ("") command delimited list of fonts
45
+ # available in vnc display (not set if blank or `:vnc_args` is set)
46
+ # (assumes you don't modify `:before_script`)
47
+ # @option context [#to_s] :idle ("") timeout vnc server if no
48
+ # connection in this amount of time in seconds (not set if blank or
49
+ # `:vnc_args` is set) (assumes you don't modify `:before_script`)
50
+ # @option context [#to_s] :extra_args ("") any extra arguments used
51
+ # when initializing the vnc server process (not set if blank or
52
+ # `:vnc_args` is set) (assumes you don't modify `:before_script`)
53
+ # @option context [#to_s] :vnc_clean ("...") script used to clean up
54
+ # any active vnc sessions (assumes you don't modify `:before_script`
55
+ # or `:clean_script`)
56
+ # @see Template
57
+ def initialize(context = {})
58
+ super
59
+ end
60
+
61
+ private
62
+ # We need to know the VNC and websockify connection information
63
+ def conn_params
64
+ (super + [:display, :websocket, :spassword]).uniq
65
+ end
66
+
67
+ # Before running the main script, start up a VNC server and record
68
+ # the connection information
69
+ def before_script
70
+ <<-EOT.gsub(/^ {14}/, "")
71
+ # Setup one-time use passwords and initialize the VNC password
72
+ function change_passwd () {
73
+ echo "Setting VNC password..."
74
+ password=$(create_passwd 8)
75
+ spassword=${spassword:-$(create_passwd 8)}
76
+ (
77
+ umask 077
78
+ echo -ne "${password}\\n${spassword}" | vncpasswd -f > "#{vnc_passwd}"
79
+ )
80
+ }
81
+ change_passwd
82
+
83
+ # Start up vnc server (if at first you don't succeed, try, try again)
84
+ echo "Starting VNC server..."
85
+ for i in $(seq 1 10); do
86
+ # Clean up any old VNC sessions that weren't cleaned before
87
+ #{vnc_clean}
88
+
89
+ # Attempt to start VNC server
90
+ VNC_OUT=$(vncserver -log "#{vnc_log}" -rfbauth "#{vnc_passwd}" -nohttpd -noxstartup #{vnc_args} 2>&1)
91
+ VNC_PID=$(pgrep -s 0 Xvnc) # the script above will daemonize the Xvnc process
92
+ echo "${VNC_OUT}"
93
+
94
+ # Sometimes Xvnc hangs if it fails to find working disaply, we
95
+ # should kill it and try again
96
+ kill -0 ${VNC_PID} 2>/dev/null && [[ "${VNC_OUT}" =~ "Fatal server error" ]] && kill -TERM ${VNC_PID}
97
+
98
+ # Check that Xvnc process is running, if not assume it died and
99
+ # wait some random period of time before restarting
100
+ kill -0 ${VNC_PID} 2>/dev/null || sleep 0.$(random 1 9)s
101
+
102
+ # If running, then all is well and break out of loop
103
+ kill -0 ${VNC_PID} 2>/dev/null && break
104
+ done
105
+
106
+ # If we fail to start it after so many tries, then just give up
107
+ kill -0 ${VNC_PID} 2>/dev/null || clean_up 1
108
+
109
+ # Parse output for ports used
110
+ display=$(echo "${VNC_OUT}" | awk -F':' '/^Desktop/{print $NF}')
111
+ port=$((5900+display))
112
+
113
+ echo "Successfully started VNC server on ${host}:${port}..."
114
+
115
+ #{super}
116
+ EOT
117
+ end
118
+
119
+ # Run the script under the VNC server's display
120
+ def run_script
121
+ %(DISPLAY=:${display} #{super})
122
+ end
123
+
124
+ # After startup the main script, scan the VNC server log file for
125
+ # successful connections so that the password can be reset
126
+ def after_script
127
+ websockify_cmd = context.fetch(:websockify_cmd, "${WEBSOCKIFY_CMD:-/opt/websockify/run}").to_s
128
+
129
+ <<-EOT.gsub(/^ {14}/, "")
130
+ #{super}
131
+
132
+ # Launch websockify websocket server
133
+ echo "Starting websocket server..."
134
+ websocket=$(find_port)
135
+ #{websockify_cmd} -D ${websocket} localhost:${port}
136
+
137
+ # Set up background process that scans the log file for successful
138
+ # connections by users, and change the password after every
139
+ # connection
140
+ echo "Scanning VNC log file for user authentications..."
141
+ while read -r line; do
142
+ if [[ ${line} =~ "Full-control authentication enabled for" ]]; then
143
+ change_passwd
144
+ create_yml
145
+ fi
146
+ done < <(tail -f --pid=${SCRIPT_PID} "#{vnc_log}") &
147
+ EOT
148
+ end
149
+
150
+ # Clean up the running VNC server and any other stale VNC servers
151
+ def clean_script
152
+ <<-EOT.gsub(/^ {14}/, "")
153
+ #{super}
154
+
155
+ #{vnc_clean}
156
+ [[ -n ${display} ]] && vncserver -kill :${display}
157
+ EOT
158
+ end
159
+
160
+ # Log file for VNC server
161
+ def vnc_log
162
+ context.fetch(:vnc_log, "vnc.log").to_s
163
+ end
164
+
165
+ # Password file for VNC server
166
+ def vnc_passwd
167
+ context.fetch(:vnc_passwd, "vnc.passwd").to_s
168
+ end
169
+
170
+ # Arguments sent to `vncserver` command
171
+ def vnc_args
172
+ context.fetch(:vnc_args) do
173
+ name = context.fetch(:name, "").to_s
174
+ geometry = context.fetch(:geometry, "").to_s
175
+ dpi = context.fetch(:dpi, "").to_s
176
+ fonts = context.fetch(:fonts, "").to_s
177
+ idle = context.fetch(:idle, "").to_s
178
+ extra_args = context.fetch(:extra_args, "").to_s
179
+
180
+ args = []
181
+ args << "-name #{name}" unless name.empty?
182
+ args << "-geometry #{geometry}" unless geometry.empty?
183
+ args << "-dpi #{dpi}" unless dpi.empty?
184
+ args << "-fp #{fonts}" unless fonts.empty?
185
+ args << "-idletimeout #{idle}" unless idle.empty?
186
+ args << extra_args
187
+
188
+ args.join(" ")
189
+ end.to_s
190
+ end
191
+
192
+ # Clean up any stale VNC sessions
193
+ def vnc_clean
194
+ context.fetch(:vnc_clean) do
195
+ %(vncserver -list | awk '/^:/{system("kill -0 "$2" 2>/dev/null || vncserver -kill "$1)}')
196
+ end.to_s
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end