windoo 1.0.1
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 +7 -0
- data/CHANGES.md +9 -0
- data/LICENSE.txt +177 -0
- data/README.md +222 -0
- data/lib/windoo/base_classes/array_manager.rb +335 -0
- data/lib/windoo/base_classes/criteria_manager.rb +327 -0
- data/lib/windoo/base_classes/criterion.rb +226 -0
- data/lib/windoo/base_classes/json_object.rb +472 -0
- data/lib/windoo/configuration.rb +221 -0
- data/lib/windoo/connection/actions.rb +152 -0
- data/lib/windoo/connection/attributes.rb +156 -0
- data/lib/windoo/connection/connect.rb +402 -0
- data/lib/windoo/connection/constants.rb +55 -0
- data/lib/windoo/connection/token.rb +489 -0
- data/lib/windoo/connection.rb +92 -0
- data/lib/windoo/converters.rb +31 -0
- data/lib/windoo/exceptions.rb +34 -0
- data/lib/windoo/mixins/api_collection.rb +408 -0
- data/lib/windoo/mixins/constants.rb +43 -0
- data/lib/windoo/mixins/default_connection.rb +75 -0
- data/lib/windoo/mixins/immutable.rb +34 -0
- data/lib/windoo/mixins/loading.rb +38 -0
- data/lib/windoo/mixins/patch/component.rb +102 -0
- data/lib/windoo/mixins/software_title/extension_attribute.rb +106 -0
- data/lib/windoo/mixins/utility.rb +23 -0
- data/lib/windoo/objects/capability.rb +82 -0
- data/lib/windoo/objects/capability_manager.rb +52 -0
- data/lib/windoo/objects/component.rb +99 -0
- data/lib/windoo/objects/component_criteria_manager.rb +26 -0
- data/lib/windoo/objects/component_criterion.rb +66 -0
- data/lib/windoo/objects/extension_attribute.rb +149 -0
- data/lib/windoo/objects/kill_app.rb +92 -0
- data/lib/windoo/objects/kill_app_manager.rb +89 -0
- data/lib/windoo/objects/patch.rb +235 -0
- data/lib/windoo/objects/patch_manager.rb +240 -0
- data/lib/windoo/objects/requirement.rb +85 -0
- data/lib/windoo/objects/requirement_manager.rb +52 -0
- data/lib/windoo/objects/software_title.rb +407 -0
- data/lib/windoo/validate.rb +548 -0
- data/lib/windoo/version.rb +15 -0
- data/lib/windoo/zeitwerk_config.rb +158 -0
- data/lib/windoo.rb +56 -0
- metadata +141 -0
@@ -0,0 +1,221 @@
|
|
1
|
+
# Copyright 2025 Pixar
|
2
|
+
#
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
4
|
+
# at the root of this project.
|
5
|
+
#
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'singleton'
|
9
|
+
|
10
|
+
module Windoo
|
11
|
+
|
12
|
+
# A class for working with pre-defined settings & preferences for Windoo
|
13
|
+
#
|
14
|
+
# This is a singleton class, only one instance can exist at a time.
|
15
|
+
#
|
16
|
+
# When the module loads, that instance is created, and is used to provide default
|
17
|
+
# values throughout Windoo. It can be accessed via Windoo.config in applications.
|
18
|
+
#
|
19
|
+
# @note Many values in Windoo will also have a hard-coded default, if not defined
|
20
|
+
# in the configuration.
|
21
|
+
#
|
22
|
+
# When the Windoo::Configuration instance is created, the {GLOBAL_CONF} file (/etc/windoo.conf)
|
23
|
+
# is examined if it exists, and the items in it are loaded into the attributes.
|
24
|
+
#
|
25
|
+
# Then the user-specific {USER_CONF} file (~/.windoo.conf) is examined if it exists, and
|
26
|
+
# any attributes defined there will override those values from the {GLOBAL_CONF}.
|
27
|
+
#
|
28
|
+
# The file format is one attribute per line, thus:
|
29
|
+
# attr_name: value
|
30
|
+
#
|
31
|
+
# Lines that don't start with a known attribute name followed by a colon are ignored.
|
32
|
+
# If an attribute is defined more than once, the last one wins.
|
33
|
+
#
|
34
|
+
# See {CONF_KEYS} for the available attributes, and how they are converted to the appropriate
|
35
|
+
# Ruby class when loaded.
|
36
|
+
#
|
37
|
+
# At any point, the attributes can read or changed using standard Ruby getter/setter methods
|
38
|
+
# matching the name of the attribute,
|
39
|
+
# e.g.
|
40
|
+
#
|
41
|
+
# # read the current title_editor_server_name configuration value
|
42
|
+
# Windoo.config.title_editor_server_name # => 'foobar.appcatalog.jamfcloud.com'
|
43
|
+
#
|
44
|
+
# # sets the title_editor_server_name to a new value
|
45
|
+
# Windoo.config.title_editor_server_name = 'baz.appcatalog.jamfcloud.com'
|
46
|
+
#
|
47
|
+
#
|
48
|
+
# The current settings may be saved to the GLOBAL_CONF file, the USER_CONF file, or an arbitrary
|
49
|
+
# file using {#save}. The argument to {#save} should be either :user, :global, or a String or
|
50
|
+
# Pathname file path.
|
51
|
+
# NOTE: This overwrites any existing file with the current values of the Configuration object.
|
52
|
+
#
|
53
|
+
# To re-load the configuration use {#reload}. This clears the current settings, and re-reads
|
54
|
+
# both the global and user files. If a pathname is provided, e.g.
|
55
|
+
# Windoo.config.reload '/path/to/other/file'
|
56
|
+
# the current settings are cleared and reloaded from that other file.
|
57
|
+
#
|
58
|
+
# To view the current settings, use {#print}.
|
59
|
+
#
|
60
|
+
class Configuration
|
61
|
+
|
62
|
+
include Singleton
|
63
|
+
|
64
|
+
# Class Constants
|
65
|
+
#####################################
|
66
|
+
|
67
|
+
# The filename for storing the config, globally or user-level.
|
68
|
+
# The first matching file is used - the array provides
|
69
|
+
# backward compatibility with earlier versions.
|
70
|
+
# Saving will always happen to the first filename
|
71
|
+
CONF_FILENAME = 'windoo.conf'
|
72
|
+
|
73
|
+
# The Pathname to the machine-wide preferences plist
|
74
|
+
GLOBAL_CONF = Pathname.new "/etc/#{CONF_FILENAME}"
|
75
|
+
|
76
|
+
# The Pathname to the user-specific preferences plist
|
77
|
+
USER_CONF = Pathname.new("~/.#{CONF_FILENAME}").expand_path
|
78
|
+
|
79
|
+
# The attribute keys we maintain, and the type they should be stored as
|
80
|
+
CONF_KEYS = {
|
81
|
+
title_editor_server_name: :to_s,
|
82
|
+
title_editor_server_port: :to_i,
|
83
|
+
title_editor_ssl_version: :to_s,
|
84
|
+
title_editor_verify_cert: :to_bool,
|
85
|
+
title_editor_username: :to_s,
|
86
|
+
title_editor_open_timeout: :to_i,
|
87
|
+
title_editor_timeout: :to_i
|
88
|
+
}
|
89
|
+
|
90
|
+
# Attributes
|
91
|
+
#####################################
|
92
|
+
|
93
|
+
# automatically create accessors for all the CONF_KEYS
|
94
|
+
CONF_KEYS.keys.each { |k| attr_accessor k }
|
95
|
+
|
96
|
+
# Constructor
|
97
|
+
#####################################
|
98
|
+
|
99
|
+
# Initialize!
|
100
|
+
#
|
101
|
+
def initialize
|
102
|
+
read GLOBAL_CONF
|
103
|
+
read USER_CONF
|
104
|
+
end
|
105
|
+
|
106
|
+
# Public Instance Methods
|
107
|
+
#####################################
|
108
|
+
|
109
|
+
# Clear all values
|
110
|
+
#
|
111
|
+
# @return [void]
|
112
|
+
#
|
113
|
+
def clear_all
|
114
|
+
CONF_KEYS.keys.each { |k| send "#{k}=", nil }
|
115
|
+
end
|
116
|
+
|
117
|
+
# Clear the settings and reload the prefs files, or another file if provided
|
118
|
+
#
|
119
|
+
# @param file[String,Pathname] a non-standard prefs file to load
|
120
|
+
#
|
121
|
+
# @return [void]
|
122
|
+
#
|
123
|
+
def reload(file = nil)
|
124
|
+
clear_all
|
125
|
+
if file
|
126
|
+
read file
|
127
|
+
return true
|
128
|
+
end
|
129
|
+
read GLOBAL_CONF
|
130
|
+
read USER_CONF
|
131
|
+
true
|
132
|
+
end
|
133
|
+
|
134
|
+
# Save the prefs into a file
|
135
|
+
#
|
136
|
+
# @param file[Symbol,String,Pathname] either :user, :global, or an arbitrary file to save.
|
137
|
+
#
|
138
|
+
# @return [void]
|
139
|
+
#
|
140
|
+
def save(file)
|
141
|
+
path =
|
142
|
+
case file
|
143
|
+
when :global then GLOBAL_CONF
|
144
|
+
when :user then USER_CONF
|
145
|
+
else Pathname.new(file)
|
146
|
+
end
|
147
|
+
|
148
|
+
# file already exists? read it in and update the values.
|
149
|
+
if path.readable?
|
150
|
+
data = path.read
|
151
|
+
|
152
|
+
# go thru the known attributes/keys
|
153
|
+
CONF_KEYS.keys.sort.each do |k|
|
154
|
+
curr_val = send(k)
|
155
|
+
|
156
|
+
# if the key exists, update it.
|
157
|
+
if data =~ /^\s*#{k}:/
|
158
|
+
data.sub!(/^\s*#{k}:.*$/, "#{k}: #{curr_val}")
|
159
|
+
|
160
|
+
# if not, add it to the end unless it's nil
|
161
|
+
else
|
162
|
+
data += "\n#{k}: #{curr_val}" unless curr_val.nil?
|
163
|
+
end # if data =~ /^#{k}:/
|
164
|
+
end # each do |k|
|
165
|
+
|
166
|
+
else # not readable, make a new file
|
167
|
+
data = ''
|
168
|
+
CONF_KEYS.keys.sort.each do |k|
|
169
|
+
data << "#{k}: #{send k}\n" unless send(k).nil?
|
170
|
+
end
|
171
|
+
end # if path readable
|
172
|
+
|
173
|
+
# make sure we end with a newline, the save it.
|
174
|
+
data << "\n" unless data.end_with?("\n")
|
175
|
+
path.x_save data
|
176
|
+
end # read file
|
177
|
+
|
178
|
+
# Print out the current settings to stdout
|
179
|
+
#
|
180
|
+
# @return [void]
|
181
|
+
#
|
182
|
+
def print
|
183
|
+
CONF_KEYS.keys.sort.each { |k| puts "#{k}: #{send k}" }
|
184
|
+
end
|
185
|
+
|
186
|
+
# Private Instance Methods
|
187
|
+
#####################################
|
188
|
+
private
|
189
|
+
|
190
|
+
# Read in any prefs file
|
191
|
+
#
|
192
|
+
# @param file[String,Pathname] the file to read
|
193
|
+
#
|
194
|
+
# @return [void]
|
195
|
+
#
|
196
|
+
def read(file)
|
197
|
+
file = Pathname.new file
|
198
|
+
return unless file.readable?
|
199
|
+
|
200
|
+
file.read.each_line do |line|
|
201
|
+
# skip blank lines and those starting with #
|
202
|
+
next if line =~ /^\s*(#|$)/
|
203
|
+
|
204
|
+
# parse the line
|
205
|
+
next unless line.strip =~ /^\s*(\w+?):\s*(\S.*)$/
|
206
|
+
|
207
|
+
attr = Regexp.last_match(1).to_sym
|
208
|
+
next unless CONF_KEYS.key? attr
|
209
|
+
|
210
|
+
setter = "#{attr}=".to_sym
|
211
|
+
value = Regexp.last_match(2).strip
|
212
|
+
# convert the value to the correct class
|
213
|
+
value &&= value.send(CONF_KEYS[attr])
|
214
|
+
|
215
|
+
send(setter, value)
|
216
|
+
end # do line
|
217
|
+
end # read file
|
218
|
+
|
219
|
+
end # class Configuration
|
220
|
+
|
221
|
+
end # module Windoo
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# Copyright 2025 Pixar
|
2
|
+
#
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
4
|
+
# at the root of this project.
|
5
|
+
#
|
6
|
+
#
|
7
|
+
|
8
|
+
# frozen_string_literal: true
|
9
|
+
|
10
|
+
module Windoo
|
11
|
+
|
12
|
+
class Connection
|
13
|
+
|
14
|
+
# This module defines methods used for interacting with the TitleEditor API.
|
15
|
+
# This includes sending HTTP requests and handling responses
|
16
|
+
module Actions
|
17
|
+
|
18
|
+
def self.included(includer)
|
19
|
+
Windoo.verbose_include(includer, self)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param rsrc[String] the resource path to get
|
23
|
+
#
|
24
|
+
# @return [Object] The parsed JSON from the result of the request
|
25
|
+
#
|
26
|
+
def get(rsrc)
|
27
|
+
validate_connected
|
28
|
+
|
29
|
+
resp = cnx.get(rsrc)
|
30
|
+
@last_http_response = resp
|
31
|
+
return resp.body if resp.success?
|
32
|
+
|
33
|
+
handle_http_error resp
|
34
|
+
end # get
|
35
|
+
|
36
|
+
# Create a new API resource via POST
|
37
|
+
#
|
38
|
+
# @param rsrc[String] the API resource being created, the URL part after 'JSSResource/'
|
39
|
+
#
|
40
|
+
# @param content[String] the content specifying the new object.
|
41
|
+
#
|
42
|
+
# @return [Object] The parsed JSON from the result of the request
|
43
|
+
#
|
44
|
+
def post(rsrc, content)
|
45
|
+
validate_connected
|
46
|
+
|
47
|
+
# send the data
|
48
|
+
resp = cnx.post(rsrc) { |req| req.body = content }
|
49
|
+
@last_http_response = resp
|
50
|
+
return resp.body if resp.success?
|
51
|
+
|
52
|
+
handle_http_error resp
|
53
|
+
end # post
|
54
|
+
|
55
|
+
# Update an existing API resource via PUT
|
56
|
+
#
|
57
|
+
# @param rsrc[String] the API resource being changed, the URL part after 'JSSResource/'
|
58
|
+
#
|
59
|
+
# @param content[String] the content specifying the changes.
|
60
|
+
#
|
61
|
+
# @return [Object] The parsed JSON from the result of the request
|
62
|
+
#
|
63
|
+
def put(rsrc, content)
|
64
|
+
validate_connected
|
65
|
+
|
66
|
+
# send the data
|
67
|
+
resp = cnx.put(rsrc) { |req| req.body = content }
|
68
|
+
@last_http_response = resp
|
69
|
+
return resp.body if resp.success?
|
70
|
+
|
71
|
+
handle_http_error resp
|
72
|
+
end # put
|
73
|
+
|
74
|
+
# Delete a resource from the API
|
75
|
+
#
|
76
|
+
# @param rsrc[String] the resource to delete
|
77
|
+
#
|
78
|
+
# @return [Object] The parsed JSON from the result of the request
|
79
|
+
#
|
80
|
+
def delete(rsrc)
|
81
|
+
validate_connected
|
82
|
+
|
83
|
+
# send the data
|
84
|
+
resp = cnx.delete(rsrc)
|
85
|
+
@last_http_response = resp
|
86
|
+
return resp.body if resp.success?
|
87
|
+
|
88
|
+
handle_http_error resp
|
89
|
+
end # delete_rsrc
|
90
|
+
|
91
|
+
#############################
|
92
|
+
private
|
93
|
+
|
94
|
+
# Parses the given http response
|
95
|
+
# and raises a Jamf::APIError with a useful error message.
|
96
|
+
#
|
97
|
+
# @return [void]
|
98
|
+
#
|
99
|
+
def handle_http_error(resp)
|
100
|
+
return if resp.success?
|
101
|
+
|
102
|
+
case resp.status
|
103
|
+
when 404
|
104
|
+
err = Windoo::NoSuchItemError
|
105
|
+
msg = 'Not Found'
|
106
|
+
|
107
|
+
when 401
|
108
|
+
if resp.body[:errors].find { |e| e[:description] =~ /token not found/i }
|
109
|
+
err = Windoo::InvalidTokenError
|
110
|
+
msg = 'Connection Token is not valid.'
|
111
|
+
|
112
|
+
elsif resp.body[:errors].find { |e| e[:description] =~ /expired token/i }
|
113
|
+
err = Windoo::InvalidTokenError
|
114
|
+
msg = 'Connection Token has expired.'
|
115
|
+
else
|
116
|
+
err = Windoo::PermissionError
|
117
|
+
msg = 'You are not authorized to do that.'
|
118
|
+
end
|
119
|
+
when (500..599)
|
120
|
+
err = Windoo::ConnectionError
|
121
|
+
msg = 'There was an internal server error'
|
122
|
+
|
123
|
+
else
|
124
|
+
err = Windoo::ConnectionError
|
125
|
+
msg = "There was a error processing your request, status: #{resp.status}"
|
126
|
+
|
127
|
+
end # case
|
128
|
+
msg = "#{msg}\nStatus: #{resp.status}\nResponse Body:\n#{parse_http_error_body(resp)}"
|
129
|
+
raise err, msg
|
130
|
+
end
|
131
|
+
|
132
|
+
# get the body of the error response in a readable format
|
133
|
+
def parse_http_error_body(resp)
|
134
|
+
err_text = +''
|
135
|
+
resp.body[:errors].map do |err_hash|
|
136
|
+
err_text << "Code: #{err_hash[:code]}\n"
|
137
|
+
err_hash.each do |k, v|
|
138
|
+
next if k == :code
|
139
|
+
|
140
|
+
err_text << " #{k}: #{v}\n"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
err_text.chomp
|
144
|
+
rescue StandardError
|
145
|
+
resp.body
|
146
|
+
end
|
147
|
+
|
148
|
+
end # module Actions
|
149
|
+
|
150
|
+
end # class Connection
|
151
|
+
|
152
|
+
end # module Windoo
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# Copyright 2025 Pixar
|
2
|
+
#
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
4
|
+
# at the root of this project.
|
5
|
+
|
6
|
+
# frozen_string_literal: true
|
7
|
+
|
8
|
+
module Windoo
|
9
|
+
|
10
|
+
class Connection
|
11
|
+
|
12
|
+
# This module defines general attributes of a connection object
|
13
|
+
#
|
14
|
+
# These attributes actually come from the token:
|
15
|
+
# base_url, host, port, user, keep_alive?, ssl_version,
|
16
|
+
# verify_cert?, pw_fallback
|
17
|
+
#
|
18
|
+
# There are convience getters defined for them below
|
19
|
+
#############################################################
|
20
|
+
module Attributes
|
21
|
+
|
22
|
+
def self.included(includer)
|
23
|
+
Windoo.verbose_include(includer, self)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [String] the name of this connection, an arbitrary string
|
27
|
+
attr_reader :name
|
28
|
+
|
29
|
+
# @return [Integer] Seconds before an http request times out
|
30
|
+
attr_reader :timeout
|
31
|
+
|
32
|
+
# @return [Integer] Seconds before an http connection open times out
|
33
|
+
attr_reader :open_timeout
|
34
|
+
|
35
|
+
# @return [Windoo::Connection::Token] the token used for connecting
|
36
|
+
attr_reader :token
|
37
|
+
|
38
|
+
# @return [Faraday::Response] The response from the most recent API call
|
39
|
+
attr_reader :last_http_response
|
40
|
+
|
41
|
+
# @return [Time] when this connection was connected
|
42
|
+
attr_reader :connect_time
|
43
|
+
alias login_time connect_time
|
44
|
+
|
45
|
+
# @return [Faraday::Connection] The underlying connection object
|
46
|
+
attr_reader :cnx
|
47
|
+
|
48
|
+
# @return [Boolean] are we connected right now?
|
49
|
+
attr_reader :connected
|
50
|
+
alias connected? connected
|
51
|
+
|
52
|
+
# Set the name, but always note if we are the default connection
|
53
|
+
#
|
54
|
+
# Reset the response timeout for the rest connection
|
55
|
+
#
|
56
|
+
# @param timeout[Integer] the new timeout in seconds
|
57
|
+
#
|
58
|
+
# @return [void]
|
59
|
+
#
|
60
|
+
def name=(newname)
|
61
|
+
@name = default? ? "#{newname} (default)" : newname
|
62
|
+
end
|
63
|
+
|
64
|
+
# Reset the response timeout for the rest connection
|
65
|
+
#
|
66
|
+
# @param timeout[Integer] the new timeout in seconds
|
67
|
+
#
|
68
|
+
# @return [void]
|
69
|
+
#
|
70
|
+
def timeout=(new_timeout)
|
71
|
+
validate_connection
|
72
|
+
@timeout = new_timeout.to_i
|
73
|
+
@cnx.options[:timeout] = @timeout if @cnx
|
74
|
+
end
|
75
|
+
|
76
|
+
# Reset the open-connection timeout for the rest connection
|
77
|
+
#
|
78
|
+
# @param timeout[Integer] the new timeout in seconds
|
79
|
+
#
|
80
|
+
# @return [void]
|
81
|
+
#
|
82
|
+
def open_timeout=(new_timeout)
|
83
|
+
validate_connection
|
84
|
+
@open_timeout = new_timeout.to_i
|
85
|
+
@cnx.options[:open_timeout] = @open_timeout if @cnx
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [URI::HTTPS] the base URL to the server
|
89
|
+
def base_url
|
90
|
+
validate_connection
|
91
|
+
@token&.base_url
|
92
|
+
end
|
93
|
+
|
94
|
+
# @return [String] the hostname of the Jamf Pro server API connection
|
95
|
+
def host
|
96
|
+
validate_connection
|
97
|
+
@token&.host
|
98
|
+
end
|
99
|
+
alias server host
|
100
|
+
alias hostname host
|
101
|
+
|
102
|
+
# @return [Integer] The port of the Jamf Pro server API connection
|
103
|
+
def port
|
104
|
+
validate_connection
|
105
|
+
@token&.port
|
106
|
+
end
|
107
|
+
|
108
|
+
# @return [String] the username who's connected to the JSS API
|
109
|
+
def user
|
110
|
+
validate_connection
|
111
|
+
@token&.user
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [Boolean] Is the connection token being automatically refreshed?
|
115
|
+
def keep_alive?
|
116
|
+
validate_connection
|
117
|
+
@token&.keep_alive?
|
118
|
+
end
|
119
|
+
|
120
|
+
# @return [Boolean] If keep_alive is true, is the password Cached in memory
|
121
|
+
# to use if the refresh fails?
|
122
|
+
def pw_fallback?
|
123
|
+
validate_connection
|
124
|
+
@token&.pw_fallback?
|
125
|
+
end
|
126
|
+
|
127
|
+
# @return [String] SSL version used for the connection
|
128
|
+
def ssl_version
|
129
|
+
validate_connection
|
130
|
+
@token&.ssl_version
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [Boolean] Should the SSL certifcate from the server be verified?
|
134
|
+
def verify_cert?
|
135
|
+
validate_connection
|
136
|
+
@token&.verify_cert?
|
137
|
+
end
|
138
|
+
alias verify_cert verify_cert?
|
139
|
+
|
140
|
+
# @return [Hash] the ssl version and verify cert, to pass into faraday connections
|
141
|
+
def ssl_options
|
142
|
+
validate_connection
|
143
|
+
@token&.ssl_options
|
144
|
+
end
|
145
|
+
|
146
|
+
# raise an error if no token yet
|
147
|
+
# @return [void]
|
148
|
+
def validate_connection
|
149
|
+
raise Windoo::NotConnectedError, 'Not connected, use #connect first' unless connected?
|
150
|
+
end
|
151
|
+
|
152
|
+
end # module
|
153
|
+
|
154
|
+
end # class Connection
|
155
|
+
|
156
|
+
end # module Windoo
|