schwab_rb 0.3.2 → 0.3.3

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +193 -193
  3. data/.rubocop.yml +2 -0
  4. data/examples/fetch_account_numbers.rb +1 -1
  5. data/examples/fetch_user_preferences.rb +1 -1
  6. data/lib/schwab_rb/auth/auth_context.rb +19 -15
  7. data/lib/schwab_rb/auth/init_client_easy.rb +36 -32
  8. data/lib/schwab_rb/auth/init_client_login.rb +165 -161
  9. data/lib/schwab_rb/auth/init_client_token_file.rb +27 -23
  10. data/lib/schwab_rb/auth/login_flow_server.rb +39 -36
  11. data/lib/schwab_rb/auth/token.rb +25 -21
  12. data/lib/schwab_rb/auth/token_manager.rb +88 -84
  13. data/lib/schwab_rb/clients/async_client.rb +2 -0
  14. data/lib/schwab_rb/clients/base_client.rb +2 -0
  15. data/lib/schwab_rb/configuration.rb +2 -0
  16. data/lib/schwab_rb/constants.rb +6 -4
  17. data/lib/schwab_rb/data_objects/account_numbers.rb +1 -0
  18. data/lib/schwab_rb/data_objects/market_hours.rb +1 -1
  19. data/lib/schwab_rb/data_objects/market_movers.rb +1 -0
  20. data/lib/schwab_rb/data_objects/option_expiration_chain.rb +1 -1
  21. data/lib/schwab_rb/data_objects/order_preview.rb +21 -49
  22. data/lib/schwab_rb/data_objects/price_history.rb +1 -1
  23. data/lib/schwab_rb/orders/builder.rb +162 -158
  24. data/lib/schwab_rb/orders/destination.rb +2 -0
  25. data/lib/schwab_rb/orders/duration.rb +14 -8
  26. data/lib/schwab_rb/orders/equity_instructions.rb +2 -0
  27. data/lib/schwab_rb/orders/errors.rb +2 -0
  28. data/lib/schwab_rb/orders/instruments.rb +2 -0
  29. data/lib/schwab_rb/orders/option_instructions.rb +2 -0
  30. data/lib/schwab_rb/orders/order.rb +2 -0
  31. data/lib/schwab_rb/orders/price_link_basis.rb +2 -0
  32. data/lib/schwab_rb/orders/price_link_type.rb +2 -0
  33. data/lib/schwab_rb/orders/session.rb +16 -10
  34. data/lib/schwab_rb/orders/special_instruction.rb +2 -0
  35. data/lib/schwab_rb/orders/stop_price_link_basis.rb +2 -0
  36. data/lib/schwab_rb/orders/stop_price_link_type.rb +2 -0
  37. data/lib/schwab_rb/orders/stop_type.rb +2 -0
  38. data/lib/schwab_rb/orders/tax_lot_method.rb +2 -0
  39. data/lib/schwab_rb/utils/enum_enforcer.rb +3 -4
  40. data/lib/schwab_rb/utils/logger.rb +3 -1
  41. data/lib/schwab_rb/utils/redactor.rb +3 -5
  42. data/lib/schwab_rb/version.rb +1 -1
  43. metadata +2 -2
@@ -1,23 +1,27 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "oauth2"
2
4
 
3
- module SchwabRb::Auth
4
- class AuthContext
5
- class << self
6
- def build(oauth_client, callback_url, authorization_url, state: nil)
7
- auth_params = { redirect_uri: callback_url }
8
- auth_params[:state] = state if state
9
- authorization_url = oauth_client.auth_code.authorize_url(auth_params)
5
+ module SchwabRb
6
+ module Auth
7
+ class AuthContext
8
+ class << self
9
+ def build(oauth_client, callback_url, authorization_url, state: nil)
10
+ auth_params = { redirect_uri: callback_url }
11
+ auth_params[:state] = state if state
12
+ authorization_url = oauth_client.auth_code.authorize_url(auth_params)
10
13
 
11
- new(callback_url, authorization_url, state)
14
+ new(callback_url, authorization_url, state)
15
+ end
12
16
  end
13
- end
14
17
 
