trmnl_preview 0.30.2 → 0.30.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 685a69fdb40d83171f2f8cb17d89b6954fae9c97f9f551d4210d228acbdb0690
4
- data.tar.gz: e666f0a63344dc33447eb70cabf3202bab6c6030f967716e3b50954583e222e7
3
+ metadata.gz: be8279b91f8e33f4750e7176131b29d393fe8a920f4313bcd6770af6fca278dc
4
+ data.tar.gz: aef27ba9bf23146aa0a52a2b8374e5c521f90b20a164247696a290c8d99b161e
5
5
  SHA512:
6
- metadata.gz: a53e8130820062a99d94d25b2a140506baefccb9e2747c30c46d2cc7795a96c423c4c537c520514c3706a9aafacd1cb215957fdd3af6048b5145632ddb24058a
7
- data.tar.gz: ebbaa2038fb76c066394dd98218a5305741d2b274763ddc2d41ad4151fc096763ec4593338f364d71807f032687264341e0585f17cf630684276101d757c6f78
6
+ metadata.gz: 76dd26fb7ad267c2a62ddc34e0ad5b220a3adea743dcfe2aac96b3de4d915c8d1a29cbf4b39bb0900c395ddb781015269f7fa913b278f79aec56010c790a82e5
7
+ data.tar.gz: 26f62dff933de915714295a8ae80530a2a567dabf4e4f676559dc5c20fc18c9fb780a3659fa9d5dadf5e63afaf6b4117989a16177727af19820a8290c26beca9
data/CHANGELOG.md CHANGED
@@ -1,6 +1,11 @@
1
1
 
2
2
  # Changelog
3
3
 
4
+ ## 0.30.3
5
+
6
+ - `trmnlp test` serves the Framework's stylesheets, scripts and fonts from a cache in `~/.cache/trmnl/assets`, downloaded from trmnl.com once and kept as long as trmnl.com says. A run no longer fails when trmnl.com is slow or down, as long as the files were fetched before; when a file is due for a refresh and trmnl.com cannot be reached, the copy on disk is used.
7
+ - `trmnlp test` also serves the Google Fonts stylesheet the Framework links for Inter, and its font files, from the same cache, asked for as Firefox asks so Google sends the same fonts. A test page now loads nothing from outside unless the recipe itself does.
8
+
4
9
  ## 0.30.2
5
10
 
