bluebox-boxcutter 0.0.14
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.
- data/.gitignore +1 -0
- data/CHANGELOG.md +23 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +51 -0
- data/README.md +36 -0
- data/Rakefile +14 -0
- data/bin/boxcutter +4 -0
- data/bluebox-boxcutter.gemspec +33 -0
- data/doc/screenshot.png +0 -0
- data/lib/bluebox-boxcutter.rb +94 -0
- data/lib/bluebox-boxcutter/cli.rb +111 -0
- data/lib/bluebox-boxcutter/git.rb +32 -0
- data/lib/bluebox-boxcutter/kvm.rb +36 -0
- data/lib/bluebox-boxcutter/machine.rb +84 -0
- data/lib/bluebox-boxcutter/password.rb +37 -0
- data/lib/bluebox-boxcutter/razor.rb +59 -0
- data/lib/bluebox-boxcutter/ui.rb +53 -0
- data/lib/bluebox-boxcutter/version.rb +3 -0
- data/spec/boxcutter_spec.rb +11 -0
- data/spec/fixtures/private/hosts.json +4 -0
- data/spec/fixtures/private/hosts/live_search_machines +4 -0
- data/spec/fixtures/private/hosts/show/123.json +13 -0
- data/spec/fixtures/private/machines/123/edit +18 -0
- data/spec/fixtures/private/service_password +13 -0
- data/spec/fixtures/private/service_passwords/fetch_password/163 +14 -0
- data/spec/fixtures/private/service_passwords/fetch_password/164 +14 -0
- data/spec/fixtures/private/service_passwords/fetch_password/165 +14 -0
- data/spec/git_spec.rb +17 -0
- data/spec/machine_spec.rb +46 -0
- data/spec/razor_spec.rb +28 -0
- data/spec/spec_helper.rb +91 -0
- data/spec/ui_spec.rb +82 -0
- metadata +223 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.gem
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
## v0.0.13 (August 22, 2013)
|
2
|
+
|
3
|
+
A rework of updating, you can now just update boxutter via 'boxcutter update' and it will
|
4
|
+
clone down the latest and greatest and update your local gem
|
5
|
+
|
6
|
+
### Improvements
|
7
|
+
* Pull request [#32](https://github.blueboxgrid.com/bluebox-tech/boxcutter/pull/32): [update] just update - Sam
|
8
|
+
|
9
|
+
## v0.0.12 (August 21, 2013)
|
10
|
+
|
11
|
+
You can now see if you have the latest version with: boxcutter check-for-update
|
12
|
+
|
13
|
+
### Improvements
|
14
|
+
* Pull request [#31](https://github.blueboxgrid.com/bluebox-tech/boxcutter/pull/31): [update_avail] version check against upstream - Sam
|
15
|
+
|
16
|
+
## v0.0.11 (August 21, 2013)
|
17
|
+
|
18
|
+
### Improvements
|
19
|
+
* Pull request [#30](https://github.blueboxgrid.com/bluebox-tech/boxcutter/pull/30): [more_info] show host info when rebooting or razoring - Sam
|
20
|
+
* Pull request [#29](https://github.blueboxgrid.com/bluebox-tech/boxcutter/pull/29): rm hwagent token - Sam
|
21
|
+
|
22
|
+
|
23
|
+
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
bluebox-boxcutter (0.0.8)
|
5
|
+
colorize
|
6
|
+
commander
|
7
|
+
httparty
|
8
|
+
mash
|
9
|
+
nokogiri
|
10
|
+
spunkmeyer (>= 0.0.5)
|
11
|
+
terminal-table
|
12
|
+
|
13
|
+
GEM
|
14
|
+
remote: https://rubygems.org/
|
15
|
+
specs:
|
16
|
+
colorize (0.5.8)
|
17
|
+
commander (4.1.4)
|
18
|
+
highline (~> 1.6.11)
|
19
|
+
diff-lcs (1.2.4)
|
20
|
+
highline (1.6.19)
|
21
|
+
httparty (0.11.0)
|
22
|
+
multi_json (~> 1.0)
|
23
|
+
multi_xml (>= 0.5.2)
|
24
|
+
mash (0.1.1)
|
25
|
+
mini_portile (0.5.1)
|
26
|
+
multi_json (1.7.7)
|
27
|
+
multi_xml (0.5.4)
|
28
|
+
nokogiri (1.6.0)
|
29
|
+
mini_portile (~> 0.5.0)
|
30
|
+
rake (10.1.0)
|
31
|
+
rspec (2.14.1)
|
32
|
+
rspec-core (~> 2.14.0)
|
33
|
+
rspec-expectations (~> 2.14.0)
|
34
|
+
rspec-mocks (~> 2.14.0)
|
35
|
+
rspec-core (2.14.4)
|
36
|
+
rspec-expectations (2.14.0)
|
37
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
38
|
+
rspec-mocks (2.14.1)
|
39
|
+
spunkmeyer (0.0.5)
|
40
|
+
colorize
|
41
|
+
sqlite3
|
42
|
+
sqlite3 (1.3.7)
|
43
|
+
terminal-table (1.4.5)
|
44
|
+
|
45
|
+
PLATFORMS
|
46
|
+
ruby
|
47
|
+
|
48
|
+
DEPENDENCIES
|
49
|
+
bluebox-boxcutter!
|
50
|
+
rake
|
51
|
+
rspec
|
data/README.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# boxcutter
|
2
|
+
|
3
|
+
A command-line interface to BoxPanel.
|
4
|
+
|
5
|
+
# prerequisites
|
6
|
+
|
7
|
+
This tool requres `ruby --version` >= `1.9.3`.
|
8
|
+
If you are not already running a suitable version, [rbenv](https://github.com/sstephenson/rbenv) is recommended.
|
9
|
+
|
10
|
+
# installation
|
11
|
+
|
12
|
+
git clone git@github.blueboxgrid.com:bluebox-tech/boxcutter.git
|
13
|
+
cd boxcutter
|
14
|
+
gem install rspec
|
15
|
+
rake install
|
16
|
+
|
17
|
+
# usage
|
18
|
+
|
19
|
+
*You must be logged into BoxPanel in the Chrome browser - boxcutter auths with your Chrome cookies*
|
20
|
+
|
21
|
+
```bash
|
22
|
+
boxcutter help # usage
|
23
|
+
boxcutter machine-search app # search for machine names given a substring
|
24
|
+
boxcutter machine-show ds590 # show machine details given its name
|
25
|
+
boxcutter password "kvm[2-5]" # show passwords given a password name regex
|
26
|
+
boxcutter kvm kvm9.sea03 # open a kvm console given the password name
|
27
|
+
boxcutter reboot ds590 # reboot a machine
|
28
|
+
boxcutter razor-tags # show available razor tags
|
29
|
+
boxcutter razor ds590 ubuntu-precise # razor a machine
|
30
|
+
boxcutter razor-log ds590 # show razor log messages
|
31
|
+
```
|
32
|
+
|
33
|
+
# example
|
34
|
+
|
35
|
+

|
36
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
$: << "#{File.dirname __FILE__}/lib"
|
3
|
+
require 'bluebox-boxcutter/version'
|
4
|
+
|
5
|
+
task :install do
|
6
|
+
sh %{gem build bluebox-boxcutter.gemspec}
|
7
|
+
sh %{gem uninstall -x bluebox-boxcutter} if system 'gem which bluebox-boxcutter'
|
8
|
+
sh %{gem install bluebox-boxcutter-#{Boxcutter::VERSION}.gem --no-ri --no-rdoc}
|
9
|
+
end
|
10
|
+
|
11
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
12
|
+
t.pattern = Dir.glob('spec/**/*_spec.rb')
|
13
|
+
t.rspec_opts = '--color'
|
14
|
+
end
|
data/bin/boxcutter
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#
|
2
|
+
# -*- encoding: utf-8 -*-
|
3
|
+
$: << "#{File.dirname __FILE__}/lib"
|
4
|
+
require 'bluebox-boxcutter/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'bluebox-boxcutter'
|
8
|
+
s.version = Boxcutter::VERSION
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = [ 'Sam Cooper', 'Tim Miller' ]
|
11
|
+
s.email = [ '' ]
|
12
|
+
s.homepage = 'https://github.blueboxgrid.com/bluebox-tech/boxcutter'
|
13
|
+
s.summary = %q{cli for boxpanel}
|
14
|
+
s.description = %q{cli for boxpanel.}
|
15
|
+
|
16
|
+
s.required_ruby_version = ">= 1.9.1"
|
17
|
+
s.required_rubygems_version = ">= 1.3.7"
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
s.test_files = `git ls-files -- {spec}/*`.split("\n")
|
21
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
22
|
+
s.require_paths = ['lib']
|
23
|
+
|
24
|
+
s.add_runtime_dependency 'colorize', '>= 0'
|
25
|
+
s.add_runtime_dependency 'commander', '>= 0'
|
26
|
+
s.add_runtime_dependency 'httparty', '>= 0'
|
27
|
+
s.add_runtime_dependency 'mash', '>= 0'
|
28
|
+
s.add_runtime_dependency 'nokogiri', '>= 0'
|
29
|
+
s.add_runtime_dependency 'spunkmeyer', '>= 0.0.5'
|
30
|
+
s.add_runtime_dependency 'terminal-table', '>= 0'
|
31
|
+
s.add_development_dependency 'rake', '>= 0'
|
32
|
+
s.add_development_dependency 'rspec', '>= 0'
|
33
|
+
end
|
data/doc/screenshot.png
ADDED
Binary file
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'bluebox-boxcutter/kvm'
|
3
|
+
require 'bluebox-boxcutter/machine'
|
4
|
+
require 'bluebox-boxcutter/password'
|
5
|
+
require 'bluebox-boxcutter/razor'
|
6
|
+
require 'bluebox-boxcutter/ui'
|
7
|
+
require 'bluebox-boxcutter/version'
|
8
|
+
require 'bluebox-boxcutter/git'
|
9
|
+
require 'nokogiri'
|
10
|
+
require 'spunkmeyer'
|
11
|
+
require 'mash'
|
12
|
+
|
13
|
+
module Boxcutter
|
14
|
+
|
15
|
+
def self.version
|
16
|
+
eval(File.read("#{File.dirname __FILE__}/../bluebox-boxcutter.gemspec")).version.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.install(git_repo)
|
20
|
+
|
21
|
+
# if we're passed a file path
|
22
|
+
if git_repo.match /^(\/.*)+\/?/
|
23
|
+
Dir.chdir git_repo
|
24
|
+
`rake install`
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.update
|
29
|
+
Boxcutter::Git::clone
|
30
|
+
upstream_version = Boxcutter::Git::upstream_version
|
31
|
+
our_version = Boxcutter::VERSION
|
32
|
+
if Gem::Version.new(upstream_version) > Gem::Version.new(our_version)
|
33
|
+
Boxcutter::install(Boxcutter::Git::LOCAL_CLONE)
|
34
|
+
end
|
35
|
+
Boxcutter::Git::rm_clone
|
36
|
+
end
|
37
|
+
|
38
|
+
# flatten any child hashes into top level hash
|
39
|
+
# e.g. {"x" => { "a" => "b" }
|
40
|
+
# will become {"x_a" => "b" }
|
41
|
+
def self.flatten_hash(hash, parent_key = "", new_hash = { })
|
42
|
+
hash.each do |key,value|
|
43
|
+
key_string = parent_key.empty? ? key : "#{parent_key}_#{key}"
|
44
|
+
if value.class == Hash
|
45
|
+
flatten_hash(value, key_string, new_hash)
|
46
|
+
else
|
47
|
+
new_hash.merge!({ key_string => value })
|
48
|
+
end
|
49
|
+
end
|
50
|
+
new_hash
|
51
|
+
end
|
52
|
+
|
53
|
+
# a boxpanel HTTP client, which auths with cookies from one's browser.
|
54
|
+
class Boxpanel
|
55
|
+
include HTTParty
|
56
|
+
|
57
|
+
BASE_URI = 'https://boxpanel.bluebox.net/'
|
58
|
+
base_uri BASE_URI
|
59
|
+
|
60
|
+
# get cookies from browser.
|
61
|
+
def self.cookie_header
|
62
|
+
cookies = Spunkmeyer.cookies BASE_URI
|
63
|
+
cookies.to_a.map { |c| "#{c.first}=#{c.last[:value]}" }.join '; '
|
64
|
+
end
|
65
|
+
|
66
|
+
# wrap the default get method with auth and error handling.
|
67
|
+
def self.get(path, options={}, &block)
|
68
|
+
options[:headers] ||= {}
|
69
|
+
options[:headers]['Cookie'] = cookie_header
|
70
|
+
resp = super path, options, &block
|
71
|
+
raise "request failed with #{resp.code}: #{path}" unless resp.code == 200
|
72
|
+
resp.body
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.get_html(path, options={}, &block)
|
76
|
+
Nokogiri::HTML Boxcutter::Boxpanel::get(path, options, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
# wrap the default post method with auth and error handling.
|
80
|
+
def self.post(path, options={}, &block)
|
81
|
+
options[:headers] ||= {}
|
82
|
+
options[:headers]['Cookie'] = cookie_header
|
83
|
+
resp = super path, options, &block
|
84
|
+
raise "post request failed with #{resp.code}: #{path}" unless resp.code == 200
|
85
|
+
resp.body
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.machine_url(machine_id)
|
89
|
+
BASE_URI + "private/machines/#{machine_id}/edit"
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'commander/import'
|
3
|
+
|
4
|
+
program :name, 'boxcutter'
|
5
|
+
program :version, Boxcutter::VERSION
|
6
|
+
program :description, 'CLI to boxpanel'
|
7
|
+
|
8
|
+
global_option('-y', '--yes', 'assume yes to all prompts') { Boxcutter.yes! }
|
9
|
+
global_option('-c', '--no-colors', 'no colors in stdout') { Boxcutter.no_colors! }
|
10
|
+
|
11
|
+
command 'machine-search'.to_sym do |c|
|
12
|
+
c.syntax ='boxcutter machine-search HOSTNAME_REGEX'
|
13
|
+
c.description = 'search for hostnames matching a regex'
|
14
|
+
c.action do |args, options|
|
15
|
+
Boxcutter.error! "missing regex: #{c.syntax}" unless args.length == 1
|
16
|
+
puts Boxcutter::table Boxcutter::Machine.search args.first
|
17
|
+
end
|
18
|
+
end
|
19
|
+
alias_command :'msearch', :'machine-search'
|
20
|
+
|
21
|
+
command 'machine-show'.to_sym do |c|
|
22
|
+
c.syntax ='boxcutter machine-show HOSTNAME'
|
23
|
+
c.description = "show a host's attributes"
|
24
|
+
c.action do |args, options|
|
25
|
+
Boxcutter.error! "missing hostname: #{c.syntax}" unless args.length == 1
|
26
|
+
puts Boxcutter::table Boxcutter::Machine.get args.first
|
27
|
+
end
|
28
|
+
end
|
29
|
+
alias_command :'mshow', :'machine-show'
|
30
|
+
|
31
|
+
command 'password'.to_sym do |c|
|
32
|
+
c.syntax ='boxcutter password NAME_REGEX'
|
33
|
+
c.description = 'search for passowords matching a given name regexp'
|
34
|
+
c.action do |args, options|
|
35
|
+
Boxcutter.error! "missing password regexp: #{c.syntax}" unless args.length == 1
|
36
|
+
puts Boxcutter::table Boxcutter::Password.search args.first
|
37
|
+
end
|
38
|
+
end
|
39
|
+
alias_command :'pw', :'password'
|
40
|
+
|
41
|
+
command 'kvm'.to_sym do |c|
|
42
|
+
c.syntax = 'boxcutter kvm PASSWORD_NAME_REGEX'
|
43
|
+
c.description = 'start a kvm console by password name'
|
44
|
+
c.action do |args, options|
|
45
|
+
Boxcutter.error! "missing kvm name: #{c.syntax}" unless args.length == 1
|
46
|
+
creds = Boxcutter::Password.search args.first
|
47
|
+
puts Boxcutter::table creds
|
48
|
+
error! "password regex is not unique: #{args.first}" if creds.length > 2
|
49
|
+
Boxcutter::KVM.start creds.last[1], creds.last[2], creds.last[3]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
command 'reboot'.to_sym do |c|
|
54
|
+
c.syntax = 'boxcutter reboot HOSTNAME'
|
55
|
+
c.description = 'reboot a host'
|
56
|
+
c.action do |args, options|
|
57
|
+
Boxcutter.error! "missing hostname: #{c.syntax}" unless args.length == 1
|
58
|
+
machine = Boxcutter::Machine.get args.first
|
59
|
+
Boxcutter::Machine::machine_summary(machine).each { |msg| Boxcutter.msg msg }
|
60
|
+
Boxcutter.confirm "this will reboot #{machine[:hostname]}!" do
|
61
|
+
Boxcutter::Machine.power_cycle! machine
|
62
|
+
Boxcutter::msg "reboot task issued for #{machine[:hostname]}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
command 'razor'.to_sym do |c|
|
68
|
+
c.syntax = 'boxcutter razor HOSTNAME RAZOR_TAG_NAME'
|
69
|
+
c.description = 'reimage a host with razor'
|
70
|
+
c.action do |args, options|
|
71
|
+
machine = Boxcutter::Machine.get(args.first)
|
72
|
+
Boxcutter.error! "must provide hostname and razor tag name" unless args.length == 2
|
73
|
+
Boxcutter::Machine::machine_summary(machine).each { |msg| Boxcutter.msg msg }
|
74
|
+
Boxcutter.confirm "this will re-image #{args.first}!" do
|
75
|
+
Boxcutter::Razor.image! machine, args.last
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
command 'razor-tags'.to_sym do |c|
|
81
|
+
c.syntax = 'boxcutter razor-tags'
|
82
|
+
c.description = 'show available razor tags'
|
83
|
+
c.action do
|
84
|
+
tags = Boxcutter::Razor::StubbleClient.tags
|
85
|
+
puts Boxcutter::table([[:name]] + tags.map { |t| [ t ] })
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
command 'razor-log'.to_sym do |c|
|
90
|
+
c.syntax = 'boxcutter razor-log HOSTNAME'
|
91
|
+
c.description = 'show current razor log for a host'
|
92
|
+
c.action do |args, options|
|
93
|
+
Boxcutter::error! "must provide hostname" unless args.length == 1
|
94
|
+
logs = Boxcutter::Razor::StubbleClient.log(Boxcutter::Machine::eth0_mac(Boxcutter::Machine.get(args.first)))
|
95
|
+
if logs.any?
|
96
|
+
logs.each { |l| l['timestamp'] = DateTime.strptime(l['timestamp'].to_s,'%s').to_s if l['timestamp'] }
|
97
|
+
logs.each { |l| l.delete 'node_uuid' if l['node_uuid'] }
|
98
|
+
puts Boxcutter::table_of_hashes(logs)
|
99
|
+
else
|
100
|
+
Boxcutter::error! "no logs for #{args.first}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
command 'update'.to_sym do |c|
|
106
|
+
c.syntax = 'boxcutter update'
|
107
|
+
c.description = 'update to the latest version'
|
108
|
+
c.action do |args, options|
|
109
|
+
Boxcutter::update
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Boxcutter
|
2
|
+
module Git
|
3
|
+
|
4
|
+
REPO = "git@github.blueboxgrid.com:bluebox-tech/boxcutter.git"
|
5
|
+
LOCAL_CLONE = "/tmp/boxcutter_gem"
|
6
|
+
|
7
|
+
def self.upstream_version
|
8
|
+
File.read("#{LOCAL_CLONE}/lib/bluebox-boxcutter/version.rb").match(/VERSION\s?=\s?['"](.*)['"]/)[1]
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.clone
|
12
|
+
if git?
|
13
|
+
`git clone #{REPO} #{LOCAL_CLONE} 2>&1 > /dev/null`
|
14
|
+
if $?.to_i != 0
|
15
|
+
Boxcutter.error! "Problem cloning git repo: #{REPO} to #{LOCAL_CLONE}"
|
16
|
+
end
|
17
|
+
else
|
18
|
+
Boxcutter.error! "Could not find git"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.rm_clone
|
23
|
+
FileUtils.rm_rf LOCAL_CLONE
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.git?
|
27
|
+
`git --version`
|
28
|
+
$?.to_i == 0
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Boxcutter
|
5
|
+
module KVM
|
6
|
+
|
7
|
+
# given a kvm url and creds, start the java kvm console thingy.
|
8
|
+
def self.start(url, user, password)
|
9
|
+
u = URI url
|
10
|
+
base, path = [ "#{u.scheme}://#{u.host}", u.path ]
|
11
|
+
path = '/auth.asp' # path is missing from some of the password entries.
|
12
|
+
|
13
|
+
client = Class.new do
|
14
|
+
include HTTParty
|
15
|
+
base_uri base
|
16
|
+
end
|
17
|
+
|
18
|
+
response = client.post(path, :body => {:login => user, :password => password, :action_login => 'Login'})
|
19
|
+
cookie = response.request.options[:headers]['Cookie']
|
20
|
+
jnlp_contents = client.get(extract_jnlp_path(response.body), :headers => {'Cookie' => cookie}).body
|
21
|
+
run_jnlp(jnlp_contents)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.extract_jnlp_path(html)
|
25
|
+
Nokogiri::HTML(html).css('a').map { |a| a['href'] }.select { |a| a =~ /spider.jnlp/ }.first
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.run_jnlp(contents)
|
29
|
+
Tempfile.new('kvm-webstart').tap do |f|
|
30
|
+
f.write contents
|
31
|
+
f.close
|
32
|
+
`javaws #{f.path} &`
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|