roku_builder 3.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +101 -0
  5. data/Guardfile +21 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +282 -0
  8. data/bin/roku +152 -0
  9. data/config.json.example +28 -0
  10. data/lib/roku_builder.rb +32 -0
  11. data/lib/roku_builder/config_manager.rb +157 -0
  12. data/lib/roku_builder/controller.rb +582 -0
  13. data/lib/roku_builder/inspector.rb +90 -0
  14. data/lib/roku_builder/keyer.rb +52 -0
  15. data/lib/roku_builder/linker.rb +46 -0
  16. data/lib/roku_builder/loader.rb +197 -0
  17. data/lib/roku_builder/manifest_manager.rb +63 -0
  18. data/lib/roku_builder/monitor.rb +62 -0
  19. data/lib/roku_builder/navigator.rb +107 -0
  20. data/lib/roku_builder/packager.rb +47 -0
  21. data/lib/roku_builder/tester.rb +32 -0
  22. data/lib/roku_builder/util.rb +31 -0
  23. data/lib/roku_builder/version.rb +4 -0
  24. data/rakefile +8 -0
  25. data/roku_builder.gemspec +36 -0
  26. data/tests/roku_builder/config_manager_test.rb +400 -0
  27. data/tests/roku_builder/controller_test.rb +250 -0
  28. data/tests/roku_builder/inspector_test.rb +153 -0
  29. data/tests/roku_builder/keyer_test.rb +88 -0
  30. data/tests/roku_builder/linker_test.rb +37 -0
  31. data/tests/roku_builder/loader_test.rb +153 -0
  32. data/tests/roku_builder/manifest_manager_test.rb +25 -0
  33. data/tests/roku_builder/monitor_test.rb +34 -0
  34. data/tests/roku_builder/navigator_test.rb +72 -0
  35. data/tests/roku_builder/packager_test.rb +125 -0
  36. data/tests/roku_builder/test_files/controller_test/load_config_test.json +28 -0
  37. data/tests/roku_builder/test_files/controller_test/valid_config.json +28 -0
  38. data/tests/roku_builder/test_files/loader_test/c +0 -0
  39. data/tests/roku_builder/test_files/loader_test/manifest +0 -0
  40. data/tests/roku_builder/test_files/loader_test/source/a +0 -0
  41. data/tests/roku_builder/test_files/loader_test/source/b +0 -0
  42. data/tests/roku_builder/test_files/manifest_manager_test/manifest_template +2 -0
  43. data/tests/roku_builder/test_helper.rb +6 -0
  44. data/tests/roku_builder/tester_test.rb +33 -0
  45. data/tests/roku_builder/util_test.rb +23 -0
  46. metadata +286 -0