6
11
  - `trmnlp serve`'s HTML preview of a device TRMNL scales runs the page's scripts at the CSS size too, then shows the page at the device's size and stops its timers, as the PNG and the device do. Before, a table showed 8 of 20 rows on a TRMNL X where the device shows 15.
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'net/http'
7
+ require 'socket'
8
+
9
+ require_relative '../framework_version'
10
+
11
+ module TRMNLP
12
+ module Testing
13
+ # Serves the Framework's files and Google Fonts to test pages from disk, for as long as their hosts say.
14
+ class AssetCache
15
+ HOST = FrameworkVersion::DEFAULT_ASSET_HOST
16
+ FRAMEWORK_LINK = %r{#{Regexp.escape(HOST)}(?=/(?:css|js)/)}
17
+ GOOGLE_FONTS_LINK = %r{https://(fonts\.(?:googleapis|gstatic)\.com)/}
18
+ SAFE_PATH = %r{\A/(?:[\w.-]+/)*[\w.-]+\z}
19
+ DOWNLOAD_TIMEOUT_SECONDS = 10
20
+ BAD_GATEWAY = "HTTP/1.1 502 Bad Gateway\r\ncontent-length: 0\r\nconnection: close\r\n\r\n"
21
+
22
+ def initialize(dir:, clock: -> { Time.now })
23
+ @dir = dir.to_s
24
+ @clock = clock
25
+ end
26
+
27
+ def start
28
+ @server = TCPServer.new('127.0.0.1', 0)
29
+ @thread = Thread.new { loop { Thread.new(@server.accept) { serve(it) } } }
30
+ self
31
+ end
32
+
33
+ def stop
34
+ @thread&.kill
35
+ @server&.close
36
+ end
37
+
38
+ def url = "http://127.0.0.1:#{@server.addr[1]}"
39
+
40
+ def point_framework_links_at_cache(html) = point_google_fonts_at_cache(html.gsub(FRAMEWORK_LINK, url))
41
+
42
+ private
43
+
44
+ def point_google_fonts_at_cache(text) = text.gsub(GOOGLE_FONTS_LINK) { "#{url}/#{::Regexp.last_match(1)}/" }
45
+
46
+ def serve(socket)
47
+ path, query = socket.gets.to_s.split[1].to_s.split('?', 2)
48
+ user_agent = nil
49
+ while (line = socket.gets) && !line.strip.empty?
50
+ user_agent = line.split(':', 2).last.strip if line.downcase.start_with?('user-agent:')
51
+ end
52
+ asset = !path.to_s.include?('..') && path.to_s.match?(SAFE_PATH) && asset(path, query, user_agent)
53
+ socket.write(asset ? response(asset) : BAD_GATEWAY)
54
+ rescue IOError, SystemCallError
55
+ nil
56
+ ensure
57
+ socket.close unless socket.closed?
58
+ end
59
+
60
+ def asset(path, query, user_agent)
61
+ key = query ? "#{path}.#{Digest::SHA256.hexdigest(query)[0, 16]}" : path
62
+ cached = cached(key)
63
+ return cached if cached && cached['expires_at'] > @clock.call.to_i
64
+
65
+ download(key, URI("#{upstream(path)}#{"?#{query}" if query}"), user_agent) || cached
66
+ end
67
+
68
+ # A Google Fonts path starts with its host, /fonts.gstatic.com/s/...; any other path is trmnl.com's.
69
+ def upstream(path)
70
+ host, rest = path.delete_prefix('/').split('/', 2)
71
+ host.match?(/\Afonts\.(googleapis|gstatic)\.com\z/) ? "https://#{host}/#{rest}" : "#{HOST}#{path}"
72
+ end
73
+
74
+ def cached(key)
75
+ meta = JSON.parse(File.read(file(key, '.json')))
76
+ meta.merge('body' => File.binread(file(key)))
77
+ rescue Errno::ENOENT, JSON::ParserError
78
+ nil
79
+ end
80
+
81
+ # trmnl.com sends its CORS header only to a request with an Origin; Google picks the font format by User-Agent.
82
+ def download(key, uri, user_agent)
83
+ headers = { 'Origin' => url, 'User-Agent' => user_agent }.compact
84
+ timeouts = { open_timeout: DOWNLOAD_TIMEOUT_SECONDS, read_timeout: DOWNLOAD_TIMEOUT_SECONDS }
85
+ answer = Net::HTTP.start(uri.host, uri.port, use_ssl: true, **timeouts) { it.get(uri.request_uri, headers) }
86
+ answer.is_a?(Net::HTTPSuccess) ? cache_download(key, answer) : nil
87
+ rescue StandardError
88
+ nil
89
+ end
90
+
91
+ def cache_download(path, answer)
92
+ meta = { 'content_type' => answer['content-type'], 'cache_control' => answer['cache-control'],
93
+ 'allow_origin' => answer['access-control-allow-origin'],
94
+ 'expires_at' => @clock.call.to_i + answer['cache-control'].to_s[/max-age=(\d+)/, 1].to_i }
95
+ write_to_disk(path, answer.body, meta)
96
+ meta.merge('body' => answer.body)
97
+ end
98
+
99
+ def write_to_disk(path, body, meta)
100
+ write(file(path), body)
101
+ write(file(path, '.json'), JSON.generate(meta))
102
+ rescue SystemCallError
103
+ nil
104
+ end
105
+
106
+ # Written beside the file and renamed over it, so another run reading the cache sees a whole file.
107
+ def write(path, content)
108
+ FileUtils.mkdir_p(File.dirname(path))
109
+ temporary = "#{path}.#{Process.pid}.#{Thread.current.object_id}"
110
+ File.binwrite(temporary, content)
111
+ File.rename(temporary, path)
112
+ end
113
+
114
+ def file(path, extension = '') = File.join(@dir, "#{path}#{extension}")
115
+
116
+ # A stylesheet's Google font files are pointed at the cache as it is served, since the cache's port changes.
117
+ def response(asset)
118
+ stylesheet = asset['content_type'].to_s.start_with?('text/css')
119
+ body = stylesheet ? point_google_fonts_at_cache(asset['body']) : asset['body']
120
+ headers = { 'content-type' => asset['content_type'], 'cache-control' => asset['cache_control'],
121
+ 'access-control-allow-origin' => asset['allow_origin'],
122
+ 'content-length' => body.bytesize, 'connection' => 'close' }.compact
123
+ "HTTP/1.1 200 OK\r\n#{headers.map { "#{it[0]}: #{it[1]}\r\n" }.join}\r\n#{body}"
124
+ end
125
+ end
126
+ end
127
+ end
@@ -13,9 +13,10 @@ module TRMNLP
13
13
  # The Firefox one example renders in, checked out of the pool on first use and back in by #release.
14
14
  # Pages are drawn through ScreenGenerator and Screenshot like `trmnlp build --png`'s, but opened from page_server.
15
15
  class Browser
16
- def initialize(pool:, page_server:)
16
+ def initialize(pool:, page_server:, asset_cache:)
17
17
  @pool = pool
18
18
  @page_server = page_server
19
+ @asset_cache = asset_cache
19
20
  @screenshot = Screenshot.new(pool:)
20
21
  @pages = {}.compare_by_identity
21
22
  @fresh_browsers = []
@@ -24,13 +25,15 @@ module TRMNLP
24
25
  # A Firefox of its own, with nothing cached, closed when this browser is released.
25
26
  def fresh
26
27
  pool = BrowserPool.new(driver_factory: FirefoxDriver.method(:build), max_size: 1)
27
- self.class.new(pool:, page_server: @page_server).tap { @fresh_browsers << [it, pool] }
28
+ browser = self.class.new(pool:, page_server: @page_server, asset_cache: @asset_cache)
29
+ browser.tap { @fresh_browsers << [it, pool] }
28
30
  end
29
31
 
30
32
  # Loads the screen's page; its picture is only taken when a test asks for it.
31
33
  def load(screen)
32
34
  generator = generator(screen)
33
- @pages[screen] = [generator.html, generator.width, generator.height, @page_server.add(generator.html)]
35
+ html = @asset_cache.point_framework_links_at_cache(generator.html)
36
+ @pages[screen] = [html, generator.width, generator.height, @page_server.add(html)]
34
37
  show(screen)
35
38
  end
36
39
 
@@ -8,6 +8,7 @@ require 'capybara/rspec/matchers' # after rspec/expectations, or its matchers lo
8
8
  require_relative '../../trmnlp'
9
9
  require_relative '../browser_pool'
10
10
  require_relative '../firefox_driver'
11
+ require_relative 'asset_cache'
11
12
  require_relative 'browser'
12
13
  require_relative 'certificate_authority'
13
14
  require_relative 'ink_count'
@@ -33,6 +34,11 @@ module TRMNLP
33
34
  @browser_pool ||= BrowserPool.new(driver_factory: FirefoxDriver.method(:build), max_size: 1)
34
35
  end
35
36
 
37
+ def self.asset_cache
38
+ @asset_cache ||= AssetCache.new(dir: Paths.new(plugin_dir).cache_dir.join('assets')).start
39
+ .tap { |cache| at_exit { cache.stop } }
40
+ end
41
+
36
42
  def self.page_server
37
43
  @page_server ||= PageServer.new.start.tap { |server| at_exit { server.stop } }
38
44
  end
@@ -41,7 +47,8 @@ module TRMNLP
41
47
  def trmnl = @trmnl ||= Plugin.new(Testing.plugin_dir, browser: trmnl_browser, authority: Testing.authority)
42
48
 
43
49
  def trmnl_browser
44
- @trmnl_browser ||= Browser.new(pool: Testing.browser_pool, page_server: Testing.page_server)
50
+ @trmnl_browser ||= Browser.new(pool: Testing.browser_pool, page_server: Testing.page_server,
51
+ asset_cache: Testing.asset_cache)
45
52
  end
46
53
  end
47
54
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRMNLP
4
- VERSION = '0.30.2'
4
+ VERSION = '0.30.3'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trmnl_preview
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.2
4
+ version: 0.30.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rockwell Schrock
@@ -476,6 +476,7 @@ files:
476
476
  - lib/trmnlp/screen_generator.rb
477
477
  - lib/trmnlp/screenshot.rb
478
478
  - lib/trmnlp/select_field_choices.rb
479
+ - lib/trmnlp/testing/asset_cache.rb
479
480
  - lib/trmnlp/testing/browser.rb
480
481
  - lib/trmnlp/testing/certificate_authority.rb
481
482
  - lib/trmnlp/testing/device_models.rb