15
- def initialize(callback_url, authorization_url, state)
16
- @callback_url = callback_url
17
- @authorization_url = authorization_url
18
- @state = state
19
- end
18
+ def initialize(callback_url, authorization_url, state)
19
+ @callback_url = callback_url
20
+ @authorization_url = authorization_url
21
+ @state = state
22
+ end
20
23
 
21
- attr_reader :callback_url, :authorization_url, :state
24
+ attr_reader :callback_url, :authorization_url, :state
25
+ end
22
26
  end
23
27
  end
@@ -1,43 +1,47 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "oauth2"
2
4
  require_relative "init_client_token_file"
3
5
  require_relative "init_client_login"
4
6
 
5
- module SchwabRb::Auth
6
- def self.init_client_easy(
7
- api_key,
8
- app_secret,
9
- callback_url,
10
- token_path,
11
- asyncio: false,
12
- enforce_enums: false,
13
- callback_timeout: 300.0,
14
- interactive: true,
15
- requested_browser: nil
16
- )
17
-
18
- raise OAuth2::Error.new("No token found") unless File.exist?(token_path)
19
-
20
- client = SchwabRb::Auth.init_client_token_file(
21
- api_key,
22
- app_secret,
23
- token_path,
24
- enforce_enums: enforce_enums
25
- )
26
- client.refresh! if client.session.expired?
27
- raise OAuth2::Error.new("Token expired") if client.session.expired?
28
-
29
- client
30
- rescue StandardError
31
- SchwabRb::Auth.init_client_login(
7
+ module SchwabRb
8
+ module Auth
9
+ def self.init_client_easy(
32
10
  api_key,
33
11
  app_secret,
34
12
  callback_url,
35
13
  token_path,
36
- asyncio: asyncio,
37
- enforce_enums: enforce_enums,
38
- callback_timeout: callback_timeout,
39
- interactive: interactive,
40
- requested_browser: requested_browser
14
+ asyncio: false,
15
+ enforce_enums: false,
16
+ callback_timeout: 300.0,
17
+ interactive: true,
18
+ requested_browser: nil
41
19
  )
20
+
21
+ raise OAuth2::Error, "No token found" unless File.exist?(token_path)
22
+
23
+ client = SchwabRb::Auth.init_client_token_file(
24
+ api_key,
25
+ app_secret,
26
+ token_path,
27
+ enforce_enums: enforce_enums
28
+ )
29
+ client.refresh! if client.session.expired?
30
+ raise OAuth2::Error, "Token expired" if client.session.expired?
31
+
32
+ client
33
+ rescue StandardError
34
+ SchwabRb::Auth.init_client_login(
35
+ api_key,
36
+ app_secret,
37
+ callback_url,
38
+ token_path,
39
+ asyncio: asyncio,
40
+ enforce_enums: enforce_enums,
41
+ callback_timeout: callback_timeout,
42
+ interactive: interactive,
43
+ requested_browser: requested_browser
44
+ )
45
+ end
42
46
  end
43
47
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "openssl"
2
4
  require "uri"
3
5
  require "net/http"
@@ -5,200 +7,202 @@ require "json"
5
7
  require "oauth2"
6
8
  # require 'logger'
7
9
 
8
- module SchwabRb::Auth
9
- class RedirectTimeoutError < StandardError
10
- def initialize(msg = "Timed out waiting for a callback")
11
- super
10
+ module SchwabRb
11
+ module Auth
12
+ class RedirectTimeoutError < StandardError
13
+ def initialize(msg = "Timed out waiting for a callback")
14
+ super
15
+ end
12
16
  end
13
- end
14
17
 
15
- class RedirectServerExitedError < StandardError; end
16
-
17
- # class TokenExchangeError < StandardError
18
- # def initialize(msg)
19
- # super(msg)
20
- # end
21
- # end
22
- class InvalidHostname < ArgumentError
23
- def initialize(hostname)
24
- msg = "Disallowed hostname #{hostname}. init_client_login only allows callback URLs with hostname 127.0.0.1."
25
- super(msg)
18
+ class RedirectServerExitedError < StandardError; end
19
+
20
+ # class TokenExchangeError < StandardError
21
+ # def initialize(msg)
22
+ # super(msg)
23
+ # end
24
+ # end
25
+ class InvalidHostname < ArgumentError
26
+ def initialize(hostname)
27
+ msg = "Disallowed hostname #{hostname}. init_client_login only allows callback URLs with hostname 127.0.0.1."
28
+ super(msg)
29
+ end
26
30
  end
27
- end
28
31
 
29
- def self.init_client_login(
30
- api_key,
31
- app_secret,
32
- callback_url,
33
- token_path,
34
- asyncio: false,
35
- enforce_enums: false,
36
- callback_timeout: 300.0,
37
- interactive: true,
38
- requested_browser: nil
39
- )
40
-
41
- callback_timeout = if !callback_timeout
42
- 0
43
- elsif callback_timeout < 0
44
- raise ArgumentError, "callback_timeout must be non-negative"
45
- else
46
- callback_timeout
47
- end
48
-
49
- parsed = URI.parse(callback_url)
50
- raise InvalidHostname.new(parsed.host) unless parsed.host == "127.0.0.1"
51
-
52
- callback_port = parsed.port || 4567
53
- callback_path = parsed.path.empty? ? "/" : parsed.path
54
-
55
- cert_file, key_file = create_ssl_certificate
56
-
57
- SchwabRb::Auth::LoginFlowServer.run_in_thread(
58
- callback_port: callback_port,
59
- callback_path: callback_path,
60
- cert_file: cert_file,
61
- key_file: key_file
32
+ def self.init_client_login(
33
+ api_key,
34
+ app_secret,
35
+ callback_url,
36
+ token_path,
37
+ asyncio: false,
38
+ enforce_enums: false,
39
+ callback_timeout: 300.0,
40
+ interactive: true,
41
+ requested_browser: nil
62
42
  )
63
43
 
64
- begin
65
- # NOTE: wait for server to start
66
- start_time = Time.now
67
- while true
68
- begin
69
- uri = URI("https://127.0.0.1:#{callback_port}/status")
70
- http = Net::HTTP.new(uri.host, uri.port)
71
- http.use_ssl = true
72
- http.ca_file = cert_file.path
44
+ callback_timeout = if !callback_timeout
45
+ 0
46
+ elsif callback_timeout.negative?
47
+ raise ArgumentError, "callback_timeout must be non-negative"
48
+ else
49
+ callback_timeout
50
+ end
73
51
 
74
- http.set_debug_output($stdout)
52
+ parsed = URI.parse(callback_url)
53
+ raise InvalidHostname, parsed.host unless parsed.host == "127.0.0.1"
75
54
 
76
- resp = http.get(uri.path)
55
+ callback_port = parsed.port || 4567
56
+ callback_path = parsed.path.empty? ? "/" : parsed.path
77
57
 
78
- break if resp.is_a?(Net::HTTPSuccess)
79
- rescue Errno::ECONNREFUSED
80
- sleep 0.1
81
- end
58
+ cert_file, key_file = create_ssl_certificate
82
59
 
83
- raise RedirectServerExitedError if Time.now - start_time > 5
84
- end
60
+ SchwabRb::Auth::LoginFlowServer.run_in_thread(
61
+ callback_port: callback_port,
62
+ callback_path: callback_path,
63
+ cert_file: cert_file,
64
+ key_file: key_file
65
+ )
85
66
 
86
- auth_context = build_auth_context(api_key, callback_url)
67
+ begin
68
+ # NOTE: wait for server to start
69
+ start_time = Time.now
70
+ loop do
71
+ begin
72
+ uri = URI("https://127.0.0.1:#{callback_port}/status")
73
+ http = Net::HTTP.new(uri.host, uri.port)
74
+ http.use_ssl = true
75
+ http.ca_file = cert_file.path
87
76
 
88
- puts <<~MESSAGE
89
- ***********************************************************************
90
- Open this URL in your browser to log in:
91
- #{auth_context.authorization_url}
92
- ***********************************************************************
93
- MESSAGE
77
+ http.set_debug_output($stdout)
94
78
 
95
- if interactive
96
- puts "Press ENTER to open the browser..."
97
- gets
98
- end
79
+ resp = http.get(uri.path)
99
80
 
100
- `open "#{auth_context.authorization_url}}"`
81
+ break if resp.is_a?(Net::HTTPSuccess)
82
+ rescue Errno::ECONNREFUSED
83
+ sleep 0.1
84
+ end
101
85
 
102
- timeout_time = Time.now + callback_timeout
103
- received_url = nil
86
+ raise RedirectServerExitedError if Time.now - start_time > 5
87
+ end
104
88
 
105
- while Time.now < timeout_time
106
- unless LoginFlowServer.queue.empty?
107
- received_url = LoginFlowServer.queue.pop
108
- break
89
+ auth_context = build_auth_context(api_key, callback_url)
90
+
91
+ puts <<~MESSAGE
92
+ ***********************************************************************
93
+ Open this URL in your browser to log in:
94
+ #{auth_context.authorization_url}
95
+ ***********************************************************************
96
+ MESSAGE
97
+
98
+ if interactive
99
+ puts "Press ENTER to open the browser..."
100
+ gets
109
101
  end
110
- sleep 0.1
111
- end
112
102
 
113
- raise RedirectTimeoutError.new unless received_url
103
+ `open "#{auth_context.authorization_url}}"`
114
104
 
115
- client_from_received_url(
116
- api_key,
117
- app_secret,
118
- auth_context,
119
- received_url,
120
- token_path
121
- )
122
- ensure
123
- LoginFlowServer.stop
105
+ timeout_time = Time.now + callback_timeout
106
+ received_url = nil
107
+
108
+ while Time.now < timeout_time
109
+ unless LoginFlowServer.queue.empty?
110
+ received_url = LoginFlowServer.queue.pop
111
+ break
112
+ end
113
+ sleep 0.1
114
+ end
115
+
116
+ raise RedirectTimeoutError unless received_url
117
+
118
+ client_from_received_url(
119
+ api_key,
120
+ app_secret,
121
+ auth_context,
122
+ received_url,
123
+ token_path
124
+ )
125
+ ensure
126
+ LoginFlowServer.stop
127
+ end
124
128
  end
125
- end
126
129
 
127
- def self.create_ssl_certificate
128
- key = OpenSSL::PKey::RSA.new(2048)
129
- cert = OpenSSL::X509::Certificate.new
130
+ def self.create_ssl_certificate
131
+ key = OpenSSL::PKey::RSA.new(2048)
132
+ cert = OpenSSL::X509::Certificate.new
130
133
 
131
- cert.subject = OpenSSL::X509::Name.parse("/CN=127.0.0.1")
132
- cert.issuer = cert.subject
133
- cert.public_key = key.public_key
134
- cert.not_before = Time.now
135
- cert.not_after = Time.now + (60 * 60 * 24) # 1 day
136
- cert.serial = 0x0
137
- cert.version = 2
138
- cert.sign(key, OpenSSL::Digest.new("SHA256"))
134
+ cert.subject = OpenSSL::X509::Name.parse("/CN=127.0.0.1")
135
+ cert.issuer = cert.subject
136
+ cert.public_key = key.public_key
137
+ cert.not_before = Time.now
138
+ cert.not_after = Time.now + (60 * 60 * 24) # 1 day
139
+ cert.serial = 0x0
140
+ cert.version = 2
141
+ cert.sign(key, OpenSSL::Digest.new("SHA256"))
139
142
 
140
- cert_file = Tempfile.new("cert.pem")
141
- cert_file.write(cert.to_pem)
142
- cert_file.close
143
+ cert_file = Tempfile.new("cert.pem")
144
+ cert_file.write(cert.to_pem)
145
+ cert_file.close
143
146
 
144
- key_file = Tempfile.new("key.pem")
145
- key_file.write(key.to_pem)
146
- key_file.close
147
+ key_file = Tempfile.new("key.pem")
148
+ key_file.write(key.to_pem)
149
+ key_file.close
147
150
 
148
- [cert_file, key_file]
149
- end
151
+ [cert_file, key_file]
152
+ end
150
153
 
151
- def self.build_auth_context(api_key, callback_url, state: nil)
152
- oauth = OAuth2::Client.new(
153
- api_key,
154
- nil,
155
- site: SchwabRb::Constants::SCHWAB_BASE_URL,
156
- authorize_url: "/v1/oauth/authorize",
157
- connection_opts: { ssl: { verify: false } }
158
- )
154
+ def self.build_auth_context(api_key, callback_url, state: nil)
155
+ oauth = OAuth2::Client.new(
156
+ api_key,
157
+ nil,
158
+ site: SchwabRb::Constants::SCHWAB_BASE_URL,
159
+ authorize_url: "/v1/oauth/authorize",
160
+ connection_opts: { ssl: { verify: false } }
161
+ )
159
162
 
160
- auth_params = { redirect_uri: callback_url }
161
- auth_params[:state] = state if state
162
- authorization_url = oauth.auth_code.authorize_url(auth_params)
163
+ auth_params = { redirect_uri: callback_url }
164
+ auth_params[:state] = state if state
165
+ authorization_url = oauth.auth_code.authorize_url(auth_params)
163
166
 
164
- AuthContext.new(callback_url, authorization_url, state)
165
- end
167
+ AuthContext.new(callback_url, authorization_url, state)
168
+ end
166
169
 
167
- def self.client_from_received_url(
168
- api_key, app_secret, auth_context, received_url, token_path, enforce_enums: true
169
- )
170
- oauth = OAuth2::Client.new(
171
- api_key,
172
- app_secret,
173
- site: SchwabRb::Constants::SCHWAB_BASE_URL,
174
- token_url: "/v1/oauth/token"
170
+ def self.client_from_received_url(
171
+ api_key, app_secret, auth_context, received_url, token_path, enforce_enums: true
175
172
  )
176
- uri = URI.parse(received_url)
177
- params = URI.decode_www_form(uri.query).to_h
178
- authorization_code = params["code"]
173
+ oauth = OAuth2::Client.new(
174
+ api_key,
175
+ app_secret,
176
+ site: SchwabRb::Constants::SCHWAB_BASE_URL,
177
+ token_url: "/v1/oauth/token"
178
+ )
179
+ uri = URI.parse(received_url)
180
+ params = URI.decode_www_form(uri.query).to_h
181
+ authorization_code = params["code"]
179
182
 
180
- token = oauth.auth_code.get_token(authorization_code, redirect_uri: auth_context.callback_url)
183
+ token = oauth.auth_code.get_token(authorization_code, redirect_uri: auth_context.callback_url)
181
184
 
182
- metadata_manager = SchwabRb::Auth::TokenManager.from_oauth2_token(
183
- token,
184
- Time.now.to_i,
185
- token_path: token_path
186
- )
187
- metadata_manager.to_file
185
+ metadata_manager = SchwabRb::Auth::TokenManager.from_oauth2_token(
186
+ token,
187
+ Time.now.to_i,
188
+ token_path: token_path
189
+ )
190
+ metadata_manager.to_file
188
191
 
189
- session = OAuth2::AccessToken.new(
190
- oauth,
191
- token.token,
192
- refresh_token: token.refresh_token,
193
- expires_at: token.expires_at
194
- )
192
+ session = OAuth2::AccessToken.new(
193
+ oauth,
194
+ token.token,
195
+ refresh_token: token.refresh_token,
196
+ expires_at: token.expires_at
197
+ )
195
198
 
196
- SchwabRb::Client.new(
197
- api_key,
198
- app_secret,
199
- session,
200
- token_manager: metadata_manager,
201
- enforce_enums: enforce_enums
202
- )
199
+ SchwabRb::Client.new(
200
+ api_key,
201
+ app_secret,
202
+ session,
203
+ token_manager: metadata_manager,
204
+ enforce_enums: enforce_enums
205
+ )
206
+ end
203
207
  end
204
208
  end
@@ -1,30 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "oauth2"
2
4
 
3
- module SchwabRb::Auth
4
- def self.init_client_token_file(api_key, app_secret, token_path, enforce_enums: true)
5
- oauth = OAuth2::Client.new(
6
- api_key,
7
- app_secret,
8
- site: SchwabRb::Constants::SCHWAB_BASE_URL,
9
- token_url: "/v1/oauth/token"
10
- )
5
+ module SchwabRb
6
+ module Auth
7
+ def self.init_client_token_file(api_key, app_secret, token_path, enforce_enums: true)
8
+ oauth = OAuth2::Client.new(
9
+ api_key,
10
+ app_secret,
11
+ site: SchwabRb::Constants::SCHWAB_BASE_URL,
12
+ token_url: "/v1/oauth/token"
13
+ )
11
14
 
12
- metadata_manager = SchwabRb::Auth::TokenManager.from_file(token_path)
13
- token = metadata_manager.token
15
+ metadata_manager = SchwabRb::Auth::TokenManager.from_file(token_path)
16
+ token = metadata_manager.token
14
17
 
15
- session = OAuth2::AccessToken.new(
16
- oauth,
17
- token.token,
18
- refresh_token: token.refresh_token,
19
- expires_at: token.expires_at
20
- )
18
+ session = OAuth2::AccessToken.new(
19
+ oauth,
20
+ token.token,
21
+ refresh_token: token.refresh_token,
22
+ expires_at: token.expires_at
23
+ )
21
24
 
22
- SchwabRb::Client.new(
23
- api_key,
24
- app_secret,
25
- session,
26
- token_manager: metadata_manager,
27
- enforce_enums: enforce_enums
28
- )
25
+ SchwabRb::Client.new(
26
+ api_key,
27
+ app_secret,
28
+ session,
29
+ token_manager: metadata_manager,
30
+ enforce_enums: enforce_enums
31
+ )
32
+ end
29
33
  end
30
34
  end
@@ -1,55 +1,58 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sinatra"
2
4
  require "puma"
3
- require "thread"
4
5
 
5
- module SchwabRb::Auth
6
- class LoginFlowServer < Sinatra::Base
7
- class << self
8
- attr_accessor :queue
9
- end
6
+ module SchwabRb
7
+ module Auth
8
+ class LoginFlowServer < Sinatra::Base
9
+ class << self
10
+ attr_accessor :queue
11
+ end
10
12
 
11
- self.queue = Queue.new
13
+ self.queue = Queue.new
12
14
 
13
- disable :logging
15
+ disable :logging
14
16
 
15
- get "/status" do
16
- "running"
17
- end
17
+ get "/status" do
18
+ "running"
19
+ end
18
20
 
19
- def self.create_routes(root_path)
20
- get root_path do
21
- self.class.queue.push(request.url)
22
- "Callback received! You may now close this window/tab."
21
+ def self.create_routes(root_path)
22
+ get root_path do
23
+ self.class.queue.push(request.url)
24
+ "Callback received! You may now close this window/tab."
25
+ end
23
26
  end
24
- end
25
27
 
26
- def self.run_in_thread(callback_port: 4567, callback_path: "/", cert_file: nil, key_file: nil)
27
- create_routes(callback_path)
28
+ def self.run_in_thread(callback_port: 4567, callback_path: "/", cert_file: nil, key_file: nil)
29
+ create_routes(callback_path)
28
30
 
29
- thread = Thread.new do
30
- set :server, "puma"
31
- set :port, callback_port
32
- set :bind, "127.0.0.1"
31
+ thread = Thread.new do
32
+ set :server, "puma"
33
+ set :port, callback_port
34
+ set :bind, "127.0.0.1"
33
35
 
34
- ctx = Puma::MiniSSL::Context.new.tap do |ctx|
35
- ctx.key = key_file.path
36
- ctx.cert = cert_file.path
37
- ctx.verify_mode = Puma::MiniSSL::VERIFY_NONE
38
- end
36
+ ctx = Puma::MiniSSL::Context.new.tap do |ctx|
37
+ ctx.key = key_file.path
38
+ ctx.cert = cert_file.path
39
+ ctx.verify_mode = Puma::MiniSSL::VERIFY_NONE
40
+ end
39
41
 
40
- puts ctx.inspect
42
+ puts ctx.inspect
41
43
 
42
- Puma::Server.new(self).tap do |server|
43
- server.add_ssl_listener("127.0.0.1", callback_port, ctx)
44
- server.run
44
+ Puma::Server.new(self).tap do |server|
45
+ server.add_ssl_listener("127.0.0.1", callback_port, ctx)
46
+ server.run
47
+ end
45
48
  end
49
+ sleep 0.5
50
+ thread
46
51
  end
47
- sleep 0.5
48
- thread
49
- end
50
52
 
51
- def self.stop
52
- Thread.list.each { |t| t.exit if t != Thread.main }
53
+ def self.stop
54
+ Thread.list.each { |t| t.exit if t != Thread.main }
55
+ end
53
56
  end
54
57
  end
55
58
  end