@@ -0,0 +1,90 @@
1
+ module RokuBuilder
2
+
3
+ # Collects information on a package for submission
4
+ class Inspector < Util
5
+
6
+ # Inspects the given pkg
7
+ # @param pkg [String] Path to the pkg to be inspected
8
+ # @param password [String] Password for the given pkg
9
+ # @return [Hash] Package information. Contains the following keys:
10
+ # * app_name
11
+ # * dev_id
12
+ # * creation_date
13
+ # * dev_zip
14
+ def inspect(pkg:, password:)
15
+
16
+ # upload new key with password
17
+ path = "/plugin_inspect"
18
+ conn = Faraday.new(url: @url) do |f|
19
+ f.request :digest, @dev_username, @dev_password
20
+ f.request :multipart
21
+ f.request :url_encoded
22
+ f.adapter Faraday.default_adapter
23
+ end
24
+ payload = {
25
+ mysubmit: "Inspect",
26
+ passwd: password,
27
+ archive: Faraday::UploadIO.new(pkg, 'application/octet-stream')
28
+ }
29
+ response = conn.post path, payload
30
+
31
+ app_name = /App Name:\s*<\/td>\s*<td>\s*<font[^>]*>([^<]*)<\/font>\s*<\/td>/.match(response.body)
32
+ dev_id = nil
33
+ creation_date = nil
34
+ dev_zip = nil
35
+ if app_name
36
+ app_name = app_name[1]
37
+ dev_id = /Dev ID:\s*<\/td>\s*<td>\s*<font[^>]*>([^<]*)<\/font>\s*<\/td>/.match(response.body)[1]
38
+ creation_date = /Creation Date:\s*<\/td>\s*<td>\s*<font[^>]*>\s*<script[^>]*>\s*var d = new Date\(([^\)]*)\)[^<]*<\/script><\/font>\s*<\/td>/.match(response.body.gsub("\n", ''))[1]
39
+ dev_zip = /dev.zip:\s*<\/td>\s*<td>\s*<font[^>]*>([^<]*)<\/font>\s*<\/td>/.match(response.body)[1]
40
+ else
41
+ app_name = /App Name:[^<]*<div[^>]*>([^<]*)<\/div>/.match(response.body)[1]
42
+ dev_id = /Dev ID:[^<]*<div[^>]*><font[^>]*>([^<]*)<\/font><\/div>/.match(response.body)[1]
43
+ creation_date = /new Date\(([^\/]*)\)/.match(response.body.gsub("\n", ''))[1]
44
+ dev_zip = /dev.zip:[^<]*<div[^>]*><font[^>]*>([^<]*)<\/font><\/div>/.match(response.body)[1]
45
+ end
46
+
47
+ return {app_name: app_name, dev_id: dev_id, creation_date: Time.at(creation_date.to_i).to_s, dev_zip: dev_zip}
48
+
49
+ end
50
+
51
+ # Capture a screencapture for the currently sideloaded app
52
+ # @return [Boolean] Success
53
+ def screencapture(out_folder:, out_file: nil)
54
+ path = "/plugin_inspect"
55
+ conn = Faraday.new(url: @url) do |f|
56
+ f.request :digest, @dev_username, @dev_password
57
+ f.request :multipart
58
+ f.request :url_encoded
59
+ f.adapter Faraday.default_adapter
60
+ end
61
+ payload = {
62
+ mysubmit: "Screenshot",
63
+ passwd: @dev_password,
64
+ archive: Faraday::UploadIO.new("/dev/null", 'application/octet-stream')
65
+ }
66
+ response = conn.post path, payload
67
+
68
+ path = /<img src="([^"]*)">/.match(response.body)
69
+ return false unless path
70
+ path = path[1]
71
+ unless out_file
72
+ out_file = /time=([^"]*)">/.match(response.body)
73
+ out_file = "dev_#{out_file[1]}.jpg" if out_file
74
+ end
75
+
76
+ conn = Faraday.new(url: @url) do |f|
77
+ f.request :digest, @dev_username, @dev_password
78
+ f.adapter Faraday.default_adapter
79
+ end
80
+
81
+ response = conn.get path
82
+
83
+ File.open(File.join(out_folder, out_file), "w") do |io|
84
+ io.write(response.body)
85
+ end
86
+ @logger.info "Screen captured to #{File.join(out_folder, out_file)}"
87
+ return response.success?
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,52 @@
1
+ module RokuBuilder
2
+
3
+ # Change or get dev key
4
+ class Keyer < Util
5
+
6
+ # Sets the key on the roku device
7
+ # @param keyed_pkg [String] Path for a package signed with the desired key
8
+ # @param password [String] Password for the package
9
+ # @return [Boolean] True if key changed, false otherwise
10
+ def rekey(keyed_pkg:, password:)
11
+ oldId = dev_id
12
+
13
+ # upload new key with password
14
+ path = "/plugin_inspect"
15
+ conn = Faraday.new(url: @url) do |f|
16
+ f.request :digest, @dev_username, @dev_password
17
+ f.request :multipart
18
+ f.request :url_encoded
19
+ f.adapter Faraday.default_adapter
20
+ end
21
+ payload = {
22
+ mysubmit: "Rekey",
23
+ passwd: password,
24
+ archive: Faraday::UploadIO.new(keyed_pkg, 'application/octet-stream')
25
+ }
26
+ response = conn.post path, payload
27
+
28
+ # check key
29
+ newId = dev_id
30
+ newId != oldId
31
+ end
32
+
33
+ # Get the current dev id
34
+ # @return [String] The current dev id
35
+ def dev_id
36
+ path = "/plugin_package"
37
+ conn = Faraday.new(url: @url) do |f|
38
+ f.request :digest, @dev_username, @dev_password
39
+ f.adapter Faraday.default_adapter
40
+ end
41
+ response = conn.get path
42
+
43
+ dev_id = /Your Dev ID:\s*<font[^>]*>([^<]*)<\/font>/.match(response.body)
44
+ if dev_id
45
+ dev_id = dev_id[1]
46
+ else
47
+ dev_id = /Your Dev ID:[^>]*<\/label> ([^<]*)/.match(response.body)[1]
48
+ end
49
+ dev_id
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ module RokuBuilder
2
+
3
+ # Launch application, sending parameters
4
+ class Linker < Util
5
+ # Deeplink to the currently sideloaded app
6
+ # @param options [String] Options string
7
+ # @note Options string should be formated like the following: "<key>:<value>[, <key>:<value>]*"
8
+ # @note Any options will be accepted and sent to the app
9
+ def link(options:)
10
+ path = "/launch/dev"
11
+ payload = {}
12
+ return false unless options
13
+ opts = options.split(/,\s*/)
14
+ opts.each do |opt|
15
+ opt = opt.split(":")
16
+ key = opt.shift.to_sym
17
+ value = opt.join(":")
18
+ payload[key] = value
19
+ end
20
+
21
+ unless payload.keys.count > 0
22
+ return false
23
+ end
24
+
25
+ path = "#{path}?#{parameterize(payload)}"
26
+ conn = Faraday.new(url: "#{@url}:8060") do |f|
27
+ f.request :digest, @dev_username, @dev_password
28
+ f.request :multipart
29
+ f.request :url_encoded
30
+ f.adapter Faraday.default_adapter
31
+ end
32
+
33
+ response = conn.post path
34
+ return response.success?
35
+ end
36
+
37
+ private
38
+
39
+ # Parameterize options to be sent to the app
40
+ # @param params [Hash] Parameters to be sent
41
+ # @return [String] Parameters as a string, URI escaped
42
+ def parameterize(params)
43
+ URI.escape(params.collect{|k,v| "#{k}=#{URI.escape(v, "?&")}"}.join('&'))
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,197 @@
1
+ module RokuBuilder
2
+
3
+ # Load/Unload/Build roku applications
4
+ class Loader < Util
5
+
6
+ # Sideload an app onto a roku device
7
+ # @param root_dir [String] Path to the root directory of the roku app
8
+ # @param branch [String] Branch of the git repository to sideload. Pass nil to use working directory. Default: nil
9
+ # @param update_manifest [Boolean] Flag to update the manifest file before sideloading. Default: false
10
+ # @param fetch [Boolean] Flag to fetch all remotes before sideloading. Default: false
11
+ # @param folders [Array<String>] Array of folders to be sideloaded. Pass nil to send all folders. Default: nil
12
+ # @param files [Array<String>] Array of files to be sideloaded. Pass nil to send all files. Default: nil
13
+ # @return [String] Build version on success, nil otherwise
14
+ def sideload(root_dir:, branch: nil, update_manifest: false, fetch: false, folders: nil, files: nil)
15
+ @root_dir = root_dir
16
+ result = nil
17
+ stash = nil
18
+ if branch
19
+ git = Git.open(@root_dir)
20
+ if fetch
21
+ for remote in git.remotes
22
+ git.fetch(remote)
23
+ end
24
+ end
25
+ end
26
+ current_dir = Dir.pwd
27
+ begin
28
+ if git and branch and branch != git.current_branch
29
+ Dir.chdir(@root_dir)
30
+ current_branch = git.current_branch
31
+ stash = git.branch.stashes.save("roku-builder-temp-stash")
32
+ git.checkout(branch)
33
+ end
34
+
35
+ # Update manifest
36
+ build_version = ""
37
+ if update_manifest
38
+ build_version = ManifestManager.update_build(root_dir: root_dir, logger: @logger)
39
+ else
40
+ build_version = ManifestManager.build_version(root_dir: root_dir, logger: @logger)
41
+ end
42
+
43
+ outfile = build(root_dir: root_dir, branch: branch, build_version: build_version, folders: folders, files: files)
44
+
45
+ path = "/plugin_install"
46
+
47
+ # Connect to roku and upload file
48
+ conn = Faraday.new(url: @url) do |f|
49
+ f.request :digest, @dev_username, @dev_password
50
+ f.request :multipart
51
+ f.request :url_encoded
52
+ f.adapter Faraday.default_adapter
53
+ end
54
+ payload = {
55
+ mysubmit: "Replace",
56
+ archive: Faraday::UploadIO.new(outfile, 'application/zip')
57
+ }
58
+ response = conn.post path, payload
59
+
60
+ # Cleanup
61
+ File.delete(outfile)
62
+
63
+ if git and current_branch
64
+ git.checkout(current_branch)
65
+ git.branch.stashes.apply if stash
66
+ end
67
+
68
+ if response.status == 200 and response.body =~ /Install Success/
69
+ result = build_version
70
+ end
71
+
72
+ rescue Git::GitExecuteError => e
73
+ @logger.error "Branch or ref does not exist"
74
+ @logger.error e.message
75
+ @logger.error e.backtrace
76
+ ensure
77
+ Dir.chdir(current_dir) unless current_dir == Dir.pwd
78
+ end
79
+ result
80
+ end
81
+
82
+
83
+ # Build an app to sideload later
84
+ # @param root_dir [String] Path to the root directory of the roku app
85
+ # @param branch [String] Branch of the git repository to sideload. Pass nil to use working directory. Default: nil
86
+ # @param build_version [String] Version to assigne to the build. If nil will pull the build version form the manifest. Default: nil
87
+ # @param outfile [String] Path for the output file. If nil will create a file in /tmp. Default: nil
88
+ # @param fetch [Boolean] Flag to fetch all remotes before sideloading. Default: false
89
+ # @param folders [Array<String>] Array of folders to be sideloaded. Pass nil to send all folders. Default: nil
90
+ # @param files [Array<String>] Array of files to be sideloaded. Pass nil to send all files. Default: nil
91
+ # @return [String] Path of the build
92
+ def build(root_dir:, branch: nil, build_version: nil, outfile: nil, fetch: false, folders: nil, files: nil)
93
+ @root_dir = root_dir
94
+ result = nil
95
+ stash = nil
96
+ if branch
97
+ git = Git.open(@root_dir)
98
+ if fetch
99
+ for remote in git.remotes
100
+ git.fetch(remote)
101
+ end
102
+ end
103
+ end
104
+ current_dir = Dir.pwd
105
+ begin
106
+ if git and branch and branch != git.current_branch
107
+ Dir.chdir(@root_dir)
108
+ current_branch = git.current_branch
109
+ stash = git.branch.stashes.save("roku-builder-temp-stash")
110
+ git.checkout(branch)
111
+ end
112
+
113
+ build_version = ManifestManager.build_version(root_dir: root_dir, logger: @logger) unless build_version
114
+ unless folders
115
+ folders = Dir.entries(root_dir).select {|entry| File.directory? File.join(root_dir, entry) and !(entry =='.' || entry == '..') }
116
+ end
117
+ unless files
118
+ files = Dir.entries(root_dir).select {|entry| File.file? File.join(root_dir, entry)}
119
+ end
120
+ outfile = "/tmp/build_#{build_version}.zip" unless outfile
121
+
122
+ File.delete(outfile) if File.exists?(outfile)
123
+ io = Zip::File.open(outfile, Zip::File::CREATE)
124
+
125
+ # Add folders to zip
126
+ folders.each do |folder|
127
+ base_folder = File.join(@root_dir, folder)
128
+ entries = Dir.entries(base_folder)
129
+ entries.delete(".")
130
+ entries.delete("..")
131
+ writeEntries(@root_dir, entries, folder, io)
132
+ end
133
+
134
+ # Add file to zip
135
+ writeEntries(@root_dir, files, "", io)
136
+
137
+ io.close()
138
+
139
+ if git and current_branch
140
+ git.checkout(current_branch)
141
+ git.branch.stashes.apply if stash
142
+ end
143
+ rescue Git::GitExecuteError => e
144
+ @logger.error "Branch or ref does not exist"
145
+ @logger.error e.message
146
+ @logger.error e.backtrace
147
+ ensure
148
+ Dir.chdir(current_dir) unless current_dir == Dir.pwd
149
+ end
150
+ outfile
151
+ end
152
+
153
+ # Remove the currently sideloaded app
154
+ def unload()
155
+ path = "/plugin_install"
156
+
157
+ # Connect to roku and upload file
158
+ conn = Faraday.new(url: @url) do |f|
159
+ f.headers['Content-Type'] = Faraday::Request::Multipart.mime_type
160
+ f.request :digest, @dev_username, @dev_password
161
+ f.request :multipart
162
+ f.request :url_encoded
163
+ f.adapter Faraday.default_adapter
164
+ end
165
+ payload = {
166
+ mysubmit: "Delete",
167
+ archive: ""
168
+ }
169
+ response = conn.post path, payload
170
+ if response.status == 200 and response.body =~ /Install Success/
171
+ return true
172
+ end
173
+ return false
174
+ end
175
+
176
+ private
177
+
178
+ # Recursively write directory contents to a zip archive
179
+ # @param root_dir [String] Path of the root directory
180
+ # @param entries [Array<String>] Array of file paths of files/directories to store in the zip archive
181
+ # @param path [String] The path of the current directory starting at the root directory
182
+ # @param io [IO] zip IO object
183
+ def writeEntries(root_dir, entries, path, io)
184
+ entries.each { |e|
185
+ zipFilePath = path == "" ? e : File.join(path, e)
186
+ diskFilePath = File.join(root_dir, zipFilePath)
187
+ if File.directory?(diskFilePath)
188
+ io.mkdir(zipFilePath)
189
+ subdir =Dir.entries(diskFilePath); subdir.delete("."); subdir.delete("..")
190
+ writeEntries(root_dir, subdir, zipFilePath, io)
191
+ else
192
+ io.get_output_stream(zipFilePath) { |f| f.puts(File.open(diskFilePath, "rb").read()) }
193
+ end
194
+ }
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,63 @@
1
+ module RokuBuilder
2
+
3
+ # Updates or retrives build version
4
+ class ManifestManager
5
+
6
+ # Updates the build version in the manifest file
7
+ # @param root_dir [String] Path to the root directory for the app
8
+ # @return [String] Build version on success, empty string otherwise
9
+ def self.update_build(root_dir:, logger:)
10
+
11
+ build_version = ""
12
+
13
+ temp_file = Tempfile.new('manifest')
14
+ path = File.join(root_dir, 'manifest')
15
+ begin
16
+ File.open(path, 'r') do |file|
17
+ file.each_line do |line|
18
+ if line.include?("build_version")
19
+
20
+ #Update build version.
21
+ build_version = line.split(".")
22
+ iteration = 0
23
+ if 2 == build_version.length
24
+ iteration = build_version[1].to_i + 1
25
+ build_version[0] = Time.now.strftime("%m%d%y")
26
+ build_version[1] = iteration
27
+ build_version = build_version.join(".")
28
+ else
29
+ #Use current date.
30
+ build_version = Time.now.strftime("%m%d%y")+".1"
31
+ end
32
+ temp_file.puts "build_version=#{build_version}"
33
+ else
34
+ temp_file.puts line
35
+ end
36
+ end
37
+ end
38
+ temp_file.rewind
39
+ FileUtils.cp(temp_file.path, path)
40
+ ensure
41
+ temp_file.close
42
+ temp_file.unlink
43
+ end
44
+ build_version
45
+ end
46
+
47
+ # Retrive the build version from the manifest file
48
+ # @param root_dir [String] Path to the root directory for the app
49
+ # @return [String] Build version on success, empty string otherwise
50
+ def self.build_version(root_dir:, logger:)
51
+ path = File.join(root_dir, 'manifest')
52
+ build_version = ""
53
+ File.open(path, 'r') do |file|
54
+ file.each_line do |line|
55
+ if line.include?("build_version")
56
+ build_version = line.split("=")[1].chomp
57
+ end
58
+ end
59
+ end
60
+ build_version
61
+ end
62
+ end
63
+ end